@pouchy_ai/admin-sdk 0.8.0 → 0.10.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/CHANGELOG.md CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  All notable changes to `@pouchy_ai/admin-sdk` are documented here.
4
4
 
5
+ ## 0.10.0 — 2026-07-29
6
+
7
+ - **`AdminApiError.code` — the Admin API's machine-readable failure tag.** The
8
+ server has been sending `code` on ten failures across nineteen emit sites;
9
+ this package read `error` off the body and dropped `code` on the floor, so the
10
+ field was unreachable from the first-party client.
11
+ - **Why it matters more than it sounds: every one of those ten codes is a 409.**
12
+ On this plane the status line never disambiguates, and two of the durable-run
13
+ routes answer 409 for two different reasons each, with opposite recoveries —
14
+ `resumeRun` (`stale_token`: re-read and answer again / `run_not_parked`: stop,
15
+ someone already decided) and `signalRun` (`event_mismatch`: your event is early
16
+ or wrong, retry later / `run_not_waiting`: the wait ended, stop). This
17
+ package's own doc comments told callers to make exactly those distinctions,
18
+ which was not possible without string-matching English prose.
19
+ - **`ADMIN_ERROR_CODES` + `AdminErrorCode` are exported** so a `switch` is typed.
20
+ The type carries a `(string & {})` arm (same doctrine as the companion SDK's
21
+ `CompanionErrorCodeValue`), so a code newer than your installed build still
22
+ arrives as a readable string instead of being filtered out. The list is
23
+ drift-gated against the server's own vocabulary in both directions.
24
+ - Additive only — no method signature, request shape, status code or error
25
+ message changed. `code` is `undefined` on every failure the server did not tag
26
+ (400s, 404s, the 429, transport errors), and a non-string or empty `code` on
27
+ the wire leaves it `undefined` rather than stamping a falsy value a `switch`
28
+ would fall through on.
29
+
30
+ ## 0.9.0 — 2026-07-28
31
+
32
+ - **Channel CRUD is typed (CR-77).** `createChannel` takes
33
+ `type: CreatableChannelType` instead of `string` — the full 31-transport
34
+ union, minus `internal-a2a`, which is in the platform's union but has no
35
+ adapter and is answered 400, so excluding it turns a runtime rejection into a
36
+ compile error. `secret` is now `ChannelSecretInput` (`token`,
37
+ `inboundSecret`, `extra`) rather than `Record<string, unknown>`.
38
+ - The union is a duplicate of the server's `TransportType` (this package is
39
+ standalone and cannot import it), so a drift gate binds the two and fails CI
40
+ in BOTH directions — a transport on one side and not the other is either an
41
+ integrator typed out of a working call, or a compile-time promise the API
42
+ rejects.
43
+ - Per-transport `secret.extra` field lists are still documented in
44
+ <https://pouchy.ai/docs/channel-setup> rather than modelled per type: the sets are
45
+ operator-supplied and move with each provider's own console.
46
+
5
47
  ## 0.8.0 — 2026-07-28
6
48
 
7
49
  - **Durable runs.** `listRuns`, `createRun`, `getRun`, `cancelRun`, `resumeRun`,
package/README.md CHANGED
@@ -100,6 +100,62 @@ try {
100
100
  }
