@pouchy_ai/admin-sdk 0.6.1 → 0.7.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/CHANGELOG.md +59 -0
- package/README.md +32 -0
- package/dist/index.d.ts +25 -4
- package/dist/index.js +63 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@pouchy_ai/admin-sdk` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.7.1 — 2026-07-27
|
|
6
|
+
|
|
7
|
+
Fix: `timeoutMs: 0` now disables the deadline instead of aborting every request.
|
|
8
|
+
|
|
9
|
+
- **`timeoutMs: 0` means "no deadline".** It previously meant "abort on the next
|
|
10
|
+
tick": `opts.timeoutMs ?? …` correctly treats `0` as a set value, but that `0`
|
|
11
|
+
was then handed to `AbortSignal.timeout(0)`, which fires immediately — so a
|
|
12
|
+
client constructed with `timeoutMs: 0` failed *every* call with the
|
|
13
|
+
self-contradicting `request timed out after 0ms`. The companion JS SDK has
|
|
14
|
+
documented `requestTimeoutMs: 0` as "disables" since 0.35.0 and implements it
|
|
15
|
+
as an explicit `if (!budget)` branch; a host carrying that idiom to this
|
|
16
|
+
package got the opposite behaviour. The signal is now omitted entirely when
|
|
17
|
+
the resolved deadline is `0`.
|
|
18
|
+
- No other value changes. An unset `timeoutMs` still resolves to the
|
|
19
|
+
route-sized default (30s, or `LONG_WORK_TIMEOUT_MS` for the four
|
|
20
|
+
`maxDuration: 300` handlers), and any positive value still bounds every
|
|
21
|
+
request as before.
|
|
22
|
+
|
|
23
|
+
## 0.7.0 — 2026-07-27
|
|
24
|
+
|
|
25
|
+
Adds: a throttled request now tells you how long to wait.
|
|
26
|
+
|
|
27
|
+
- **`AdminApiError.retryAfter`** — seconds, present on a 429 and `undefined` on
|
|
28
|
+
every other failure. Every write this client makes (`POST` / `PATCH` /
|
|
29
|
+
`DELETE`) passes through one per-IP throttle on the server — a
|
|
30
|
+
`v1-admin-write` bucket of 120 non-GET requests per minute across all of
|
|
31
|
+
`/v1/*` — and the natural migration loop (`updateAgent` per agent,
|
|
32
|
+
`setUserSuspended` per user, `setSkillRate` per skill) reaches 120 in seconds.
|
|
33
|
+
The server has always answered with both a `Retry-After` header and a body
|
|
34
|
+
`retryAfter`; this package discarded both, so the one caller who provably
|
|
35
|
+
needs a backoff number had only the prose message to guess from — and a
|
|
36
|
+
guessed-too-short retry keeps the sliding window saturated, extending the
|
|
37
|
+
throttle it is trying to escape.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
try {
|
|
41
|
+
await admin.updateAgent(id, patch);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e instanceof AdminApiError && e.status === 429) {
|
|
44
|
+
await new Promise((r) => setTimeout(r, (e.retryAfter ?? 5) * 1000));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Resolution order matches the companion JS SDK's `retryAfterFrom`: body
|
|
50
|
+
`retryAfterSec`, then body `retryAfter` (the key the `/v1` throttle actually
|
|
51
|
+
emits), then the `Retry-After` header as seconds, then its RFC 9110 HTTP-date
|
|
52
|
+
form converted to a non-negative delta. A response that names none leaves
|
|
53
|
+
`retryAfter` **undefined** — never `0`, which would tell backoff code to retry
|
|
54
|
+
immediately.
|
|
55
|
+
|
|
56
|
+
- **Back-compatible.** New optional third constructor argument and a new
|
|
57
|
+
optional readonly property; `status`, `message` and the `instanceof
|
|
58
|
+
AdminApiError` check are untouched. Minor bump because the public error type
|
|
59
|
+
gained a member.
|
|
60
|
+
|
|
61
|
+
Both sibling SDKs already surfaced this value (JS `CompanionError.retryAfter`,
|
|
62
|
+
python `CompanionError.retry_after`) — same name, same units, same precedence.
|
|
63
|
+
|
|
5
64
|
## 0.6.1 — 2026-07-27
|
|
6
65
|
|
|
7
66
|
Fix: the four long-running requests are no longer cut at 30s while the server is
|
package/README.md
CHANGED
|
@@ -80,6 +80,11 @@ Setting `timeoutMs` explicitly always wins and applies to **every** request, lon
|
|
|
80
80
|
or short. `requestDeadlineMs(method, path)` returns the default a given request
|
|
81
81
|
would use.
|
|
82
82
|
|
|
83
|
+
`timeoutMs: 0` **disables** the deadline — the request runs unbounded. This is
|
|
84
|
+
the same contract as the companion JS SDK's `requestTimeoutMs: 0`, so the idiom
|
|
85
|
+
carries between the two packages. (Before 0.7.1 a `0` aborted every request on
|
|
86
|
+
the next tick.)
|
|
87
|
+
|
|
83
88
|
## Errors
|
|
84
89
|
|
|
85
90
|
Every method throws `AdminApiError` on failure — a non-2xx response, a network
|
|
@@ -95,6 +100,33 @@ try {
|
|
|
95
100
|
}
|
|
96
101
|
```
|
|
97
102
|
|
|
103
|
+
### Throttling (429)
|
|
104
|
+
|
|
105
|
+
Writes are rate-limited per IP — 120 non-`GET` requests per minute across the
|
|
106
|
+
whole `/v1` plane — so a migration loop over agents, users or skills will hit it.
|
|
107
|
+
On a 429, `AdminApiError.retryAfter` carries the server's backoff in **seconds**
|
|
108
|
+
(from the body's `retryAfter` / `retryAfterSec`, or the `Retry-After` header).
|
|
109
|
+
It is `undefined` on every other failure, and `undefined` — never `0` — when the
|
|
110
|
+
server named no delay, so `??` your own floor rather than retrying immediately:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
async function withBackoff<T>(call: () => Promise<T>, tries = 5): Promise<T> {
|
|
114
|
+
for (let i = 0; ; i++) {
|
|
115
|
+
try {
|
|
116
|
+
return await call();
|
|
117
|
+
} catch (e) {
|
|
118
|
+
if (!(e instanceof AdminApiError) || e.status !== 429 || i >= tries) throw e;
|
|
119
|
+
await new Promise((r) => setTimeout(r, (e.retryAfter ?? 5) * 1000));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await withBackoff(() => admin.updateAgent(id, { status: 'published' }));
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Reads (`GET`) are not covered by that bucket. `retryAfter` is available from
|
|
128
|
+
0.7.0.
|
|
129
|
+
|
|
98
130
|
## Surface
|
|
99
131
|
|
|
100
132
|
| Area | Methods |
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const ADMIN_SDK_VERSION = "0.
|
|
1
|
+
export declare const ADMIN_SDK_VERSION = "0.7.1";
|
|
2
2
|
export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1/admin";
|
|
3
3
|
/** Deadline for the routes whose server handler declares `maxDuration: 300` —
|
|
4
4
|
* the server's own ceiling plus headroom, so a client abort can only ever mean
|
|
@@ -41,14 +41,35 @@ export interface AdminClientOptions {
|
|
|
41
41
|
* and the GDPR user delete), which default to
|
|
42
42
|
* {@link LONG_WORK_TIMEOUT_MS}. Setting this explicitly always wins and
|
|
43
43
|
* applies to every request, long or short. A request that outlives its
|
|
44
|
-
* deadline rejects with an AdminApiError(status 0).
|
|
44
|
+
* deadline rejects with an AdminApiError(status 0).
|
|
45
|
+
*
|
|
46
|
+
* `0` DISABLES the deadline — the request runs unbounded. This mirrors the
|
|
47
|
+
* companion JS SDK's `requestTimeoutMs`, whose documented contract is the
|
|
48
|
+
* same ("`0` disables"), so the idiom carries between the two packages.
|
|
49
|
+
* Before 0.7.1 a `0` here meant "abort on the next tick", i.e. every call
|
|
50
|
+
* failed with the self-contradicting `request timed out after 0ms`. */
|
|
45
51
|
timeoutMs?: number;
|
|
46
52
|
}
|
|
47
53
|
/** Thrown on any non-2xx response. `status` is the HTTP status; `message` is the
|
|
48
|
-
* server's `error` string when present
|
|
54
|
+
* server's `error` string when present; `retryAfter` is the throttle's own
|
|
55
|
+
* backoff in SECONDS on a 429 (undefined on every other failure).
|
|
56
|
+
*
|
|
57
|
+
* Why `retryAfter` exists: the entire write surface of this SDK sits behind one
|
|
58
|
+
* per-IP throttle — `hooks.server.ts` runs a `v1-admin-write` bucket (120
|
|
59
|
+
* non-GET requests / minute) over every `/v1/*` path, and `/v1/admin` is where
|
|
60
|
+
* this client lives. A migration loop (`updateAgent` per agent,
|
|
61
|
+
* `setUserSuspended` per user, `setSkillRate` per skill) reaches 120 in
|
|
62
|
+
* seconds. The server answers with BOTH a `Retry-After` header and a body
|
|
63
|
+
* `retryAfter`, and this package used to discard both — leaving the one caller
|
|
64
|
+
* who provably needs a backoff number with nothing but a prose message to
|
|
65
|
+
* guess from, and a guessed-too-short retry keeps the sliding window
|
|
66
|
+
* saturated. Both sibling SDKs already surface it (JS
|
|
67
|
+
* `CompanionError.retryAfter`, python `CompanionError.retry_after`); this is
|
|
68
|
+
* the same value under the same name. */
|
|
49
69
|
export declare class AdminApiError extends Error {
|
|
50
70
|
status: number;
|
|
51
|
-
|
|
71
|
+
readonly retryAfter?: number;
|
|
72
|
+
constructor(message: string, status: number, retryAfter?: number);
|
|
52
73
|
}
|
|
53
74
|
export type Env = 'live' | 'test';
|
|
54
75
|
export type AgentStatus = 'draft' | 'published';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// import { createAdminClient } from '@pouchy_ai/admin-sdk';
|
|
9
9
|
// const admin = createAdminClient({ adminKey: process.env.POUCHY_ADMIN_KEY! });
|
|
10
10
|
// const { agents } = await admin.listAgents();
|
|
11
|
-
export const ADMIN_SDK_VERSION = '0.
|
|
11
|
+
export const ADMIN_SDK_VERSION = '0.7.1';
|
|
12
12
|
export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1/admin';
|
|
13
13
|
/** Default per-request timeout (ms). A hung upstream otherwise never rejects. */
|
|
14
14
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
@@ -56,14 +56,65 @@ export function requestDeadlineMs(method, path) {
|
|
|
56
56
|
: DEFAULT_TIMEOUT_MS;
|
|
57
57
|
}
|
|
58
58
|
/** Thrown on any non-2xx response. `status` is the HTTP status; `message` is the
|
|
59
|
-
* server's `error` string when present
|
|
59
|
+
* server's `error` string when present; `retryAfter` is the throttle's own
|
|
60
|
+
* backoff in SECONDS on a 429 (undefined on every other failure).
|
|
61
|
+
*
|
|
62
|
+
* Why `retryAfter` exists: the entire write surface of this SDK sits behind one
|
|
63
|
+
* per-IP throttle — `hooks.server.ts` runs a `v1-admin-write` bucket (120
|
|
64
|
+
* non-GET requests / minute) over every `/v1/*` path, and `/v1/admin` is where
|
|
65
|
+
* this client lives. A migration loop (`updateAgent` per agent,
|
|
66
|
+
* `setUserSuspended` per user, `setSkillRate` per skill) reaches 120 in
|
|
67
|
+
* seconds. The server answers with BOTH a `Retry-After` header and a body
|
|
68
|
+
* `retryAfter`, and this package used to discard both — leaving the one caller
|
|
69
|
+
* who provably needs a backoff number with nothing but a prose message to
|
|
70
|
+
* guess from, and a guessed-too-short retry keeps the sliding window
|
|
71
|
+
* saturated. Both sibling SDKs already surface it (JS
|
|
72
|
+
* `CompanionError.retryAfter`, python `CompanionError.retry_after`); this is
|
|
73
|
+
* the same value under the same name. */
|
|
60
74
|
export class AdminApiError extends Error {
|
|
61
75
|
status;
|
|
62
|
-
|
|
76
|
+
retryAfter;
|
|
77
|
+
constructor(message, status, retryAfter) {
|
|
63
78
|
super(message);
|
|
64
79
|
this.name = 'AdminApiError';
|
|
65
80
|
this.status = status;
|
|
81
|
+
if (retryAfter !== undefined)
|
|
82
|
+
this.retryAfter = retryAfter;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Seconds to wait before retrying a throttled response, or undefined when the
|
|
86
|
+
* server named neither a body field nor a header.
|
|
87
|
+
*
|
|
88
|
+
* Precedence mirrors the JS companion SDK's `retryAfterFrom`, plus the one key
|
|
89
|
+
* this plane actually emits: the `/v1` admin-write 429 body says `retryAfter`,
|
|
90
|
+
* while the `/api/companion` 429 body says `retryAfterSec`. Accept both so the
|
|
91
|
+
* same helper is correct if a route is ever aligned on the other name.
|
|
92
|
+
*
|
|
93
|
+
* A missing/blank header must stay undefined — `Number(null)` is 0, which
|
|
94
|
+
* would stamp `retryAfter: 0` on EVERY error without the header (a 404, a
|
|
95
|
+
* 400 …) and tell backoff code "retry immediately". */
|
|
96
|
+
function retryAfterFrom(res, body) {
|
|
97
|
+
for (const k of ['retryAfterSec', 'retryAfter']) {
|
|
98
|
+
const v = body?.[k];
|
|
99
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
100
|
+
return v;
|
|
66
101
|
}
|
|
102
|
+
// Optional-chained because `opts.fetch` is a documented injection point and
|
|
103
|
+
// hand-rolled test doubles routinely return a bare `{ ok, status, json }`
|
|
104
|
+
// with no `headers` — reading a header must never turn their 4xx fixture
|
|
105
|
+
// into a TypeError.
|
|
106
|
+
const raw = res.headers?.get?.('Retry-After');
|
|
107
|
+
if (raw === null || raw === undefined || raw.trim() === '')
|
|
108
|
+
return undefined;
|
|
109
|
+
const header = Number(raw);
|
|
110
|
+
if (Number.isFinite(header) && header >= 0)
|
|
111
|
+
return header;
|
|
112
|
+
// RFC 9110 also allows an HTTP-date (proxies/CDNs emit it) — convert to a
|
|
113
|
+
// non-negative seconds delta. Unparseable stays undefined.
|
|
114
|
+
const at = Date.parse(raw);
|
|
115
|
+
if (!Number.isNaN(at))
|
|
116
|
+
return Math.max(0, Math.ceil((at - Date.now()) / 1000));
|
|
117
|
+
return undefined;
|
|
67
118
|
}
|
|
68
119
|
function qs(params) {
|
|
69
120
|
const u = new URLSearchParams();
|
|
@@ -84,6 +135,8 @@ export function createAdminClient(opts) {
|
|
|
84
135
|
// An explicit host `timeoutMs` always wins (JS SDK contract); otherwise
|
|
85
136
|
// the deadline is sized to the ROUTE, so the four handlers that declare
|
|
86
137
|
// `maxDuration: 300` are not cut at 30s while they are still working.
|
|
138
|
+
// `??` (not `||`) is deliberate: 0 is a SET value meaning "no deadline",
|
|
139
|
+
// and it must not fall through to the route default.
|
|
87
140
|
const timeoutMs = opts.timeoutMs ?? requestDeadlineMs(method, path);
|
|
88
141
|
// Every failure surfaces as an AdminApiError (the doc contract): a
|
|
89
142
|
// network/DNS error or a timeout would otherwise escape as a raw
|
|
@@ -98,7 +151,12 @@ export function createAdminClient(opts) {
|
|
|
98
151
|
...(body !== undefined ? { 'content-type': 'application/json' } : {})
|
|
99
152
|
},
|
|
100
153
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
101
|
-
signal
|
|
154
|
+
// timeoutMs === 0 → no signal at all. `AbortSignal.timeout(0)`
|
|
155
|
+
// does NOT mean "no deadline"; it fires on the next tick, so
|
|
156
|
+
// arming it here aborted every request before the response could
|
|
157
|
+
// land. The companion JS SDK spells the same branch
|
|
158
|
+
// (`if (!budget) return this.doFetch(...)`).
|
|
159
|
+
signal: timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined
|
|
102
160
|
});
|
|
103
161
|
}
|
|
104
162
|
catch (e) {
|
|
@@ -107,7 +165,7 @@ export function createAdminClient(opts) {
|
|
|
107
165
|
}
|
|
108
166
|
const data = (await res.json().catch(() => ({})));
|
|
109
167
|
if (!res.ok)
|
|
110
|
-
throw new AdminApiError(data?.error ?? `HTTP ${res.status}`, res.status);
|
|
168
|
+
throw new AdminApiError(data?.error ?? `HTTP ${res.status}`, res.status, retryAfterFrom(res, data));
|
|
111
169
|
return data;
|
|
112
170
|
}
|
|
113
171
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/admin-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Typed TypeScript client for the Pouchy Admin API \u2014 manage agents, keys, end users, knowledge, skills, channels, schedules, webhooks and credentials headlessly, with a project Admin key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|