@clipwright/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/dist/account.d.ts +49 -0
- package/dist/account.js +45 -0
- package/dist/account.js.map +1 -0
- package/dist/api-failure.d.ts +174 -0
- package/dist/api-failure.js +450 -0
- package/dist/api-failure.js.map +1 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +88 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/rate-limit.d.ts +138 -0
- package/dist/rate-limit.js +152 -0
- package/dist/rate-limit.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { apiError, debtOutstandingError, insufficientCreditsError } from "./errors.js";
|
|
2
|
+
import { rateLimitedError } from "./rate-limit.js";
|
|
3
|
+
/**
|
|
4
|
+
* ПОВТОРЯТЬ ИЛИ НЕТ — ОДНОЙ ФУНКЦИЕЙ, а не условием на каждой поверхности.
|
|
5
|
+
*
|
|
6
|
+
* Ошибиться здесь можно в обе стороны, и цены разные. Не повторить 429 — это
|
|
7
|
+
* оборванный `makeUgc` на ране, за который уже заплачено. Повторить 402 — это
|
|
8
|
+
* агент, крутящий отказ по деньгам в цикле: баланс от повторов не растёт, а
|
|
9
|
+
* лимит частоты они съедают, и человек получает вместо «пополни счёт» молчание
|
|
10
|
+
* до самого дедлайна.
|
|
11
|
+
*/
|
|
12
|
+
export function isRetryableFailure(failure) {
|
|
13
|
+
return failure.kind === "rate_limited" || failure.kind === "server_error";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Пауза перед повтором, мс. `fallbackMs` — собственный шаг вызывателя (для
|
|
17
|
+
* поллинга это его интервал): он же и пол, потому что сервер, не назвавший
|
|
18
|
+
* `Retry-After`, не разрешил долбить чаще обычного.
|
|
19
|
+
*
|
|
20
|
+
* Вызывать имеет смысл только после `isRetryableFailure`; на терминальном
|
|
21
|
+
* отказе результат бессмыслен, и функция не притворяется, что это не так.
|
|
22
|
+
*/
|
|
23
|
+
export function retryDelayMs(failure, fallbackMs) {
|
|
24
|
+
const fromServer = failure.kind === "rate_limited" || failure.kind === "server_error"
|
|
25
|
+
? failure.retryAfterSeconds
|
|
26
|
+
: undefined;
|
|
27
|
+
return Math.max(fallbackMs, (fromServer ?? 0) * 1000);
|
|
28
|
+
}
|
|
29
|
+
const MONTHS = [
|
|
30
|
+
"Jan",
|
|
31
|
+
"Feb",
|
|
32
|
+
"Mar",
|
|
33
|
+
"Apr",
|
|
34
|
+
"May",
|
|
35
|
+
"Jun",
|
|
36
|
+
"Jul",
|
|
37
|
+
"Aug",
|
|
38
|
+
"Sep",
|
|
39
|
+
"Oct",
|
|
40
|
+
"Nov",
|
|
41
|
+
"Dec",
|
|
42
|
+
];
|
|
43
|
+
const MONTH = MONTHS.join("|");
|
|
44
|
+
const DAY_SHORT = "Mon|Tue|Wed|Thu|Fri|Sat|Sun";
|
|
45
|
+
const DAY_LONG = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday";
|
|
46
|
+
/** IMF-fixdate: `Sun, 06 Nov 1994 08:49:37 GMT` — форма, которую слать ОБЯЗАНЫ. */
|
|
47
|
+
const IMF_FIXDATE = new RegExp(`^(?:${DAY_SHORT}), (\\d{2}) (${MONTH}) (\\d{4}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
|
|
48
|
+
/** RFC 850: `Sunday, 06-Nov-94 08:49:37 GMT` — устаревшая, но принимать обязаны. */
|
|
49
|
+
const RFC850_DATE = new RegExp(`^(?:${DAY_LONG}), (\\d{2})-(${MONTH})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}) GMT$`);
|
|
50
|
+
/** asctime: `Sun Nov 6 08:49:37 1994` — тоже устаревшая и тоже обязательная. */
|
|
51
|
+
const ASCTIME_DATE = new RegExp(`^(?:${DAY_SHORT}) (${MONTH}) ([ \\d]\\d) (\\d{2}):(\\d{2}):(\\d{2}) (\\d{4})$`);
|
|
52
|
+
/**
|
|
53
|
+
* HTTP-date → миллисекунды эпохи. Три формы RFC 9110 §5.6.7, СОБСТВЕННОЙ
|
|
54
|
+
* сборкой через `Date.UTC`, а не через `Date.parse`.
|
|
55
|
+
*
|
|
56
|
+
* ПОЧЕМУ НЕ `Date.parse`. Внешнее ревью показало на его квирках два разных
|
|
57
|
+
* дефекта, и оба — молчаливые.
|
|
58
|
+
*
|
|
59
|
+
* 1. Двузначный год формы RFC 850 V8 разворачивает по своему правилу:
|
|
60
|
+
* `29-Jul-75` он читает как 1975 год, тогда как RFC велит считать годом в
|
|
61
|
+
* прошлое только то, что оказалось БОЛЕЕ ЧЕМ НА 50 ЛЕТ в будущем, — то есть
|
|
62
|
+
* это 2075-й (проверено запуском). Назначенная пауза превращалась в дату из
|
|
63
|
+
* прошлого, то есть в ноль, и клиент шёл опрашивать снова своим шагом.
|
|
64
|
+
* 2. Форма asctime не несёт пояса, и `Date.parse` читает её как МЕСТНОЕ время,
|
|
65
|
+
* тогда как HTTP считает её UTC. В поясе UTC+3 пауза «до 15:00:30» давала
|
|
66
|
+
* 30 секунд вместо трёх часов (проверено запуском с `TZ=Europe/Minsk`):
|
|
67
|
+
* повтор уходил почти на три часа раньше разрешённого.
|
|
68
|
+
*
|
|
69
|
+
* Сборка из разобранных полей обеих ошибок не имеет: пояс задан явно, век
|
|
70
|
+
* вычисляется по правилу RFC. Заодно исчезает зависимость поведения от часового
|
|
71
|
+
* пояса машины, на которой запущен клиент.
|
|
72
|
+
*
|
|
73
|
+
* БЛИЗКИЕ, НО НЕ СОВПАДАЮЩИЕ НАПИСАНИЯ ОТВЕРГАЮТСЯ НАМЕРЕННО — `UTC` вместо
|
|
74
|
+
* `GMT`, нижний регистр, однозначный день в IMF-fixdate. RFC задаёт эти
|
|
75
|
+
* написания буквально, регистр в том числе, поэтому нестрогое совпадение — уже
|
|
76
|
+
* не HTTP-date, а догадка о нём. Цена отказа мала и известна: заголовок читается
|
|
77
|
+
* как «сервер паузы не назвал», и клиент ждёт свой обычный шаг.
|
|
78
|
+
*
|
|
79
|
+
* День недели в строке не сверяется с датой: RFC разрешает получателю его
|
|
80
|
+
* игнорировать, а расхождение не делает дату непонятной.
|
|
81
|
+
*/
|
|
82
|
+
function httpDateToEpochMs(raw, now) {
|
|
83
|
+
/**
|
|
84
|
+
* Сборка момента из полей — с ПРОВЕРКОЙ, что поля описывают существующую дату.
|
|
85
|
+
*
|
|
86
|
+
* `Date.UTC` невалидные поля не отвергает, а НОРМАЛИЗУЕТ: 32 июля становится
|
|
87
|
+
* первым августа, 25:00 — часом следующих суток, 29 февраля невисокосного
|
|
88
|
+
* года — первым марта (внешнее ревью, круг 3). Для нас это не мелочь: мусорный
|
|
89
|
+
* заголовок превращался бы не в «сервер паузы не назвал», а в паузу на трое
|
|
90
|
+
* суток, клиент проспал бы весь свой дедлайн и вернул таймаут по рану, который
|
|
91
|
+
* к тому моменту уже завершился.
|
|
92
|
+
*
|
|
93
|
+
* Секунда 60 разрешена НАМЕРЕННО: это координационная секунда, RFC 9110 её
|
|
94
|
+
* допускает, и отвергать законное значение было бы такой же ошибкой, как
|
|
95
|
+
* принимать незаконное. `Date.UTC` переносит её на следующую минуту — разница
|
|
96
|
+
* в одну секунду против паузы, которую всё равно назначил не мы.
|
|
97
|
+
*/
|
|
98
|
+
const utc = (year, month, day, h, m, s) => {
|
|
99
|
+
const monthIndex = MONTHS.indexOf(month);
|
|
100
|
+
const dayNumber = Number.parseInt(day, 10);
|
|
101
|
+
const hours = Number.parseInt(h, 10);
|
|
102
|
+
const minutes = Number.parseInt(m, 10);
|
|
103
|
+
const seconds = Number.parseInt(s, 10);
|
|
104
|
+
if (hours > 23 || minutes > 59 || seconds > 60)
|
|
105
|
+
return undefined;
|
|
106
|
+
if (dayNumber < 1)
|
|
107
|
+
return undefined;
|
|
108
|
+
// ГОД МЛАДШЕ 1900 ОТВЕРГАЕТСЯ, И ЭТО НЕ ПРИДИРКА (внешнее ревью, круг 4).
|
|
109
|
+
// У `Date.UTC` есть наследственный квирк: годы 0–99 он трактует как 1900+год,
|
|
110
|
+
// поэтому `Sun, 01 Jan 0099 …` превращался в 1999-й (проверено запуском). Дата
|
|
111
|
+
// уезжала в прошлое, пауза считалась нулевой, и агент получал «подожди 0
|
|
112
|
+
// секунд» — то есть повторял отбитый запрос немедленно и мог продлить себе
|
|
113
|
+
// блокировку. Нижняя граница взята из семантики даты RFC 5322, на которую
|
|
114
|
+
// ссылается RFC 9110: раньше 1900 года HTTP-дат не бывает.
|
|
115
|
+
if (year < 1900)
|
|
116
|
+
return undefined;
|
|
117
|
+
// КАЛЕНДАРЬ СВЕРЯЕТСЯ ОТДЕЛЬНО ОТ ВРЕМЕНИ СУТОК, и это не стилистика.
|
|
118
|
+
// Сверка на полной дате отвергала бы координационную секунду: `23:59:60`
|
|
119
|
+
// переносится на следующие сутки, число месяца уезжает, и законное значение
|
|
120
|
+
// выглядело бы как невалидное. Поэтому число месяца проверяется на полуночи
|
|
121
|
+
// тех же суток, а время суток — диапазонами выше.
|
|
122
|
+
const midnight = new Date(Date.UTC(year, monthIndex, dayNumber));
|
|
123
|
+
if (midnight.getUTCDate() !== dayNumber || midnight.getUTCMonth() !== monthIndex) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
return Date.UTC(year, monthIndex, dayNumber, hours, minutes, seconds);
|
|
127
|
+
};
|
|
128
|
+
const imf = IMF_FIXDATE.exec(raw);
|
|
129
|
+
if (imf !== null) {
|
|
130
|
+
const [, day, month, year, h, m, s] = imf;
|
|
131
|
+
return utc(Number.parseInt(year, 10), month, day, h, m, s);
|
|
132
|
+
}
|
|
133
|
+
const rfc850 = RFC850_DATE.exec(raw);
|
|
134
|
+
if (rfc850 !== null) {
|
|
135
|
+
const [, day, month, shortYear, h, m, s] = rfc850;
|
|
136
|
+
// ПРАВИЛО ВЕКА ИЗ RFC 9110 §5.6.7: берём тот же век, что у текущего года, и
|
|
137
|
+
// отступаем на сто лет назад, только если получилось больше чем на 50 лет
|
|
138
|
+
// вперёд. Именно этого правила нет у `Date.parse`.
|
|
139
|
+
const nowYear = now.getUTCFullYear();
|
|
140
|
+
let year = Math.floor(nowYear / 100) * 100 + Number.parseInt(shortYear, 10);
|
|
141
|
+
if (year - nowYear > 50)
|
|
142
|
+
year -= 100;
|
|
143
|
+
return utc(year, month, day, h, m, s);
|
|
144
|
+
}
|
|
145
|
+
const asctime = ASCTIME_DATE.exec(raw);
|
|
146
|
+
if (asctime !== null) {
|
|
147
|
+
const [, month, day, h, m, s, year] = asctime;
|
|
148
|
+
return utc(Number.parseInt(year, 10), month, day.trim(), h, m, s);
|
|
149
|
+
}
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* `Retry-After` в секундах, обе формы RFC 9110.
|
|
154
|
+
*
|
|
155
|
+
* ДВЕ ФОРМЫ — НЕ ПЕДАНТИЗМ. Дельту в секундах отдаёт наша мидлвара, а HTTP-дату
|
|
156
|
+
* — чужие прокси (её разрешает та же RFC, и nginx с облачными edge её ставят).
|
|
157
|
+
* Клиент, знающий только цифры, на дате получил бы `NaN` и либо не ждал вовсе,
|
|
158
|
+
* либо ждал бесконечность — обе ветки хуже, чем «подожди свой обычный шаг».
|
|
159
|
+
*
|
|
160
|
+
* `now` — параметр, а не `Date.now()` внутри: разбор даты обязан быть проверяем
|
|
161
|
+
* прогоном, а не зависеть от того, когда его запустили.
|
|
162
|
+
*/
|
|
163
|
+
export function parseRetryAfterSeconds(raw, now) {
|
|
164
|
+
if (raw === null || raw === undefined)
|
|
165
|
+
return undefined;
|
|
166
|
+
const trimmed = raw.trim();
|
|
167
|
+
if (trimmed.length === 0)
|
|
168
|
+
return undefined;
|
|
169
|
+
if (/^\d+$/.test(trimmed)) {
|
|
170
|
+
const seconds = Number.parseInt(trimmed, 10);
|
|
171
|
+
return Number.isSafeInteger(seconds) ? seconds : undefined;
|
|
172
|
+
}
|
|
173
|
+
// ДАТА РАЗБИРАЕТСЯ СОБСТВЕННОЙ ГРАММАТИКОЙ, А НЕ `Date.parse`.
|
|
174
|
+
//
|
|
175
|
+
// Легаси-разбор дат в V8 принимает почти всё: `Date.parse("-5")` даёт 30
|
|
176
|
+
// апреля 2001 года, `Date.parse("7.5")` — 4 июля того же, а
|
|
177
|
+
// `Date.parse("Mon 1/2/2099")` — 2099-й (всё проверено запуском). Мусор
|
|
178
|
+
// превращался бы в паузу длиной в десятилетия; клиент обрезал бы её своим
|
|
179
|
+
// дедлайном, проспал бы его весь и вернул таймаут по рану, который к тому
|
|
180
|
+
// моменту уже успешно завершился. Разбор и его доводы — в `httpDateToEpochMs`.
|
|
181
|
+
const at = httpDateToEpochMs(trimmed, now);
|
|
182
|
+
if (at === undefined || Number.isNaN(at))
|
|
183
|
+
return undefined;
|
|
184
|
+
// Прошедшая дата значит «уже можно»: ноль, а не отрицательное число, иначе
|
|
185
|
+
// `Math.max` ниже по течению получил бы значение, тянущее паузу вниз.
|
|
186
|
+
return Math.max(0, Math.ceil((at - now.getTime()) / 1000));
|
|
187
|
+
}
|
|
188
|
+
/** Сколько символов чужого тела дотащить до сообщения. */
|
|
189
|
+
const BODY_EXCERPT_LIMIT = 200;
|
|
190
|
+
/**
|
|
191
|
+
* Однострочная выжимка нечитаемого тела.
|
|
192
|
+
*
|
|
193
|
+
* ЗАЧЕМ ОНА ВООБЩЕ. Сообщение «HTTP 429» без единой подробности отправляет
|
|
194
|
+
* человека читать логи прокси, которых у него нет. Строка «error code: 1015» из
|
|
195
|
+
* тела Cloudflare отвечает на вопрос сразу. Обрезка обязательна: HTML-страница
|
|
196
|
+
* ошибки бывает в десятки килобайт, и целиком она не сообщение, а свалка.
|
|
197
|
+
*/
|
|
198
|
+
function excerpt(bodyText) {
|
|
199
|
+
if (bodyText === undefined)
|
|
200
|
+
return undefined;
|
|
201
|
+
const flat = bodyText.replace(/\s+/g, " ").trim();
|
|
202
|
+
if (flat.length === 0)
|
|
203
|
+
return undefined;
|
|
204
|
+
return flat.length > BODY_EXCERPT_LIMIT ? `${flat.slice(0, BODY_EXCERPT_LIMIT)}…` : flat;
|
|
205
|
+
}
|
|
206
|
+
/** `JSON.parse`, который не бросает: тело от прокси нам никто не обещал. */
|
|
207
|
+
function safeJson(bodyText) {
|
|
208
|
+
if (bodyText === undefined)
|
|
209
|
+
return undefined;
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(bodyText);
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Коды, за которыми закреплено ПОВЕДЕНИЕ клиента, а не только текст.
|
|
219
|
+
*
|
|
220
|
+
* Список выведен из самих схем, а не переписан руками: разъедься он с ними —
|
|
221
|
+
* и защита ниже перестала бы закрывать то, ради чего написана. Значения берутся
|
|
222
|
+
* из литералов схем, поэтому переименование кода в `errors.ts` или
|
|
223
|
+
* `rate-limit.ts` доедет сюда само.
|
|
224
|
+
*/
|
|
225
|
+
const BEHAVIOURAL_CODES = new Set([
|
|
226
|
+
rateLimitedError.shape.error.shape.code.value,
|
|
227
|
+
insufficientCreditsError.shape.error.shape.code.value,
|
|
228
|
+
debtOutstandingError.shape.error.shape.code.value,
|
|
229
|
+
]);
|
|
230
|
+
/**
|
|
231
|
+
* Статус + тело + заголовок → то, с чем можно ПОСТУПИТЬ.
|
|
232
|
+
*
|
|
233
|
+
* ТЕЛО УТОЧНЯЕТ СТАТУС, НО НЕ ОТМЕНЯЕТ ЕГО. Специализированная схема
|
|
234
|
+
* применяется, только если статус ей СООТВЕТСТВУЕТ: `rate_limited` — на 429,
|
|
235
|
+
* оба денежных кода — на 402.
|
|
236
|
+
*
|
|
237
|
+
* Первая редакция давала телу перевешивать статус, рассуждая так: код объявлен
|
|
238
|
+
* в теле, а статус по дороге переписывает кто угодно, значит 502 поверх нашего
|
|
239
|
+
* 402 — это всё ещё «не хватило кредитов». Внешнее ревью показало обратную
|
|
240
|
+
* сторону того же рассуждения, и она хуже. Кэширующий прокси, отдавший старое
|
|
241
|
+
* тело `debt_outstanding` с новым статусом 429, делал отказ ТЕРМИНАЛЬНЫМ: агент
|
|
242
|
+
* бросал бы уже оплаченный ран и требовал погасить долг, которого нет. Зеркально
|
|
243
|
+
* 402 с телом `rate_limited` становился РЕТРАЙНЫМ, прямо вопреки правилу «любой
|
|
244
|
+
* 402 останавливает». Получались и внутренне противоречивые отказы вида
|
|
245
|
+
* `status === 402 && retryable === true`.
|
|
246
|
+
*
|
|
247
|
+
* Сочетание статуса и кода из РАЗНЫХ ответов — это не сообщение, а его порча, и
|
|
248
|
+
* доверять в нём надо тому, что уцелело наверняка. Уцелел статус: он часть
|
|
249
|
+
* стартовой строки, его не кэшируют отдельно от тела и им управляет тот же
|
|
250
|
+
* узел, который решил ответить. Поэтому при расхождении класс отказа берётся из
|
|
251
|
+
* статуса, а текст — из тела: сказать «сервер перегружен, повтори» и оказаться
|
|
252
|
+
* неправым дешевле, чем бросить оплаченный ран.
|
|
253
|
+
*
|
|
254
|
+
* ЧИСЛА `retry_after_seconds` ИЗ ТЕЛА СИЛЬНЕЕ ЗАГОЛОВКА — здесь тело выигрывает
|
|
255
|
+
* законно, потому что спорят два поля ОДНОГО ответа, а не ответ с самим собой.
|
|
256
|
+
* Наша мидлвара ставит в оба места одно число, и заголовок переписывают чаще.
|
|
257
|
+
*/
|
|
258
|
+
export function parseApiFailure(input) {
|
|
259
|
+
const { status, bodyText } = input;
|
|
260
|
+
const now = input.now ?? new Date();
|
|
261
|
+
const headerSeconds = parseRetryAfterSeconds(input.retryAfterHeader, now);
|
|
262
|
+
const body = safeJson(bodyText);
|
|
263
|
+
const limited = status === 429 ? rateLimitedError.safeParse(body) : undefined;
|
|
264
|
+
if (limited?.success === true) {
|
|
265
|
+
const { error } = limited.data;
|
|
266
|
+
return {
|
|
267
|
+
kind: "rate_limited",
|
|
268
|
+
status,
|
|
269
|
+
message: error.message,
|
|
270
|
+
retryAfterSeconds: error.retry_after_seconds,
|
|
271
|
+
limit: error.limit,
|
|
272
|
+
windowSeconds: error.window_seconds,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
const insufficient = status === 402 ? insufficientCreditsError.safeParse(body) : undefined;
|
|
276
|
+
if (insufficient?.success === true) {
|
|
277
|
+
const { error } = insufficient.data;
|
|
278
|
+
return {
|
|
279
|
+
kind: "insufficient_credits",
|
|
280
|
+
status,
|
|
281
|
+
message: error.message,
|
|
282
|
+
balanceCredits: error.balance_credits,
|
|
283
|
+
requiredCredits: error.required_credits,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const debt = status === 402 ? debtOutstandingError.safeParse(body) : undefined;
|
|
287
|
+
if (debt?.success === true) {
|
|
288
|
+
const { error } = debt.data;
|
|
289
|
+
return {
|
|
290
|
+
kind: "debt_outstanding",
|
|
291
|
+
status,
|
|
292
|
+
message: error.message,
|
|
293
|
+
debtCredits: error.debt_credits,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
// Дальше тело либо не наше, либо не разобралось вовсе. Общий конверт
|
|
297
|
+
// (`apiError`) даёт хотя бы текст и код; всё, что не разобралось и им, —
|
|
298
|
+
// чужое, и сообщение придётся собрать из статуса.
|
|
299
|
+
const generic = apiError.safeParse(body);
|
|
300
|
+
const message = generic.success
|
|
301
|
+
? generic.data.error.message
|
|
302
|
+
: messageFromStatus(status, bodyText);
|
|
303
|
+
// КОД ИЗ ТЕЛА ПРОПУСКАЕТСЯ, ТОЛЬКО ЕСЛИ ОН НЕ ГОВОРЯЩИЙ (внешнее ревью, круг 2).
|
|
304
|
+
//
|
|
305
|
+
// Сюда мы попадаем в том числе после несовпадения статуса с телом: 403 с
|
|
306
|
+
// нашим телом `rate_limited` специализированную схему не прошёл — и правильно
|
|
307
|
+
// не прошёл. Но общий конверт всё равно вытаскивал из него `code:
|
|
308
|
+
// "rate_limited"`, и отчёт агенту выходил самопротиворечивым: код, который
|
|
309
|
+
// SKILL.md связывает с «подожди и повтори», приезжал вместе с `retryable:
|
|
310
|
+
// false`. Клиент, ветвящийся по коду, повторял бы терминальный отказ;
|
|
311
|
+
// ветвящийся по флагу — останавливался. Самоописывающий контракт, дающий два
|
|
312
|
+
// разных ответа на один вопрос, хуже отсутствия контракта.
|
|
313
|
+
//
|
|
314
|
+
// Поэтому коды, за которыми закреплено ПОВЕДЕНИЕ, признаются только на своём
|
|
315
|
+
// статусе. Не признанный здесь код не теряется бесследно: текст тела уходит в
|
|
316
|
+
// `message` целиком, и человек его увидит.
|
|
317
|
+
const rawCode = generic.success ? generic.data.error.code : undefined;
|
|
318
|
+
const code = rawCode !== undefined && BEHAVIOURAL_CODES.has(rawCode) ? undefined : rawCode;
|
|
319
|
+
if (status === 429) {
|
|
320
|
+
return {
|
|
321
|
+
kind: "rate_limited",
|
|
322
|
+
status,
|
|
323
|
+
message,
|
|
324
|
+
retryAfterSeconds: headerSeconds,
|
|
325
|
+
limit: undefined,
|
|
326
|
+
windowSeconds: undefined,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
if (status >= 500) {
|
|
330
|
+
return { kind: "server_error", status, message, retryAfterSeconds: headerSeconds };
|
|
331
|
+
}
|
|
332
|
+
return { kind: "client_error", status, message, code };
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Сообщение, когда тело нам ничего не сказало.
|
|
336
|
+
*
|
|
337
|
+
* Именно ЭТА ветка чинит падение, названное в US-622: `res.json()` на HTML-теле
|
|
338
|
+
* от прокси бросал `SyntaxError`, и вызыватель видел «Unexpected token <»
|
|
339
|
+
* вместо «слишком часто, подожди». Ошибка парсера не только бесполезна — она
|
|
340
|
+
* УВОДИТ: выглядит как баг клиента там, где сервер честно сказал 429.
|
|
341
|
+
*/
|
|
342
|
+
function messageFromStatus(status, bodyText) {
|
|
343
|
+
// ИСТОЧНИК ОТКАЗА НЕ ПРИПИСЫВАЕТСЯ ПО ПУСТОМУ МЕСТУ (внешнее ревью, круг 13).
|
|
344
|
+
// Прежняя редакция объявляла всякий неразобранный 429 отказом прокси — и
|
|
345
|
+
// ошибалась ровно там, где тело не удалось ПРОЧИТАТЬ: заголовки могли прийти
|
|
346
|
+
// от нашего же API, а человека отправляли искать несуществующий шлюз вместо
|
|
347
|
+
// исчерпанного лимита аккаунта.
|
|
348
|
+
if (bodyText === undefined) {
|
|
349
|
+
return `HTTP ${status}: the response body could not be read, so the reason is only known by status`;
|
|
350
|
+
}
|
|
351
|
+
// ИСТОЧНИК НЕ НАЗЫВАЕТСЯ И ЗДЕСЬ (внешнее ревью, круг 15). Круг 14 вычистил
|
|
352
|
+
// атрибуцию из `next_action`, но в самом сообщении осталась догадка «обычно
|
|
353
|
+
// это шлюз или CDN» — тот же класс, просто соседней строкой. Отдать неродное
|
|
354
|
+
// тело может и наш сервер за балансировщиком, и промежуточный узел; знать
|
|
355
|
+
// это здесь неоткуда. Всё, что известно достоверно, — код ответа и то, что
|
|
356
|
+
// тело не наше; выжимка тела ниже даёт человеку самому увидеть, кто ответил.
|
|
357
|
+
const tail = excerpt(bodyText);
|
|
358
|
+
const head = status === 429
|
|
359
|
+
? "HTTP 429: too many requests, and the body did not carry a Clipwright error object"
|
|
360
|
+
: `HTTP ${status}: the response body is not a Clipwright error object`;
|
|
361
|
+
return tail === undefined ? head : `${head}. Body: ${tail}`;
|
|
362
|
+
}
|
|
363
|
+
export function agentFailureReport(failure) {
|
|
364
|
+
const base = { message: failure.message };
|
|
365
|
+
switch (failure.kind) {
|
|
366
|
+
case "rate_limited": {
|
|
367
|
+
const seconds = failure.retryAfterSeconds;
|
|
368
|
+
return {
|
|
369
|
+
...base,
|
|
370
|
+
status: "RETRY_LATER",
|
|
371
|
+
code: "rate_limited",
|
|
372
|
+
retryable: true,
|
|
373
|
+
// ПРО ДЕНЬГИ ЗДЕСЬ НЕ УТВЕРЖДАЕТСЯ НИЧЕГО, и это правка после внешнего
|
|
374
|
+
// ревью (круг 6). Прежняя редакция дописывала «nothing was charged» ко
|
|
375
|
+
// всякому 429 — а 429 приходит и на ОПРОС СТАТУСА уже запущенного рана,
|
|
376
|
+
// за который деньги зарезервированы или уже списаны. Агент, повторивший
|
|
377
|
+
// это человеку, сказал бы неправду о его счёте.
|
|
378
|
+
//
|
|
379
|
+
// Точное утверждение про деньги умеет делать только сервер: у него есть
|
|
380
|
+
// ведро, и `rateLimitedBody` в `rate-limit.ts` формулирует его отдельно
|
|
381
|
+
// для платных и бесплатных путей. Оно приезжает в `message` целиком.
|
|
382
|
+
// Дублировать его здесь более слабой и менее верной копией значило бы
|
|
383
|
+
// завести второй источник правды о деньгах — худший из возможных.
|
|
384
|
+
next_action: seconds === undefined
|
|
385
|
+
? "Too many requests. Wait a few seconds, then repeat the SAME call — do not start a new one."
|
|
386
|
+
: `Too many requests. Wait ${seconds} seconds, then repeat the SAME call — do not start a new one.`,
|
|
387
|
+
...(seconds !== undefined && { retry_after_seconds: seconds }),
|
|
388
|
+
...(failure.limit !== undefined && { limit: failure.limit }),
|
|
389
|
+
...(failure.windowSeconds !== undefined && { window_seconds: failure.windowSeconds }),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
case "insufficient_credits":
|
|
393
|
+
return {
|
|
394
|
+
...base,
|
|
395
|
+
status: "FAILED",
|
|
396
|
+
code: "insufficient_credits",
|
|
397
|
+
retryable: false,
|
|
398
|
+
// Числа повторены в императиве намеренно: агент показывает человеку
|
|
399
|
+
// одну строку, и «не хватило кредитов» без цифр не даёт решить, сколько
|
|
400
|
+
// покупать.
|
|
401
|
+
next_action: `STOP. Do NOT retry: this account has ${failure.balanceCredits} credits and the call ` +
|
|
402
|
+
`needs ${failure.requiredCredits}. Retrying cannot change that. Tell the user to top up ` +
|
|
403
|
+
"the balance, then call again. Nothing was charged and no run was created.",
|
|
404
|
+
balance_credits: failure.balanceCredits,
|
|
405
|
+
required_credits: failure.requiredCredits,
|
|
406
|
+
};
|
|
407
|
+
case "debt_outstanding":
|
|
408
|
+
return {
|
|
409
|
+
...base,
|
|
410
|
+
status: "FAILED",
|
|
411
|
+
code: "debt_outstanding",
|
|
412
|
+
retryable: false,
|
|
413
|
+
// Отдельный текст, а не оттенок предыдущего: совет «пополни» здесь
|
|
414
|
+
// неверен — пополнение долг не снимает (см. `errors.ts`).
|
|
415
|
+
next_action: `STOP. Do NOT retry: an outstanding debt of ${failure.debtCredits} credits blocks new ` +
|
|
416
|
+
"paid runs. Tell the user to settle the debt — topping up the balance alone does not " +
|
|
417
|
+
"clear it. Nothing was charged and no run was created.",
|
|
418
|
+
debt_credits: failure.debtCredits,
|
|
419
|
+
};
|
|
420
|
+
case "server_error": {
|
|
421
|
+
const seconds = failure.retryAfterSeconds;
|
|
422
|
+
return {
|
|
423
|
+
...base,
|
|
424
|
+
status: "RETRY_LATER",
|
|
425
|
+
code: `http_${failure.status}`,
|
|
426
|
+
retryable: true,
|
|
427
|
+
// ИСТОЧНИК ОТКАЗА НЕ НАЗЫВАЕТСЯ (внешнее ревью, круг 14). Прежний текст
|
|
428
|
+
// утверждал «Clipwright API упал на своей стороне» — а 5xx одинаково
|
|
429
|
+
// отдают и наш сервер, и корпоративный шлюз, и CDN. Ошибочная
|
|
430
|
+
// атрибуция отправляет человека разбирать инцидент не в том месте, и
|
|
431
|
+
// она же противоречила соседнему сообщению, которое честно говорит, что
|
|
432
|
+
// причина известна только по статусу.
|
|
433
|
+
next_action: "The call failed with a server-side error. Repeat the SAME call in a few seconds. If it " +
|
|
434
|
+
"keeps failing, stop and tell the user; do not start a new run with a different " +
|
|
435
|
+
"idempotency key.",
|
|
436
|
+
...(seconds !== undefined && { retry_after_seconds: seconds }),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
case "client_error":
|
|
440
|
+
return {
|
|
441
|
+
...base,
|
|
442
|
+
status: "FAILED",
|
|
443
|
+
code: failure.code ?? `http_${failure.status}`,
|
|
444
|
+
retryable: false,
|
|
445
|
+
next_action: "STOP. Do NOT retry: the request itself was rejected, so repeating it unchanged gives " +
|
|
446
|
+
"the same answer. Read the message, fix the call, or tell the user what is wrong.",
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//# sourceMappingURL=api-failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-failure.js","sourceRoot":"","sources":["../src/api-failure.ts"],"names":[],"mappings":"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"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* ТЕЛА ОТКАЗОВ — СХЕМАМИ, А НЕ `c.json({...})` В ОБРАБОТЧИКЕ.
|
|
4
|
+
*
|
|
5
|
+
* До US-602 каждая ошибка API собиралась объектным литералом прямо в
|
|
6
|
+
* `apps/api/src/app.ts`, и это сходило с рук ровно потому, что все они несли
|
|
7
|
+
* одну и ту же пару `code`/`message`: разъехаться было нечему. С деньгами это
|
|
8
|
+
* перестаёт быть правдой — отказ 402 несёт ЧИСЛА, и агент по ним принимает
|
|
9
|
+
* решение (пополнить и повторить либо остановиться). Литерал в обработчике,
|
|
10
|
+
* потерявший `balance_credits`, оставил бы агента с сообщением «не хватило
|
|
11
|
+
* кредитов» без единой цифры, и обнаружилось бы это у клиента.
|
|
12
|
+
*
|
|
13
|
+
* Поэтому форма живёт здесь (правило проекта №2), а обработчик обязан пройти
|
|
14
|
+
* через `.parse()`: тело, собранное мимо схемы, не соберётся вовсе.
|
|
15
|
+
*
|
|
16
|
+
* ЧИСЛА ЛЕЖАТ ВНУТРИ `error`, А НЕ РЯДОМ С НИМ. Прецедент задан отказом по
|
|
17
|
+
* отклонённым полям (`error.fields`) и держится одним доводом: у ответа
|
|
18
|
+
* ошибки один конверт, и всё, что объясняет отказ, лежит в нём. Разложи числа
|
|
19
|
+
* по верхнему уровню — и клиенту придётся знать, для каких кодов смотреть в
|
|
20
|
+
* конверт, а для каких мимо него.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Общий конверт ошибки. Толерантен к дополнительным полям внутри `error`:
|
|
24
|
+
* типизированные отказы ниже — его расширения, и общий разбор не должен
|
|
25
|
+
* ломаться от того, что конкретный код принёс с собой числа.
|
|
26
|
+
*/
|
|
27
|
+
export declare const apiError: z.ZodObject<{
|
|
28
|
+
error: z.ZodObject<{
|
|
29
|
+
code: z.ZodString;
|
|
30
|
+
message: z.ZodString;
|
|
31
|
+
}, z.core.$strip>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type ApiError = z.infer<typeof apiError>;
|
|
34
|
+
/** Кредитов на балансе меньше веса рана. Значит «пополни». */
|
|
35
|
+
export declare const insufficientCreditsError: z.ZodObject<{
|
|
36
|
+
error: z.ZodObject<{
|
|
37
|
+
code: z.ZodLiteral<"insufficient_credits">;
|
|
38
|
+
message: z.ZodString;
|
|
39
|
+
balance_credits: z.ZodNumber;
|
|
40
|
+
required_credits: z.ZodNumber;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
export type InsufficientCreditsError = z.infer<typeof insufficientCreditsError>;
|
|
44
|
+
/**
|
|
45
|
+
* На аккаунте висит непогашенный долг. Значит «погаси», а не «пополни».
|
|
46
|
+
*
|
|
47
|
+
* Отдельный код, а не оттенок предыдущего: у аккаунта с долгом деньги на
|
|
48
|
+
* балансе могут БЫТЬ, и ответ «недостаточно кредитов» сказал бы клиенту
|
|
49
|
+
* заведомую неправду, а его агента отправил бы покупать кредиты, которые не
|
|
50
|
+
* снимут блокировку.
|
|
51
|
+
*/
|
|
52
|
+
export declare const debtOutstandingError: z.ZodObject<{
|
|
53
|
+
error: z.ZodObject<{
|
|
54
|
+
code: z.ZodLiteral<"debt_outstanding">;
|
|
55
|
+
message: z.ZodString;
|
|
56
|
+
debt_credits: z.ZodNumber;
|
|
57
|
+
}, z.core.$strip>;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export type DebtOutstandingError = z.infer<typeof debtOutstandingError>;
|
|
60
|
+
/**
|
|
61
|
+
* Сборка тела «не хватило кредитов».
|
|
62
|
+
*
|
|
63
|
+
* Формулировка живёт ЗДЕСЬ, а не в обработчике, по той же причине, что и
|
|
64
|
+
* форма: два места, отвечающие 402 (сегодня — создание рана, завтра — любой
|
|
65
|
+
* другой платный вызов), обязаны объяснять отказ одинаково.
|
|
66
|
+
*/
|
|
67
|
+
export declare function insufficientCreditsBody(args: {
|
|
68
|
+
balanceCredits: number;
|
|
69
|
+
requiredCredits: number;
|
|
70
|
+
}): InsufficientCreditsError;
|
|
71
|
+
/** Сборка тела «висит долг». См. довод о формулировке выше. */
|
|
72
|
+
export declare function debtOutstandingBody(args: {
|
|
73
|
+
debtCredits: number;
|
|
74
|
+
}): DebtOutstandingError;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* ТЕЛА ОТКАЗОВ — СХЕМАМИ, А НЕ `c.json({...})` В ОБРАБОТЧИКЕ.
|
|
4
|
+
*
|
|
5
|
+
* До US-602 каждая ошибка API собиралась объектным литералом прямо в
|
|
6
|
+
* `apps/api/src/app.ts`, и это сходило с рук ровно потому, что все они несли
|
|
7
|
+
* одну и ту же пару `code`/`message`: разъехаться было нечему. С деньгами это
|
|
8
|
+
* перестаёт быть правдой — отказ 402 несёт ЧИСЛА, и агент по ним принимает
|
|
9
|
+
* решение (пополнить и повторить либо остановиться). Литерал в обработчике,
|
|
10
|
+
* потерявший `balance_credits`, оставил бы агента с сообщением «не хватило
|
|
11
|
+
* кредитов» без единой цифры, и обнаружилось бы это у клиента.
|
|
12
|
+
*
|
|
13
|
+
* Поэтому форма живёт здесь (правило проекта №2), а обработчик обязан пройти
|
|
14
|
+
* через `.parse()`: тело, собранное мимо схемы, не соберётся вовсе.
|
|
15
|
+
*
|
|
16
|
+
* ЧИСЛА ЛЕЖАТ ВНУТРИ `error`, А НЕ РЯДОМ С НИМ. Прецедент задан отказом по
|
|
17
|
+
* отклонённым полям (`error.fields`) и держится одним доводом: у ответа
|
|
18
|
+
* ошибки один конверт, и всё, что объясняет отказ, лежит в нём. Разложи числа
|
|
19
|
+
* по верхнему уровню — и клиенту придётся знать, для каких кодов смотреть в
|
|
20
|
+
* конверт, а для каких мимо него.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Общий конверт ошибки. Толерантен к дополнительным полям внутри `error`:
|
|
24
|
+
* типизированные отказы ниже — его расширения, и общий разбор не должен
|
|
25
|
+
* ломаться от того, что конкретный код принёс с собой числа.
|
|
26
|
+
*/
|
|
27
|
+
export const apiError = z.object({
|
|
28
|
+
error: z.object({
|
|
29
|
+
code: z.string().min(1),
|
|
30
|
+
message: z.string().min(1),
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
/** Кредитов на балансе меньше веса рана. Значит «пополни». */
|
|
34
|
+
export const insufficientCreditsError = z.object({
|
|
35
|
+
error: z.object({
|
|
36
|
+
code: z.literal("insufficient_credits"),
|
|
37
|
+
message: z.string().min(1),
|
|
38
|
+
/** Сколько есть СЕЙЧАС. Ноль — законное значение. */
|
|
39
|
+
balance_credits: z.number().int().nonnegative(),
|
|
40
|
+
/** Сколько нужно на этот ран — та же оценка, что вернул `quote`. */
|
|
41
|
+
required_credits: z.number().int().positive(),
|
|
42
|
+
}),
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* На аккаунте висит непогашенный долг. Значит «погаси», а не «пополни».
|
|
46
|
+
*
|
|
47
|
+
* Отдельный код, а не оттенок предыдущего: у аккаунта с долгом деньги на
|
|
48
|
+
* балансе могут БЫТЬ, и ответ «недостаточно кредитов» сказал бы клиенту
|
|
49
|
+
* заведомую неправду, а его агента отправил бы покупать кредиты, которые не
|
|
50
|
+
* снимут блокировку.
|
|
51
|
+
*/
|
|
52
|
+
export const debtOutstandingError = z.object({
|
|
53
|
+
error: z.object({
|
|
54
|
+
code: z.literal("debt_outstanding"),
|
|
55
|
+
message: z.string().min(1),
|
|
56
|
+
debt_credits: z.number().int().positive(),
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Сборка тела «не хватило кредитов».
|
|
61
|
+
*
|
|
62
|
+
* Формулировка живёт ЗДЕСЬ, а не в обработчике, по той же причине, что и
|
|
63
|
+
* форма: два места, отвечающие 402 (сегодня — создание рана, завтра — любой
|
|
64
|
+
* другой платный вызов), обязаны объяснять отказ одинаково.
|
|
65
|
+
*/
|
|
66
|
+
export function insufficientCreditsBody(args) {
|
|
67
|
+
return insufficientCreditsError.parse({
|
|
68
|
+
error: {
|
|
69
|
+
code: "insufficient_credits",
|
|
70
|
+
message: `This run needs ${args.requiredCredits} credits, the account has ${args.balanceCredits}. ` +
|
|
71
|
+
"Top up the balance and retry; nothing was charged and no run was created.",
|
|
72
|
+
balance_credits: args.balanceCredits,
|
|
73
|
+
required_credits: args.requiredCredits,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
/** Сборка тела «висит долг». См. довод о формулировке выше. */
|
|
78
|
+
export function debtOutstandingBody(args) {
|
|
79
|
+
return debtOutstandingError.parse({
|
|
80
|
+
error: {
|
|
81
|
+
code: "debt_outstanding",
|
|
82
|
+
message: `An outstanding debt of ${args.debtCredits} credits blocks new paid runs. ` +
|
|
83
|
+
"Settle the debt and retry; topping up alone does not clear it.",
|
|
84
|
+
debt_credits: args.debtCredits,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACvB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;KAC3B,CAAC;CACH,CAAC,CAAC;AAGH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,sBAAsB,CAAC;QACvC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1B,qDAAqD;QACrD,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;QAC/C,oEAAoE;QACpE,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;KAC9C,CAAC;CACH,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC;QACnC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;KAC1C,CAAC;CACH,CAAC,CAAC;AAGH;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAGvC;IACC,OAAO,wBAAwB,CAAC,KAAK,CAAC;QACpC,KAAK,EAAE;YACL,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EACL,kBAAkB,IAAI,CAAC,eAAe,6BAA6B,IAAI,CAAC,cAAc,IAAI;gBAC1F,2EAA2E;YAC7E,eAAe,EAAE,IAAI,CAAC,cAAc;YACpC,gBAAgB,EAAE,IAAI,CAAC,eAAe;SACvC;KACF,CAAC,CAAC;AACL,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,mBAAmB,CAAC,IAEnC;IACC,OAAO,oBAAoB,CAAC,KAAK,CAAC;QAChC,KAAK,EAAE;YACL,IAAI,EAAE,kBAAkB;YACxB,OAAO,EACL,0BAA0B,IAAI,CAAC,WAAW,iCAAiC;gBAC3E,gEAAgE;YAClE,YAAY,EAAE,IAAI,CAAC,WAAW;SAC/B;KACF,CAAC,CAAC;AACL,CAAC"}
|