101
101
  ```
102
102
 
103
+ ### Failure codes (409)
104
+
105
+ **Every machine-readable failure on this API is a 409**, so `status` separates
106
+ none of them. `AdminApiError.code` is the discriminator — switch on it rather
107
+ than string-matching `message`, which is prose and may be reworded:
108
+
109
+ ```ts
110
+ import { AdminApiError } from '@pouchy_ai/admin-sdk';
111
+
112
+ // `runId` and `token` come from the run you are answering — the
113
+ // `agent.run_awaiting` webhook carries both, or read them off `getRun`.
114
+ declare const runId: string;
115
+ declare const token: string;
116
+
117
+ try {
118
+ await admin.resumeRun(runId, { approved: true, token });
119
+ } catch (e) {
120
+ if (!(e instanceof AdminApiError)) throw e;
121
+ switch (e.code) {
122
+ case 'stale_token': {
123
+ // The run moved on to a different question. Re-read and answer the
124
+ // current one — this is the retryable branch.
125
+ const { run } = await admin.getRun(runId);
126
+ if (run.awaiting) await admin.resumeRun(runId, { approved: true, token: run.awaiting.token });
127
+ break;
128
+ }
129
+ case 'run_not_parked':
130
+ // Someone else decided, or the run was cancelled. Do NOT retry.
131
+ break;
132
+ default:
133
+ throw e;
134
+ }
135
+ }
136
+ ```
137
+
138
+ The vocabulary is exported as `ADMIN_ERROR_CODES` (and the `AdminErrorCode`
139
+ type), and is append-only:
140
+
141
+ | code | route | what to do |
142
+ |---|---|---|
143
+ | `schedule_limit_reached` | `createSchedule` | delete or disable a schedule, retry |
144
+ | `channel_limit_reached` | `createChannel` | delete an unused connector, retry |
145
+ | `webhook_limit_reached` | `createWebhook` | delete an unused endpoint, retry |
146
+ | `run_limit_reached` | `createRun` | wait for a run to finish, or `cancelRun` one |
147
+ | `reembed_required` | knowledge config | clear + re-ingest to switch embedding model |
148
+ | `run_terminal` | `cancelRun` | the run already finished — nothing to cancel |
149
+ | `run_not_parked` | `resumeRun` | already decided; stop retrying |
150
+ | `stale_token` | `resumeRun` | `getRun`, then answer the current `awaiting.token` |
151
+ | `run_not_waiting` | `signalRun` | the wait ended; stop retrying |
152
+ | `event_mismatch` | `signalRun` | the run wants a different key; your event is early or wrong |
153
+
154
+ `code` is `undefined` on every failure the server did not tag (400s, 404s, the
155
+ 429, transport errors), so always keep a `default` branch. The type has a
156
+ `(string & {})` arm on purpose: a code newer than your installed build still
157
+ arrives as a readable string. Available from **0.10.0**.
158
+
103
159
  ### Throttling (429)
104
160
 
105
161
  Writes are rate-limited per IP — 120 non-`GET` requests per minute across the
@@ -142,6 +198,11 @@ Reads (`GET`) are not covered by that bucket. `retryAfter` is available from
142
198
  | Channels | `listChannels` · `createChannel` · `getChannel` · `updateChannel` · `deleteChannel` |
143
199
  | Schedules | `listSchedules` · `createSchedule` · `getSchedule` · `updateSchedule` · `deleteSchedule` |
144
200
  | Durable runs | `listRuns` · `createRun` · `getRun` · `cancelRun` · `resumeRun` · `signalRun` |
201
+
202
+ Channel types are checked at compile time: `createChannel` takes a
203
+ `CreatableChannelType` (the platform's 31-transport union minus the adapterless
204
+ `internal-a2a`), and `secret` is a named `ChannelSecretInput`. Per-transport
205
+ `secret.extra` fields are listed in <https://pouchy.ai/docs/channel-setup>.
145
206
  | Webhooks | `listWebhooks` · `createWebhook` · `updateWebhook` · `rotateWebhookSecret` · `deleteWebhook` · `testWebhook` · `redeliverWebhook` |
146
207
  | Reporting | `getUsage` · `getBilling` · `getTracesSummary` · `getRecentTraces` · `getLogs` · `getProject` · `updateProject` |
147
208
  | Escape hatch | `request(method, path, body?)` — any endpoint not yet typed |
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const ADMIN_SDK_VERSION = "0.8.0";
1
+ export declare const ADMIN_SDK_VERSION = "0.10.0";
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
@@ -50,9 +50,33 @@ export interface AdminClientOptions {
50
50
  * failed with the self-contradicting `request timed out after 0ms`. */
51
51
  timeoutMs?: number;
52
52
  }
53
+ /** Machine-readable failure tags the Admin API puts on an error body, mirroring
54
+ * the server's own list (`src/lib/server/platform/api-error-codes.ts`) and
55
+ * drift-tested against it. APPEND-ONLY: these are documented as switchable, so
56
+ * renaming or removing one breaks integrators.
57
+ *
58
+ * Every member is a **409**. That is the whole reason this vocabulary is
59
+ * published: on this plane the status line never disambiguates, so two of the
60
+ * durable-run routes answer 409 for two entirely different reasons each, with
61
+ * different recoveries. `code` is the only thing that tells them apart. */
62
+ export declare const ADMIN_ERROR_CODES: readonly ["schedule_limit_reached", "channel_limit_reached", "webhook_limit_reached", "run_limit_reached", "reembed_required", "run_terminal", "run_not_parked", "stale_token", "run_not_waiting", "event_mismatch"];
63
+ /** `AdminApiError.code` values. The `(string & {})` arm keeps the type open for
64
+ * codes newer than this SDK build while preserving autocomplete — same
65
+ * doctrine as the companion SDK's `CompanionErrorCodeValue`. */
66
+ export type AdminErrorCode = (typeof ADMIN_ERROR_CODES)[number] | (string & {});
53
67
  /** Thrown on any non-2xx response. `status` is the HTTP status; `message` is the
54
68
  * server's `error` string when present; `retryAfter` is the throttle's own
55
- * backoff in SECONDS on a 429 (undefined on every other failure).
69
+ * backoff in SECONDS on a 429 (undefined on every other failure); `code` is the
70
+ * server's machine tag when it named one (undefined otherwise).
71
+ *
72
+ * Why `code` exists: the Admin API's ten machine-readable failures are ALL on
73
+ * 409, so `status` cannot separate them and this package used to discard the
74
+ * one field that can. Two routes are the point — `resumeRun` answers 409 for
75
+ * `run_not_parked` (someone already decided; re-read, do not retry) or
76
+ * `stale_token` (your token aged out; re-read for the current one and answer
77
+ * again), and `signalRun` answers 409 for `run_not_waiting` or
78
+ * `event_mismatch`. This package's own docs told callers to distinguish those,
79
+ * which was not possible without switching on prose.
56
80
  *
57
81
  * Why `retryAfter` exists: the entire write surface of this SDK sits behind one
58
82
  * per-IP throttle — `hooks.server.ts` runs a `v1-admin-write` bucket (120
@@ -69,7 +93,8 @@ export interface AdminClientOptions {
69
93
  export declare class AdminApiError extends Error {
70
94
  status: number;
71
95
  readonly retryAfter?: number;
72
- constructor(message: string, status: number, retryAfter?: number);
96
+ readonly code?: AdminErrorCode;
97
+ constructor(message: string, status: number, retryAfter?: number, code?: string);
73
98
  }
74
99
  export type Env = 'live' | 'test';
75
100
  export type AgentStatus = 'draft' | 'published';
@@ -166,6 +191,31 @@ export interface DeliveryOutcome {
166
191
  }
167
192
  /** The Admin API client. Every method returns the parsed JSON body; failures
168
193
  * throw AdminApiError. */
194
+ /** Every transport the platform knows. Mirrors the server's `TransportType`
195
+ * (`channels/types.ts`) — this package is standalone and cannot import it, so
196
+ * `admin-sdk-channel-types.drift.test.ts` binds the two and fails CI if either
197
+ * side gains a transport the other lacks. */
198
+ export type ChannelTransportType = 'echo' | 'matrix' | 'telegram' | 'web' | 'activitypub' | 'internal-a2a' | 'discord' | 'slack' | 'nostr' | 'atproto' | 'sms' | 'email' | 'whatsapp' | 'messenger' | 'instagram' | 'line' | 'teams' | 'wechat' | 'wecom' | 'feishu' | 'dingtalk' | 'twitter' | 'xmpp' | 'qq' | 'kakao' | 'viber' | 'signal' | 'weibo' | 'alexa' | 'google-assistant' | 'voice';
199
+ /** Transports `createChannel` accepts. `internal-a2a` is in the union but has
200
+ * no adapter, so the server answers 400 — excluding it here turns a runtime
201
+ * rejection into a compile error. */
202
+ export type CreatableChannelType = Exclude<ChannelTransportType, 'internal-a2a'>;
203
+ /** Credentials for a connector, sealed server-side as one AES-GCM blob.
204
+ *
205
+ * `extra` is where the transports needing more than a token live — Twitter
206
+ * OAuth 1.0a (`consumerKey`/`consumerSecret`/`accessToken`/`accessTokenSecret`),
207
+ * Teams AAD (`appId`/`appPassword`), QQ (`appId` + Ed25519 seed), the WeChat
208
+ * family's mint credentials. Per-transport field lists are documented in
209
+ * https://pouchy.ai/docs/channel-setup; they are not modelled per-type here because the set
210
+ * is operator-supplied and moves with each provider's own console. */
211
+ export interface ChannelSecretInput {
212
+ /** Bot access token / API key used for outbound delivery. */
213
+ token?: string;
214
+ /** Inbound signing secret (e.g. Telegram `secret_token`, Matrix `hs_token`). */
215
+ inboundSecret?: string;
216
+ /** Extra named credentials for transports whose auth needs more than the pair above. */
217
+ extra?: Record<string, string>;
218
+ }
169
219
  /** A durable run as the API returns it. Typed rather than `unknown` because
170
220
  * the parked-run flow — read `status`, read `awaiting.token`, answer — is the
171
221
  * SDK's most important interaction, and forcing a cast there would put the
@@ -511,13 +561,13 @@ export interface AdminClient {
511
561
  connectors: unknown[];
512
562
  }>;
513
563
  createChannel(input: {
514
- type: string;
564
+ type: CreatableChannelType;
515
565
  agentId: string;
516
566
  env?: Env;
517
567
  config?: Record<string, unknown>;
518
568
  /** Connector credentials (bot token, signing secret, …) — stored
519
569
  * encrypted, never returned. */
520
- secret?: Record<string, unknown>;
570
+ secret?: ChannelSecretInput;
521
571
  }): Promise<{
522
572
  connector: {
523
573
  id: string;
@@ -603,7 +653,13 @@ export interface AdminClient {
603
653
  * `awaiting.token` (also carried by the `agent.run_awaiting` webhook) and
604
654
  * fences the decision against a stale or replayed answer. Without this call
605
655
  * a parked run never resumes: it holds no lease and has left the platform's
606
- * due window by design. */
656
+ * due window by design.
657
+ *
658
+ * Both failures here are **409**; switch on `AdminApiError.code` to tell
659
+ * them apart. `stale_token` — the run has moved on to a different question:
660
+ * re-read it with `getRun` and answer the current `awaiting.token`.
661
+ * `run_not_parked` — it is no longer awaiting anyone (someone else decided,
662
+ * or it was cancelled): do NOT retry. */
607
663
  resumeRun(runId: string, input: {
608
664
  approved: boolean;
609
665
  token: string;
@@ -615,7 +671,13 @@ export interface AdminClient {
615
671
  }>;
616
672
  /** Deliver an external event to a run parked on one. `event` must equal the
617
673
  * key the run is waiting for — a mismatch answers 409 `event_mismatch`, so a
618
- * webhook retry can tell "too late" from "rejected". */
674
+ * webhook retry can tell "too late" from "rejected".
675
+ *
676
+ * Read that verdict off `AdminApiError.code` (0.10.0; before that the field
677
+ * was dropped and this advice was unfollowable). `event_mismatch` — the run
678
+ * wants a DIFFERENT key, so your event is early or wrong: safe to retry
679
+ * later. `run_not_waiting` — the run is not parked on an event at all, so
680
+ * the wait already ended: stop retrying. */
619
681
  signalRun(runId: string, input: {
620
682
  event: string;
621
683
  payload?: unknown;
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.8.0';
11
+ export const ADMIN_SDK_VERSION = '0.10.0';
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;
@@ -55,9 +55,40 @@ export function requestDeadlineMs(method, path) {
55
55
  ? LONG_WORK_TIMEOUT_MS
56
56
  : DEFAULT_TIMEOUT_MS;
57
57
  }
58
+ /** Machine-readable failure tags the Admin API puts on an error body, mirroring
59
+ * the server's own list (`src/lib/server/platform/api-error-codes.ts`) and
60
+ * drift-tested against it. APPEND-ONLY: these are documented as switchable, so
61
+ * renaming or removing one breaks integrators.
62
+ *
63
+ * Every member is a **409**. That is the whole reason this vocabulary is
64
+ * published: on this plane the status line never disambiguates, so two of the
65
+ * durable-run routes answer 409 for two entirely different reasons each, with
66
+ * different recoveries. `code` is the only thing that tells them apart. */
67
+ export const ADMIN_ERROR_CODES = [
68
+ 'schedule_limit_reached',
69
+ 'channel_limit_reached',
70
+ 'webhook_limit_reached',
71
+ 'run_limit_reached',
72
+ 'reembed_required',
73
+ 'run_terminal',
74
+ 'run_not_parked',
75
+ 'stale_token',
76
+ 'run_not_waiting',
77
+ 'event_mismatch'
78
+ ];
58
79
  /** Thrown on any non-2xx response. `status` is the HTTP status; `message` is the
59
80
  * server's `error` string when present; `retryAfter` is the throttle's own
60
- * backoff in SECONDS on a 429 (undefined on every other failure).
81
+ * backoff in SECONDS on a 429 (undefined on every other failure); `code` is the
82
+ * server's machine tag when it named one (undefined otherwise).
83
+ *
84
+ * Why `code` exists: the Admin API's ten machine-readable failures are ALL on
85
+ * 409, so `status` cannot separate them and this package used to discard the
86
+ * one field that can. Two routes are the point — `resumeRun` answers 409 for
87
+ * `run_not_parked` (someone already decided; re-read, do not retry) or
88
+ * `stale_token` (your token aged out; re-read for the current one and answer
89
+ * again), and `signalRun` answers 409 for `run_not_waiting` or
90
+ * `event_mismatch`. This package's own docs told callers to distinguish those,
91
+ * which was not possible without switching on prose.
61
92
  *
62
93
  * Why `retryAfter` exists: the entire write surface of this SDK sits behind one
63
94
  * per-IP throttle — `hooks.server.ts` runs a `v1-admin-write` bucket (120
@@ -74,14 +105,27 @@ export function requestDeadlineMs(method, path) {
74
105
  export class AdminApiError extends Error {
75
106
  status;
76
107
  retryAfter;
77
- constructor(message, status, retryAfter) {
108
+ code;
109
+ constructor(message, status, retryAfter, code) {
78
110
  super(message);
79
111
  this.name = 'AdminApiError';
80
112
  this.status = status;
81
113
  if (retryAfter !== undefined)
82
114
  this.retryAfter = retryAfter;
115
+ // Only a non-empty string. A body carrying `code: null` or `code: 42`
116
+ // must leave the property ABSENT rather than stamping a falsy value that
117
+ // a `switch (e.code)` would fall through on in a way the caller can't see.
118
+ if (typeof code === 'string' && code)
119
+ this.code = code;
83
120
  }
84
121
  }
122
+ /** The `code` a non-2xx body named, or undefined. Kept beside `retryAfterFrom`
123
+ * and equally defensive: the body is parsed with a `.catch(() => ({}))`, so
124
+ * every field here is untrusted shape, not a typed response. */
125
+ function codeFrom(body) {
126
+ const v = body?.code;
127
+ return typeof v === 'string' && v ? v : undefined;
128
+ }
85
129
  /** Seconds to wait before retrying a throttled response, or undefined when the
86
130
  * server named neither a body field nor a header.
87
131
  *
@@ -165,7 +209,7 @@ export function createAdminClient(opts) {
165
209
  }
166
210
  const data = (await res.json().catch(() => ({})));
167
211
  if (!res.ok)
168
- throw new AdminApiError(data?.error ?? `HTTP ${res.status}`, res.status, retryAfterFrom(res, data));
212
+ throw new AdminApiError(data?.error ?? `HTTP ${res.status}`, res.status, retryAfterFrom(res, data), codeFrom(data));
169
213
  return data;
170
214
  }
171
215
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/admin-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
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",