@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
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export * from "./contract-dispositions.js";
|
|
|
3
3
|
export * from "./aspect.js";
|
|
4
4
|
export * from "./voices.js";
|
|
5
5
|
export * from "./runs.js";
|
|
6
|
+
export * from "./errors.js";
|
|
7
|
+
export * from "./rate-limit.js";
|
|
8
|
+
export * from "./api-failure.js";
|
|
9
|
+
export * from "./account.js";
|
|
6
10
|
export * from "./pacing.js";
|
|
7
11
|
export * from "./render-backend.js";
|
|
8
12
|
export * from "./tts-backend.js";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,17 @@ export * from "./contract-dispositions.js";
|
|
|
8
8
|
export * from "./aspect.js";
|
|
9
9
|
export * from "./voices.js";
|
|
10
10
|
export * from "./runs.js";
|
|
11
|
+
// Тела отказов: форма ответа об ошибке — тоже контракт, и с появлением чисел в
|
|
12
|
+
// теле (402) она перестала быть выводимой из кода ответа.
|
|
13
|
+
export * from "./errors.js";
|
|
14
|
+
// Rate limit: форма 429 и имена заголовков — тот же контракт, что и тела
|
|
15
|
+
// отказов выше, и по той же причине не выводится из кода ответа.
|
|
16
|
+
export * from "./rate-limit.js";
|
|
17
|
+
// Разбор отказа: тела уже описаны выше, здесь — что с ними ДЕЛАТЬ. Живёт в
|
|
18
|
+
// core, потому что решение «ретраить или остановиться» обязано быть общим у
|
|
19
|
+
// SDK, MCP и CLI (US-622).
|
|
20
|
+
export * from "./api-failure.js";
|
|
21
|
+
export * from "./account.js";
|
|
11
22
|
export * from "./pacing.js";
|
|
12
23
|
// Вендор-нейтральные контракты: всё вендорское живёт в адаптерах, не здесь.
|
|
13
24
|
export * from "./render-backend.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,6EAA6E;AAC7E,2BAA2B;AAC3B,cAAc,4BAA4B,CAAC;AAC3C,4EAA4E;AAC5E,gEAAgE;AAChE,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,4EAA4E;AAC5E,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,6EAA6E;AAC7E,2BAA2B;AAC3B,cAAc,4BAA4B,CAAC;AAC3C,4EAA4E;AAC5E,gEAAgE;AAChE,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,+EAA+E;AAC/E,0DAA0D;AAC1D,cAAc,aAAa,CAAC;AAC5B,yEAAyE;AACzE,iEAAiE;AACjE,cAAc,iBAAiB,CAAC;AAChC,2EAA2E;AAC3E,4EAA4E;AAC5E,2BAA2B;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,4EAA4E;AAC5E,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* RATE LIMIT — ФОРМА ОТВЕТА И ИМЕНА ЗАГОЛОВКОВ, ОДНИМ ДОМОМ (US-605).
|
|
4
|
+
*
|
|
5
|
+
* `spec.md` и `docs/03-api.md` объявляли «60 rpm, заголовки
|
|
6
|
+
* `X-RateLimit-Remaining`/`X-RateLimit-Reset`, код `rate_limited` 429» — и в
|
|
7
|
+
* коде не было ничего. Строить это тремя литералами в трёх пакетах значило бы
|
|
8
|
+
* воспроизвести ровно тот разрыв между объявлением и реализацией, ради
|
|
9
|
+
* закрытия которого стори и заведена: имя заголовка, разъехавшееся между
|
|
10
|
+
* сервером и клиентом, не ловится ни компилятором, ни тестом одной стороны.
|
|
11
|
+
*
|
|
12
|
+
* Поэтому здесь лежат И схема тела, И имена заголовков, И числа лимитов.
|
|
13
|
+
* REST, SDK и MCP берут их отсюда; тест `rate-limit.contract.test.ts` в
|
|
14
|
+
* `apps/api` проверяет, что литерала `rate_limited` нет ни в одной из трёх
|
|
15
|
+
* поверхностей (правило проекта №2, блокер 9 внешнего ревью плана).
|
|
16
|
+
*/
|
|
17
|
+
/** Имя заголовка с потолком ведра — сколько запросов в окне разрешено всего. */
|
|
18
|
+
export declare const RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit";
|
|
19
|
+
/** Сколько запросов в текущем окне ещё осталось. Ноль — законное значение. */
|
|
20
|
+
export declare const RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining";
|
|
21
|
+
/**
|
|
22
|
+
* Через сколько СЕКУНД остаток вырастет хотя бы на единицу.
|
|
23
|
+
*
|
|
24
|
+
* ЕДИНИЦА — ДЕЛЬТА, А НЕ UNIX-ВРЕМЯ, и это осознанное расхождение с GitHub.
|
|
25
|
+
* Дельта не зависит от часов клиента: агент на машине с уехавшим временем,
|
|
26
|
+
* вычитая epoch из своего `Date.now()`, ждал бы неверно и упирался бы в лимит
|
|
27
|
+
* повторно. Направление то же, что у черновика IETF (`RateLimit-Reset`), и то
|
|
28
|
+
* же, что у `Retry-After` ниже — две единицы в одном ответе путали бы сильнее,
|
|
29
|
+
* чем расхождение с чужой реализацией.
|
|
30
|
+
*
|
|
31
|
+
* СМЫСЛ ИМЕННО «ВЫРАСТЕТ НА ЕДИНИЦУ», а не «обнулится счётчик»: окно
|
|
32
|
+
* скользящее, и полного обнуления в нём не происходит — запросы выпадают из
|
|
33
|
+
* окна по одному. Для отказа это ровно момент, когда можно повторить.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RATE_LIMIT_RESET_HEADER = "X-RateLimit-Reset";
|
|
36
|
+
/** Стандартный `Retry-After` (RFC 9110), в секундах. Только на 429. */
|
|
37
|
+
export declare const RETRY_AFTER_HEADER = "Retry-After";
|
|
38
|
+
/** Длина окна. Одна для обоих вёдер: «в минуту» — то, что объявлено. */
|
|
39
|
+
export declare const RATE_LIMIT_WINDOW_SECONDS = 60;
|
|
40
|
+
/**
|
|
41
|
+
* ПЛАТНОЕ ВЕДРО — 60 запросов в минуту. Число не выбрано здесь, а взято из
|
|
42
|
+
* объявления (`spec.md`, `docs/03-api.md`): стори закрывает разрыв «объявлено,
|
|
43
|
+
* но не построено», и менять заодно само объявление значило бы закрыть его
|
|
44
|
+
* подгонкой.
|
|
45
|
+
*/
|
|
46
|
+
export declare const RATE_LIMIT_PAID_PER_MINUTE = 60;
|
|
47
|
+
/**
|
|
48
|
+
* БЕСПЛАТНОЕ ВЕДРО — 300 запросов в минуту, отдельным счётчиком.
|
|
49
|
+
*
|
|
50
|
+
* ПОЧЕМУ ОТДЕЛЬНО. Общее ведро с платным путём отравило бы ШТАТНЫЙ агентный
|
|
51
|
+
* цикл: по ADR-009 клиент поллит `GET /v1/runs/:id` каждые ~5 с, то есть 12
|
|
52
|
+
* запросов в минуту НА РАН. При потолке в три параллельных рана (та же стори)
|
|
53
|
+
* один только поллинг съедает 36 из 60, и агент упирался бы в лимит, ничего не
|
|
54
|
+
* заказывая — отказ там, где нет ни траты, ни нагрузки на вендора.
|
|
55
|
+
*
|
|
56
|
+
* ОТКУДА 300. Пол — те самые 36 плюс `quote` перед каждым запуском и редкий
|
|
57
|
+
* `voices`: около 40 в минуту на аккаунт, работающий на полном потолке
|
|
58
|
+
* параллельности. 300 даёт запас в 7.5 раза (место для ретраев, нескольких
|
|
59
|
+
* агентов на одном аккаунте и более частого поллинга), но остаётся ПОТОЛКОМ:
|
|
60
|
+
* зациклившийся клиент упирается в него на пятой секунде, а не выкачивает
|
|
61
|
+
* базу минутами. Число живёт здесь, а не в переменной окружения, потому что
|
|
62
|
+
* оно часть публичного контракта и объявлено в `docs/03-api.md`.
|
|
63
|
+
*/
|
|
64
|
+
export declare const RATE_LIMIT_FREE_PER_MINUTE = 300;
|
|
65
|
+
/**
|
|
66
|
+
* ПОТОЛОК ОДНОВРЕМЕННЫХ РЕНДЕРОВ НА АККАУНТ — 3 (`spec.md:106`).
|
|
67
|
+
*
|
|
68
|
+
* Число живёт рядом с лимитом частоты, потому что они считают одно и то же
|
|
69
|
+
* сверху: сколько работы аккаунт может держать у нас в моменте. Из него же
|
|
70
|
+
* выведен пол бесплатного ведра (три рана × 12 опросов в минуту).
|
|
71
|
+
*
|
|
72
|
+
* ПРЕВЫШЕНИЕ СТАВИТ В ОЧЕРЕДЬ, А НЕ ОТВЕРГАЕТ. Четвёртый ран получает 202 и
|
|
73
|
+
* ждёт своей очереди; отказ здесь был бы хуже для агента, чем ожидание, —
|
|
74
|
+
* ему пришлось бы городить собственный планировщик поверх нашего.
|
|
75
|
+
*
|
|
76
|
+
* ЭТОТ ПОТОЛОК ЖИВЁТ ПОД ОБЩИМ ЛИМИТОМ ТАРИФА Trigger.dev, а не вместо него.
|
|
77
|
+
* Числа тарифа и следствие для волны N=50 — в `docs/02-architecture.md`.
|
|
78
|
+
*/
|
|
79
|
+
export declare const MAX_CONCURRENT_RENDERS_PER_ACCOUNT = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Ведро. Оно же — ось счётчика в БД и значение колонки `bucket`.
|
|
82
|
+
*
|
|
83
|
+
* Тип живёт здесь, а не в `service-core`, ровно потому, что ведро НАЗВАНО в
|
|
84
|
+
* ответе клиенту (сообщение ниже отличает платный лимит от бесплатного).
|
|
85
|
+
* Разъедься строка в счётчике со строкой в сообщении — клиент получал бы
|
|
86
|
+
* совет ждать не того окна.
|
|
87
|
+
*/
|
|
88
|
+
export type RateLimitScope = "paid" | "free";
|
|
89
|
+
/**
|
|
90
|
+
* Тело отказа 429. Числа лежат ВНУТРИ `error` — по тому же доводу, что и у
|
|
91
|
+
* денежных отказов в `errors.ts`: у ответа об ошибке один конверт, и всё, что
|
|
92
|
+
* объясняет отказ, лежит в нём.
|
|
93
|
+
*/
|
|
94
|
+
export declare const rateLimitedError: z.ZodObject<{
|
|
95
|
+
error: z.ZodObject<{
|
|
96
|
+
code: z.ZodLiteral<"rate_limited">;
|
|
97
|
+
message: z.ZodString;
|
|
98
|
+
limit: z.ZodNumber;
|
|
99
|
+
window_seconds: z.ZodNumber;
|
|
100
|
+
retry_after_seconds: z.ZodNumber;
|
|
101
|
+
}, z.core.$strip>;
|
|
102
|
+
}, z.core.$strip>;
|
|
103
|
+
export type RateLimitedError = z.infer<typeof rateLimitedError>;
|
|
104
|
+
/**
|
|
105
|
+
* Сборка тела «слишком часто».
|
|
106
|
+
*
|
|
107
|
+
* Формулировка живёт ЗДЕСЬ, а не в мидлваре, по той же причине, что и форма:
|
|
108
|
+
* вёдер два, и объяснять отказ они обязаны одинаково. Сообщение НАЗЫВАЕТ
|
|
109
|
+
* ведро — иначе агент, упершийся в платный лимит, не отличит его от
|
|
110
|
+
* бесплатного и остановит поллинг уже принятых ранов.
|
|
111
|
+
*/
|
|
112
|
+
export declare function rateLimitedBody(args: {
|
|
113
|
+
limit: number;
|
|
114
|
+
windowSeconds: number;
|
|
115
|
+
retryAfterSeconds: number;
|
|
116
|
+
scope: RateLimitScope;
|
|
117
|
+
/**
|
|
118
|
+
* Был ли отбитый запрос АДРЕСОВАН СОЗДАНИЮ РАНА. Знает это только вызыватель:
|
|
119
|
+
* ведро само по себе не различает платный маршрут и любой неизвестный путь,
|
|
120
|
+
* который попадает туда же по безопасному дефолту.
|
|
121
|
+
*
|
|
122
|
+
* Нужен ради правдивости совета (внешнее ревью, круг 5): предупреждение
|
|
123
|
+
* «новый ключ может запустить ВТОРОЙ платный рендер» осмысленно только там,
|
|
124
|
+
* где рендер вообще бывает; на опечатке в пути или неподдержанном методе оно
|
|
125
|
+
* говорит о невозможном. По умолчанию `false` — молчим, пока не сказано
|
|
126
|
+
* обратное.
|
|
127
|
+
*
|
|
128
|
+
* ИМЯ ГОВОРИТ «АДРЕСОВАН», А НЕ «ЗАПУСТИТ», И ЭТО ТОЧНОЕ СЛОВО (круг 6).
|
|
129
|
+
* Знать, дошёл бы запрос до создания рана, мидлвара не может: битое тело,
|
|
130
|
+
* отклонённое поле, конфликт формата, опущенный килсвитч и нехватка кредитов
|
|
131
|
+
* отбиваются УЖЕ В ОБРАБОТЧИКЕ, то есть после лимита. Чтобы это выяснить,
|
|
132
|
+
* пришлось бы выполнить работу обработчика до решения о лимите — ровно ту
|
|
133
|
+
* работу, ради недопущения которой лимит и стоит впереди. Поэтому доступный
|
|
134
|
+
* факт один: «запрос шёл на платный маршрут», и обещание сформулировано в
|
|
135
|
+
* сослагательном («может»), а не в изъявительном наклонении.
|
|
136
|
+
*/
|
|
137
|
+
targetsRunCreation?: boolean;
|
|
138
|
+
}): RateLimitedError;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* RATE LIMIT — ФОРМА ОТВЕТА И ИМЕНА ЗАГОЛОВКОВ, ОДНИМ ДОМОМ (US-605).
|
|
4
|
+
*
|
|
5
|
+
* `spec.md` и `docs/03-api.md` объявляли «60 rpm, заголовки
|
|
6
|
+
* `X-RateLimit-Remaining`/`X-RateLimit-Reset`, код `rate_limited` 429» — и в
|
|
7
|
+
* коде не было ничего. Строить это тремя литералами в трёх пакетах значило бы
|
|
8
|
+
* воспроизвести ровно тот разрыв между объявлением и реализацией, ради
|
|
9
|
+
* закрытия которого стори и заведена: имя заголовка, разъехавшееся между
|
|
10
|
+
* сервером и клиентом, не ловится ни компилятором, ни тестом одной стороны.
|
|
11
|
+
*
|
|
12
|
+
* Поэтому здесь лежат И схема тела, И имена заголовков, И числа лимитов.
|
|
13
|
+
* REST, SDK и MCP берут их отсюда; тест `rate-limit.contract.test.ts` в
|
|
14
|
+
* `apps/api` проверяет, что литерала `rate_limited` нет ни в одной из трёх
|
|
15
|
+
* поверхностей (правило проекта №2, блокер 9 внешнего ревью плана).
|
|
16
|
+
*/
|
|
17
|
+
/** Имя заголовка с потолком ведра — сколько запросов в окне разрешено всего. */
|
|
18
|
+
export const RATE_LIMIT_LIMIT_HEADER = "X-RateLimit-Limit";
|
|
19
|
+
/** Сколько запросов в текущем окне ещё осталось. Ноль — законное значение. */
|
|
20
|
+
export const RATE_LIMIT_REMAINING_HEADER = "X-RateLimit-Remaining";
|
|
21
|
+
/**
|
|
22
|
+
* Через сколько СЕКУНД остаток вырастет хотя бы на единицу.
|
|
23
|
+
*
|
|
24
|
+
* ЕДИНИЦА — ДЕЛЬТА, А НЕ UNIX-ВРЕМЯ, и это осознанное расхождение с GitHub.
|
|
25
|
+
* Дельта не зависит от часов клиента: агент на машине с уехавшим временем,
|
|
26
|
+
* вычитая epoch из своего `Date.now()`, ждал бы неверно и упирался бы в лимит
|
|
27
|
+
* повторно. Направление то же, что у черновика IETF (`RateLimit-Reset`), и то
|
|
28
|
+
* же, что у `Retry-After` ниже — две единицы в одном ответе путали бы сильнее,
|
|
29
|
+
* чем расхождение с чужой реализацией.
|
|
30
|
+
*
|
|
31
|
+
* СМЫСЛ ИМЕННО «ВЫРАСТЕТ НА ЕДИНИЦУ», а не «обнулится счётчик»: окно
|
|
32
|
+
* скользящее, и полного обнуления в нём не происходит — запросы выпадают из
|
|
33
|
+
* окна по одному. Для отказа это ровно момент, когда можно повторить.
|
|
34
|
+
*/
|
|
35
|
+
export const RATE_LIMIT_RESET_HEADER = "X-RateLimit-Reset";
|
|
36
|
+
/** Стандартный `Retry-After` (RFC 9110), в секундах. Только на 429. */
|
|
37
|
+
export const RETRY_AFTER_HEADER = "Retry-After";
|
|
38
|
+
/** Длина окна. Одна для обоих вёдер: «в минуту» — то, что объявлено. */
|
|
39
|
+
export const RATE_LIMIT_WINDOW_SECONDS = 60;
|
|
40
|
+
/**
|
|
41
|
+
* ПЛАТНОЕ ВЕДРО — 60 запросов в минуту. Число не выбрано здесь, а взято из
|
|
42
|
+
* объявления (`spec.md`, `docs/03-api.md`): стори закрывает разрыв «объявлено,
|
|
43
|
+
* но не построено», и менять заодно само объявление значило бы закрыть его
|
|
44
|
+
* подгонкой.
|
|
45
|
+
*/
|
|
46
|
+
export const RATE_LIMIT_PAID_PER_MINUTE = 60;
|
|
47
|
+
/**
|
|
48
|
+
* БЕСПЛАТНОЕ ВЕДРО — 300 запросов в минуту, отдельным счётчиком.
|
|
49
|
+
*
|
|
50
|
+
* ПОЧЕМУ ОТДЕЛЬНО. Общее ведро с платным путём отравило бы ШТАТНЫЙ агентный
|
|
51
|
+
* цикл: по ADR-009 клиент поллит `GET /v1/runs/:id` каждые ~5 с, то есть 12
|
|
52
|
+
* запросов в минуту НА РАН. При потолке в три параллельных рана (та же стори)
|
|
53
|
+
* один только поллинг съедает 36 из 60, и агент упирался бы в лимит, ничего не
|
|
54
|
+
* заказывая — отказ там, где нет ни траты, ни нагрузки на вендора.
|
|
55
|
+
*
|
|
56
|
+
* ОТКУДА 300. Пол — те самые 36 плюс `quote` перед каждым запуском и редкий
|
|
57
|
+
* `voices`: около 40 в минуту на аккаунт, работающий на полном потолке
|
|
58
|
+
* параллельности. 300 даёт запас в 7.5 раза (место для ретраев, нескольких
|
|
59
|
+
* агентов на одном аккаунте и более частого поллинга), но остаётся ПОТОЛКОМ:
|
|
60
|
+
* зациклившийся клиент упирается в него на пятой секунде, а не выкачивает
|
|
61
|
+
* базу минутами. Число живёт здесь, а не в переменной окружения, потому что
|
|
62
|
+
* оно часть публичного контракта и объявлено в `docs/03-api.md`.
|
|
63
|
+
*/
|
|
64
|
+
export const RATE_LIMIT_FREE_PER_MINUTE = 300;
|
|
65
|
+
/**
|
|
66
|
+
* ПОТОЛОК ОДНОВРЕМЕННЫХ РЕНДЕРОВ НА АККАУНТ — 3 (`spec.md:106`).
|
|
67
|
+
*
|
|
68
|
+
* Число живёт рядом с лимитом частоты, потому что они считают одно и то же
|
|
69
|
+
* сверху: сколько работы аккаунт может держать у нас в моменте. Из него же
|
|
70
|
+
* выведен пол бесплатного ведра (три рана × 12 опросов в минуту).
|
|
71
|
+
*
|
|
72
|
+
* ПРЕВЫШЕНИЕ СТАВИТ В ОЧЕРЕДЬ, А НЕ ОТВЕРГАЕТ. Четвёртый ран получает 202 и
|
|
73
|
+
* ждёт своей очереди; отказ здесь был бы хуже для агента, чем ожидание, —
|
|
74
|
+
* ему пришлось бы городить собственный планировщик поверх нашего.
|
|
75
|
+
*
|
|
76
|
+
* ЭТОТ ПОТОЛОК ЖИВЁТ ПОД ОБЩИМ ЛИМИТОМ ТАРИФА Trigger.dev, а не вместо него.
|
|
77
|
+
* Числа тарифа и следствие для волны N=50 — в `docs/02-architecture.md`.
|
|
78
|
+
*/
|
|
79
|
+
export const MAX_CONCURRENT_RENDERS_PER_ACCOUNT = 3;
|
|
80
|
+
/**
|
|
81
|
+
* Тело отказа 429. Числа лежат ВНУТРИ `error` — по тому же доводу, что и у
|
|
82
|
+
* денежных отказов в `errors.ts`: у ответа об ошибке один конверт, и всё, что
|
|
83
|
+
* объясняет отказ, лежит в нём.
|
|
84
|
+
*/
|
|
85
|
+
export const rateLimitedError = z.object({
|
|
86
|
+
error: z.object({
|
|
87
|
+
code: z.literal("rate_limited"),
|
|
88
|
+
message: z.string().min(1),
|
|
89
|
+
/** Потолок ведра, в которое попал запрос. */
|
|
90
|
+
limit: z.number().int().positive(),
|
|
91
|
+
/** Длина окна в секундах — без неё `limit` не значит ничего. */
|
|
92
|
+
window_seconds: z.number().int().positive(),
|
|
93
|
+
/** Через сколько секунд повторять. Совпадает с `Retry-After`. */
|
|
94
|
+
retry_after_seconds: z.number().int().positive(),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Сборка тела «слишком часто».
|
|
99
|
+
*
|
|
100
|
+
* Формулировка живёт ЗДЕСЬ, а не в мидлваре, по той же причине, что и форма:
|
|
101
|
+
* вёдер два, и объяснять отказ они обязаны одинаково. Сообщение НАЗЫВАЕТ
|
|
102
|
+
* ведро — иначе агент, упершийся в платный лимит, не отличит его от
|
|
103
|
+
* бесплатного и остановит поллинг уже принятых ранов.
|
|
104
|
+
*/
|
|
105
|
+
export function rateLimitedBody(args) {
|
|
106
|
+
// ХВОСТ СООБЩЕНИЯ ЗАВИСИТ ОТ ВЕДРА, И ЭТО НЕ УКРАШЕНИЕ (внешнее ревью, круги 2 и 3).
|
|
107
|
+
//
|
|
108
|
+
// Первая редакция дописывала «no run was created» к ОБОИМ отказам, и это была
|
|
109
|
+
// ложь дважды.
|
|
110
|
+
//
|
|
111
|
+
// НА БЕСПЛАТНОМ ВЕДРЕ: агент, опрашивающий статус уже ЗАПУЩЕННОГО рана,
|
|
112
|
+
// получал бы 429 с уверением, что рана нет, — и мог бы бросить работу, за
|
|
113
|
+
// которую уже заплачено. Ту же работу защищает и решение пропускать
|
|
114
|
+
// бесплатные пути при отказе счётчика.
|
|
115
|
+
//
|
|
116
|
+
// НА ПЛАТНОМ: лимит стоит ПЕРЕД обработчиком, поэтому 429 получает и ПОВТОР
|
|
117
|
+
// с уже занятым `Idempotency-Key` — запрос, за которым ран существует. «No
|
|
118
|
+
// run was created» толкает агента взять новый ключ, а новый ключ есть второй
|
|
119
|
+
// платный рендер. Поэтому платный хвост больше ничего не утверждает о
|
|
120
|
+
// существовании рана и прямо велит повторить С ТЕМ ЖЕ ключом: это верно и
|
|
121
|
+
// для первого запроса (там повтор просто создаст ран), и для повтора.
|
|
122
|
+
// ОПИСАНИЕ ВЕДРА ТОЖЕ ОБЯЗАНО БЫТЬ ВЕРНЫМ (внешнее ревью, круг 4). В платное
|
|
123
|
+
// ведро по построению попадает не только создание рана, но и любой
|
|
124
|
+
// неизвестный путь под авторизованными префиксами — так выбран безопасный
|
|
125
|
+
// дефолт. Формулировка «starting runs» описывала бы клиенту не тот отказ.
|
|
126
|
+
const surface = args.scope === "paid"
|
|
127
|
+
? "paid requests (starting a run, and any unrecognised authenticated path)"
|
|
128
|
+
: "free requests (quotes, voices, run status, account)";
|
|
129
|
+
// СОВЕТ ПРО КЛЮЧ ДАЁТСЯ ТОЛЬКО ТАМ, ГДЕ ОН ОСМЫСЛЕН (круги 4 и 5).
|
|
130
|
+
//
|
|
131
|
+
// Круг 4 снял безусловность: 429 получает и запрос, который ключа не
|
|
132
|
+
// присылал. Круг 5 снял вторую неточность — предупреждение о «втором платном
|
|
133
|
+
// рендере» уходило и на опечатку в пути, и на неподдержанный метод, где
|
|
134
|
+
// никакой ключ ничего не отрендерит. Теперь его печатает только запуск рана,
|
|
135
|
+
// и `targetsRunCreation` приходит от мидлвары — она единственная знает маршрут.
|
|
136
|
+
const tail = args.scope === "free"
|
|
137
|
+
? "nothing was charged; runs already started keep going and stay retrievable."
|
|
138
|
+
: args.targetsRunCreation === true
|
|
139
|
+
? "nothing was charged. If this request carried an Idempotency-Key, retry with the SAME key: a new key can start a second paid render."
|
|
140
|
+
: "nothing was charged.";
|
|
141
|
+
return rateLimitedError.parse({
|
|
142
|
+
error: {
|
|
143
|
+
code: "rate_limited",
|
|
144
|
+
message: `Rate limit of ${args.limit} ${surface} per ${args.windowSeconds} seconds is exhausted ` +
|
|
145
|
+
`for this account. Retry in ${args.retryAfterSeconds} seconds; ${tail}`,
|
|
146
|
+
limit: args.limit,
|
|
147
|
+
window_seconds: args.windowSeconds,
|
|
148
|
+
retry_after_seconds: args.retryAfterSeconds,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;GAcG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D,8EAA8E;AAC9E,MAAM,CAAC,MAAM,2BAA2B,GAAG,uBAAuB,CAAC;AAEnE;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,mBAAmB,CAAC;AAE3D,uEAAuE;AACvE,MAAM,CAAC,MAAM,kBAAkB,GAAG,aAAa,CAAC;AAEhD,wEAAwE;AACxE,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,EAAE,CAAC;AAE7C;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,GAAG,CAAC;AAE9C;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,CAAC,CAAC;AAYpD;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC;QACd,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,cAAc,CAAC;QAC/B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1B,6CAA6C;QAC7C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QAClC,gEAAgE;QAChE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;QAC3C,iEAAiE;QACjE,mBAAmB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;KACjD,CAAC;CACH,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,IA0B/B;IACC,qFAAqF;IACrF,EAAE;IACF,8EAA8E;IAC9E,eAAe;IACf,EAAE;IACF,wEAAwE;IACxE,0EAA0E;IAC1E,oEAAoE;IACpE,uCAAuC;IACvC,EAAE;IACF,4EAA4E;IAC5E,2EAA2E;IAC3E,6EAA6E;IAC7E,sEAAsE;IACtE,0EAA0E;IAC1E,sEAAsE;IACtE,6EAA6E;IAC7E,mEAAmE;IACnE,0EAA0E;IAC1E,0EAA0E;IAC1E,MAAM,OAAO,GACX,IAAI,CAAC,KAAK,KAAK,MAAM;QACnB,CAAC,CAAC,yEAAyE;QAC3E,CAAC,CAAC,qDAAqD,CAAC;IAE5D,mEAAmE;IACnE,EAAE;IACF,qEAAqE;IACrE,6EAA6E;IAC7E,wEAAwE;IACxE,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,IAAI,GACR,IAAI,CAAC,KAAK,KAAK,MAAM;QACnB,CAAC,CAAC,4EAA4E;QAC9E,CAAC,CAAC,IAAI,CAAC,kBAAkB,KAAK,IAAI;YAChC,CAAC,CAAC,qIAAqI;YACvI,CAAC,CAAC,sBAAsB,CAAC;IAC/B,OAAO,gBAAgB,CAAC,KAAK,CAAC;QAC5B,KAAK,EAAE;YACL,IAAI,EAAE,cAAc;YACpB,OAAO,EACL,iBAAiB,IAAI,CAAC,KAAK,IAAI,OAAO,QAAQ,IAAI,CAAC,aAAa,wBAAwB;gBACxF,8BAA8B,IAAI,CAAC,iBAAiB,aAAa,IAAI,EAAE;YACzE,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,cAAc,EAAE,IAAI,CAAC,aAAa;YAClC,mBAAmB,EAAE,IAAI,CAAC,iBAAiB;SAC5C;KACF,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clipwright/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Zod schemas and the shared request/response contract of the Clipwright UGC video API",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "Dimantika
|
|
6
|
+
"author": "Dimantika Sp. z o.o.",
|
|
7
7
|
"homepage": "https://clipwright.io",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"publishConfig": {
|