@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/LICENSE
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* ТЕЛО `GET /v1/account` (US-602).
|
|
4
|
+
*
|
|
5
|
+
* ПУТЬ ОБЪЯВЛЕН ДАВНО, ТЕЛО — НЕТ. `docs/03-api.md` обещает этот маршрут с
|
|
6
|
+
* телом `{plan, credits_balance, renews_at}`. Путь берётся объявленный, тело —
|
|
7
|
+
* честное: `plan` и `renews_at` мертвы по ADR-012 (тарифных планов и продлений
|
|
8
|
+
* в продукте нет), и отдавать их означало бы поддерживать поля, за которыми
|
|
9
|
+
* ничего не стоит. Обещание снимается из документа отдельной стори (US-623).
|
|
10
|
+
*
|
|
11
|
+
* ЧЕТЫРЕ ЧИСЛА, А НЕ ОДНО, И КАЖДОЕ ОТВЕЧАЕТ НА СВОЙ ВОПРОС:
|
|
12
|
+
* - `balance_credits` — сколько денег есть;
|
|
13
|
+
* - `debt_credits` — сколько должен (блокирует новые раны отдельным кодом);
|
|
14
|
+
* - `holds_credits` — сколько заморожено под уже идущими ранами;
|
|
15
|
+
* - `daily_remaining_credits` — сколько ещё пропустит НАШ суточный
|
|
16
|
+
* предохранитель.
|
|
17
|
+
*
|
|
18
|
+
* Последнее — НЕ деньги, и потому названо отдельно, а не вычтено из баланса.
|
|
19
|
+
* Смешай их — и клиент с деньгами на счету, упёршийся в наш операционный
|
|
20
|
+
* лимит, прочтёт это как «кончились кредиты» и пойдёт покупать ещё. Ровно то
|
|
21
|
+
* же различение держат два разных кода отказа (`errors.ts`).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Начисление. `source` — z.string(), а не enum: перечень источников живёт в
|
|
25
|
+
* базе и пополняется (промо, партнёрка), а read-контракт обязан переживать
|
|
26
|
+
* новое значение у уже установленного клиента — та же асимметрия чтения и
|
|
27
|
+
* записи, что у стадий рана (`runRead`).
|
|
28
|
+
*/
|
|
29
|
+
export declare const creditGrant: z.ZodObject<{
|
|
30
|
+
amount: z.ZodNumber;
|
|
31
|
+
source: z.ZodString;
|
|
32
|
+
expires_at: z.ZodNullable<z.ZodString>;
|
|
33
|
+
created_at: z.ZodString;
|
|
34
|
+
}, z.core.$strip>;
|
|
35
|
+
export type CreditGrant = z.infer<typeof creditGrant>;
|
|
36
|
+
export declare const account: z.ZodObject<{
|
|
37
|
+
account_id: z.ZodString;
|
|
38
|
+
balance_credits: z.ZodNumber;
|
|
39
|
+
debt_credits: z.ZodNumber;
|
|
40
|
+
holds_credits: z.ZodNumber;
|
|
41
|
+
daily_remaining_credits: z.ZodNumber;
|
|
42
|
+
grants: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
43
|
+
amount: z.ZodNumber;
|
|
44
|
+
source: z.ZodString;
|
|
45
|
+
expires_at: z.ZodNullable<z.ZodString>;
|
|
46
|
+
created_at: z.ZodString;
|
|
47
|
+
}, z.core.$strip>>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export type Account = z.infer<typeof account>;
|
package/dist/account.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* ТЕЛО `GET /v1/account` (US-602).
|
|
4
|
+
*
|
|
5
|
+
* ПУТЬ ОБЪЯВЛЕН ДАВНО, ТЕЛО — НЕТ. `docs/03-api.md` обещает этот маршрут с
|
|
6
|
+
* телом `{plan, credits_balance, renews_at}`. Путь берётся объявленный, тело —
|
|
7
|
+
* честное: `plan` и `renews_at` мертвы по ADR-012 (тарифных планов и продлений
|
|
8
|
+
* в продукте нет), и отдавать их означало бы поддерживать поля, за которыми
|
|
9
|
+
* ничего не стоит. Обещание снимается из документа отдельной стори (US-623).
|
|
10
|
+
*
|
|
11
|
+
* ЧЕТЫРЕ ЧИСЛА, А НЕ ОДНО, И КАЖДОЕ ОТВЕЧАЕТ НА СВОЙ ВОПРОС:
|
|
12
|
+
* - `balance_credits` — сколько денег есть;
|
|
13
|
+
* - `debt_credits` — сколько должен (блокирует новые раны отдельным кодом);
|
|
14
|
+
* - `holds_credits` — сколько заморожено под уже идущими ранами;
|
|
15
|
+
* - `daily_remaining_credits` — сколько ещё пропустит НАШ суточный
|
|
16
|
+
* предохранитель.
|
|
17
|
+
*
|
|
18
|
+
* Последнее — НЕ деньги, и потому названо отдельно, а не вычтено из баланса.
|
|
19
|
+
* Смешай их — и клиент с деньгами на счету, упёршийся в наш операционный
|
|
20
|
+
* лимит, прочтёт это как «кончились кредиты» и пойдёт покупать ещё. Ровно то
|
|
21
|
+
* же различение держат два разных кода отказа (`errors.ts`).
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Начисление. `source` — z.string(), а не enum: перечень источников живёт в
|
|
25
|
+
* базе и пополняется (промо, партнёрка), а read-контракт обязан переживать
|
|
26
|
+
* новое значение у уже установленного клиента — та же асимметрия чтения и
|
|
27
|
+
* записи, что у стадий рана (`runRead`).
|
|
28
|
+
*/
|
|
29
|
+
export const creditGrant = z.object({
|
|
30
|
+
amount: z.number().int(),
|
|
31
|
+
source: z.string(),
|
|
32
|
+
/** `null` — грант бессрочный. */
|
|
33
|
+
expires_at: z.string().datetime().nullable(),
|
|
34
|
+
created_at: z.string().datetime(),
|
|
35
|
+
});
|
|
36
|
+
export const account = z.object({
|
|
37
|
+
account_id: z.string(),
|
|
38
|
+
balance_credits: z.number().int().nonnegative(),
|
|
39
|
+
debt_credits: z.number().int().nonnegative(),
|
|
40
|
+
holds_credits: z.number().int().nonnegative(),
|
|
41
|
+
daily_remaining_credits: z.number().int().nonnegative(),
|
|
42
|
+
/** Только НЕИСТЁКШИЕ: истёкший грант в баланс не входит и в списке не лжёт. */
|
|
43
|
+
grants: z.array(creditGrant).default([]),
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=account.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IACxB,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;IAClB,iCAAiC;IACjC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAC5C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE;IACtB,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC/C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC5C,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC7C,uBAAuB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACvD,+EAA+E;IAC/E,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACzC,CAAC,CAAC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* РАЗБОР ОТКАЗА API — ОДИН НА ВСЕ КЛИЕНТСКИЕ ПОВЕРХНОСТИ (US-622).
|
|
3
|
+
*
|
|
4
|
+
* ЧТО БЫЛО. `packages/sdk` сводил ЛЮБОЙ не-2xx к плоскому `new Error(message)`:
|
|
5
|
+
* ни статуса, ни кода, ни `Retry-After`. Пока API отвечал одними четырёхсотыми
|
|
6
|
+
* «поле не то», разницы не было — отказ был один, и делать с ним было нечего,
|
|
7
|
+
* кроме как показать текст. Фаза 1 добавила три кода, с которыми надо
|
|
8
|
+
* ПОСТУПАТЬ по-разному: 429 — подождать и повторить, 402 — остановиться и
|
|
9
|
+
* сказать человеку числа, 5xx — повторить без паузы от сервера. Плоский `Error`
|
|
10
|
+
* делает все три неразличимыми, и клиент выбирает худший вариант из трёх:
|
|
11
|
+
* первый же 429 на поллинге убивает `makeUgc` целиком.
|
|
12
|
+
*
|
|
13
|
+
* ПОЧЕМУ РАЗБОР ЖИВЁТ ЗДЕСЬ, А НЕ В SDK. Поверхностей, читающих отказ, три —
|
|
14
|
+
* SDK, MCP-форматтер и CLI, — и решение «ретраить или нет» обязано быть у них
|
|
15
|
+
* общим. Разъедься оно, и MCP советовал бы агенту ждать там, где SDK уже сдался.
|
|
16
|
+
* Тот же довод, что и у самих схем (правило проекта №2): формы отказов уже
|
|
17
|
+
* лежат в `errors.ts` и `rate-limit.ts`, здесь — только их РАЗБОР и вывод.
|
|
18
|
+
*
|
|
19
|
+
* ЧЕГО ЭТОТ МОДУЛЬ НЕ ДЕЛАЕТ. Он не ходит в сеть и не знает про `fetch`:
|
|
20
|
+
* вход — статус, текст тела и заголовок. Поэтому его можно прогнать на всех
|
|
21
|
+
* формах ответа, включая те, которые живой сервер отдать не может, а прокси
|
|
22
|
+
* между ним и клиентом — вполне (HTML-страница про перегрузку).
|
|
23
|
+
*/
|
|
24
|
+
/** Отказ по частоте: 429 либо наш, либо чужой (прокси, edge тарифа). */
|
|
25
|
+
export interface RateLimitedFailure {
|
|
26
|
+
kind: "rate_limited";
|
|
27
|
+
status: number;
|
|
28
|
+
message: string;
|
|
29
|
+
/** Через сколько секунд повторять. `undefined` — сервер не сказал. */
|
|
30
|
+
retryAfterSeconds: number | undefined;
|
|
31
|
+
/** Потолок ведра. Есть только у НАШЕГО тела; у прокси его нет. */
|
|
32
|
+
limit: number | undefined;
|
|
33
|
+
windowSeconds: number | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Кредитов меньше веса рана. Терминально: повтор не изменит баланс. */
|
|
36
|
+
export interface InsufficientCreditsFailure {
|
|
37
|
+
kind: "insufficient_credits";
|
|
38
|
+
status: number;
|
|
39
|
+
message: string;
|
|
40
|
+
balanceCredits: number;
|
|
41
|
+
requiredCredits: number;
|
|
42
|
+
}
|
|
43
|
+
/** Висит долг. Тоже терминально, но лечится погашением, а не пополнением. */
|
|
44
|
+
export interface DebtOutstandingFailure {
|
|
45
|
+
kind: "debt_outstanding";
|
|
46
|
+
status: number;
|
|
47
|
+
message: string;
|
|
48
|
+
debtCredits: number;
|
|
49
|
+
}
|
|
50
|
+
/** 5xx: сервер обещает, что дело в нём. Повторять можно. */
|
|
51
|
+
export interface ServerFailure {
|
|
52
|
+
kind: "server_error";
|
|
53
|
+
status: number;
|
|
54
|
+
message: string;
|
|
55
|
+
retryAfterSeconds: number | undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Всё остальное (4xx): повтор того же запроса даст то же самое. */
|
|
58
|
+
export interface ClientFailure {
|
|
59
|
+
kind: "client_error";
|
|
60
|
+
status: number;
|
|
61
|
+
message: string;
|
|
62
|
+
/** Код из тела, если тело вообще было контрактным. */
|
|
63
|
+
code: string | undefined;
|
|
64
|
+
}
|
|
65
|
+
export type ApiFailure = RateLimitedFailure | InsufficientCreditsFailure | DebtOutstandingFailure | ServerFailure | ClientFailure;
|
|
66
|
+
/**
|
|
67
|
+
* ПОВТОРЯТЬ ИЛИ НЕТ — ОДНОЙ ФУНКЦИЕЙ, а не условием на каждой поверхности.
|
|
68
|
+
*
|
|
69
|
+
* Ошибиться здесь можно в обе стороны, и цены разные. Не повторить 429 — это
|
|
70
|
+
* оборванный `makeUgc` на ране, за который уже заплачено. Повторить 402 — это
|
|
71
|
+
* агент, крутящий отказ по деньгам в цикле: баланс от повторов не растёт, а
|
|
72
|
+
* лимит частоты они съедают, и человек получает вместо «пополни счёт» молчание
|
|
73
|
+
* до самого дедлайна.
|
|
74
|
+
*/
|
|
75
|
+
export declare function isRetryableFailure(failure: ApiFailure): boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Пауза перед повтором, мс. `fallbackMs` — собственный шаг вызывателя (для
|
|
78
|
+
* поллинга это его интервал): он же и пол, потому что сервер, не назвавший
|
|
79
|
+
* `Retry-After`, не разрешил долбить чаще обычного.
|
|
80
|
+
*
|
|
81
|
+
* Вызывать имеет смысл только после `isRetryableFailure`; на терминальном
|
|
82
|
+
* отказе результат бессмыслен, и функция не притворяется, что это не так.
|
|
83
|
+
*/
|
|
84
|
+
export declare function retryDelayMs(failure: ApiFailure, fallbackMs: number): number;
|
|
85
|
+
/**
|
|
86
|
+
* `Retry-After` в секундах, обе формы RFC 9110.
|
|
87
|
+
*
|
|
88
|
+
* ДВЕ ФОРМЫ — НЕ ПЕДАНТИЗМ. Дельту в секундах отдаёт наша мидлвара, а HTTP-дату
|
|
89
|
+
* — чужие прокси (её разрешает та же RFC, и nginx с облачными edge её ставят).
|
|
90
|
+
* Клиент, знающий только цифры, на дате получил бы `NaN` и либо не ждал вовсе,
|
|
91
|
+
* либо ждал бесконечность — обе ветки хуже, чем «подожди свой обычный шаг».
|
|
92
|
+
*
|
|
93
|
+
* `now` — параметр, а не `Date.now()` внутри: разбор даты обязан быть проверяем
|
|
94
|
+
* прогоном, а не зависеть от того, когда его запустили.
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseRetryAfterSeconds(raw: string | null | undefined, now: Date): number | undefined;
|
|
97
|
+
export interface ApiFailureInput {
|
|
98
|
+
status: number;
|
|
99
|
+
/**
|
|
100
|
+
* Сырой текст тела. Именно текст: `res.json()` на HTML бросает парсером.
|
|
101
|
+
*
|
|
102
|
+
* `undefined` — тело ПРОЧИТАТЬ НЕ УДАЛОСЬ (соединение оборвалось после
|
|
103
|
+
* заголовков). Это НЕ то же самое, что пустое или чужое тело, и путать их
|
|
104
|
+
* нельзя: по пустой строке нельзя судить, кто ответил (внешнее ревью, круг 13).
|
|
105
|
+
*/
|
|
106
|
+
bodyText: string | undefined;
|
|
107
|
+
/** Значение заголовка `Retry-After`, как его отдал сервер. */
|
|
108
|
+
retryAfterHeader?: string | null | undefined;
|
|
109
|
+
/** Точка отсчёта для формы HTTP-date. */
|
|
110
|
+
now?: Date;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Статус + тело + заголовок → то, с чем можно ПОСТУПИТЬ.
|
|
114
|
+
*
|
|
115
|
+
* ТЕЛО УТОЧНЯЕТ СТАТУС, НО НЕ ОТМЕНЯЕТ ЕГО. Специализированная схема
|
|
116
|
+
* применяется, только если статус ей СООТВЕТСТВУЕТ: `rate_limited` — на 429,
|
|
117
|
+
* оба денежных кода — на 402.
|
|
118
|
+
*
|
|
119
|
+
* Первая редакция давала телу перевешивать статус, рассуждая так: код объявлен
|
|
120
|
+
* в теле, а статус по дороге переписывает кто угодно, значит 502 поверх нашего
|
|
121
|
+
* 402 — это всё ещё «не хватило кредитов». Внешнее ревью показало обратную
|
|
122
|
+
* сторону того же рассуждения, и она хуже. Кэширующий прокси, отдавший старое
|
|
123
|
+
* тело `debt_outstanding` с новым статусом 429, делал отказ ТЕРМИНАЛЬНЫМ: агент
|
|
124
|
+
* бросал бы уже оплаченный ран и требовал погасить долг, которого нет. Зеркально
|
|
125
|
+
* 402 с телом `rate_limited` становился РЕТРАЙНЫМ, прямо вопреки правилу «любой
|
|
126
|
+
* 402 останавливает». Получались и внутренне противоречивые отказы вида
|
|
127
|
+
* `status === 402 && retryable === true`.
|
|
128
|
+
*
|
|
129
|
+
* Сочетание статуса и кода из РАЗНЫХ ответов — это не сообщение, а его порча, и
|
|
130
|
+
* доверять в нём надо тому, что уцелело наверняка. Уцелел статус: он часть
|
|
131
|
+
* стартовой строки, его не кэшируют отдельно от тела и им управляет тот же
|
|
132
|
+
* узел, который решил ответить. Поэтому при расхождении класс отказа берётся из
|
|
133
|
+
* статуса, а текст — из тела: сказать «сервер перегружен, повтори» и оказаться
|
|
134
|
+
* неправым дешевле, чем бросить оплаченный ран.
|
|
135
|
+
*
|
|
136
|
+
* ЧИСЛА `retry_after_seconds` ИЗ ТЕЛА СИЛЬНЕЕ ЗАГОЛОВКА — здесь тело выигрывает
|
|
137
|
+
* законно, потому что спорят два поля ОДНОГО ответа, а не ответ с самим собой.
|
|
138
|
+
* Наша мидлвара ставит в оба места одно число, и заголовок переписывают чаще.
|
|
139
|
+
*/
|
|
140
|
+
export declare function parseApiFailure(input: ApiFailureInput): ApiFailure;
|
|
141
|
+
/**
|
|
142
|
+
* ОТКАЗ, РАЗВЁРНУТЫЙ ДЛЯ АГЕНТА, — ОДНОЙ ФУНКЦИЕЙ НА ВСЕ ПОВЕРХНОСТИ (US-622).
|
|
143
|
+
*
|
|
144
|
+
* ПОЧЕМУ НЕ «ПРОСТО ТЕКСТ ОШИБКИ». Отказ по деньгам — это не сбой, а РАЗВИЛКА,
|
|
145
|
+
* и агент обязан её пройти, а не ретраить: 402 повторами не лечится, баланс от
|
|
146
|
+
* них не растёт. Значит ответ обязан нести три вещи, которых в строке нет:
|
|
147
|
+
* числа (сколько есть, сколько нужно), императив («останови и скажи человеку»)
|
|
148
|
+
* и признак, что повторять бессмысленно. Форма скопирована с `next_action` из
|
|
149
|
+
* ADR-009 ровно потому, что она уже работает: `make_ugc` и `get_run` говорят с
|
|
150
|
+
* агентом так же, и отказ, говорящий иначе, читался бы как другой продукт.
|
|
151
|
+
*
|
|
152
|
+
* ПОЧЕМУ ЗДЕСЬ, А НЕ В MCP-ФОРМАТТЕРЕ. Поверхностей три, и разъехаться им
|
|
153
|
+
* нельзя: MCP отдаёт этот объект агенту, CLI печатает те же числа человеку, а
|
|
154
|
+
* SDK держит их в типизированной ошибке. Собери форму в MCP — и CLI пришлось бы
|
|
155
|
+
* собирать её ВТОРОЙ раз, то есть завести второй источник правды о том, что
|
|
156
|
+
* значит отказ (правило проекта №2).
|
|
157
|
+
*/
|
|
158
|
+
export interface AgentFailureReport {
|
|
159
|
+
/** Императив: повторять этот же вызов позже или прекратить. */
|
|
160
|
+
status: "RETRY_LATER" | "FAILED";
|
|
161
|
+
/** Машиночитаемая причина. Для чужих тел — `http_<status>`. */
|
|
162
|
+
code: string;
|
|
163
|
+
message: string;
|
|
164
|
+
/** Дубль `status` для условия в коде: строку легко сравнить неверно. */
|
|
165
|
+
retryable: boolean;
|
|
166
|
+
next_action: string;
|
|
167
|
+
retry_after_seconds?: number;
|
|
168
|
+
limit?: number;
|
|
169
|
+
window_seconds?: number;
|
|
170
|
+
balance_credits?: number;
|
|
171
|
+
required_credits?: number;
|
|
172
|
+
debt_credits?: number;
|
|
173
|
+
}
|
|
174
|
+
export declare function agentFailureReport(failure: ApiFailure): AgentFailureReport;
|