@cpajs/cpa 0.0.0-stage → 0.0.1-alpha-2

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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/dist/client/errors.d.ts +70 -0
  4. package/dist/client/errors.d.ts.map +1 -0
  5. package/dist/client/errors.js +91 -0
  6. package/dist/client/errors.js.map +1 -0
  7. package/dist/client/index.d.ts +46 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +65 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/resources/config.d.ts +61 -0
  12. package/dist/client/resources/config.d.ts.map +1 -0
  13. package/dist/client/resources/config.js +80 -0
  14. package/dist/client/resources/config.js.map +1 -0
  15. package/dist/client/resources/credentials.d.ts +64 -0
  16. package/dist/client/resources/credentials.d.ts.map +1 -0
  17. package/dist/client/resources/credentials.js +94 -0
  18. package/dist/client/resources/credentials.js.map +1 -0
  19. package/dist/client/resources/download.d.ts +8 -0
  20. package/dist/client/resources/download.d.ts.map +1 -0
  21. package/dist/client/resources/download.js +17 -0
  22. package/dist/client/resources/download.js.map +1 -0
  23. package/dist/client/resources/logs.d.ts +23 -0
  24. package/dist/client/resources/logs.d.ts.map +1 -0
  25. package/dist/client/resources/logs.js +37 -0
  26. package/dist/client/resources/logs.js.map +1 -0
  27. package/dist/client/resources/oauth.d.ts +43 -0
  28. package/dist/client/resources/oauth.d.ts.map +1 -0
  29. package/dist/client/resources/oauth.js +70 -0
  30. package/dist/client/resources/oauth.js.map +1 -0
  31. package/dist/client/resources/plugins.d.ts +25 -0
  32. package/dist/client/resources/plugins.d.ts.map +1 -0
  33. package/dist/client/resources/plugins.js +47 -0
  34. package/dist/client/resources/plugins.js.map +1 -0
  35. package/dist/client/resources/requests.d.ts +14 -0
  36. package/dist/client/resources/requests.d.ts.map +1 -0
  37. package/dist/client/resources/requests.js +16 -0
  38. package/dist/client/resources/requests.js.map +1 -0
  39. package/dist/client/resources/routing.d.ts +12 -0
  40. package/dist/client/resources/routing.d.ts.map +1 -0
  41. package/dist/client/resources/routing.js +20 -0
  42. package/dist/client/resources/routing.js.map +1 -0
  43. package/dist/client/resources/server.d.ts +17 -0
  44. package/dist/client/resources/server.d.ts.map +1 -0
  45. package/dist/client/resources/server.js +49 -0
  46. package/dist/client/resources/server.js.map +1 -0
  47. package/dist/client/resources/usage.d.ts +16 -0
  48. package/dist/client/resources/usage.d.ts.map +1 -0
  49. package/dist/client/resources/usage.js +20 -0
  50. package/dist/client/resources/usage.js.map +1 -0
  51. package/dist/client/transport.d.ts +60 -0
  52. package/dist/client/transport.d.ts.map +1 -0
  53. package/dist/client/transport.js +170 -0
  54. package/dist/client/transport.js.map +1 -0
  55. package/dist/client/types/common.d.ts +38 -0
  56. package/dist/client/types/common.d.ts.map +1 -0
  57. package/dist/client/types/common.js +4 -0
  58. package/dist/client/types/common.js.map +1 -0
  59. package/dist/client/types/config.d.ts +407 -0
  60. package/dist/client/types/config.d.ts.map +1 -0
  61. package/dist/client/types/config.js +9 -0
  62. package/dist/client/types/config.js.map +1 -0
  63. package/dist/client/types/credentials.d.ts +240 -0
  64. package/dist/client/types/credentials.d.ts.map +1 -0
  65. package/dist/client/types/credentials.js +4 -0
  66. package/dist/client/types/credentials.js.map +1 -0
  67. package/dist/client/types/index.d.ts +10 -0
  68. package/dist/client/types/index.d.ts.map +1 -0
  69. package/dist/client/types/index.js +2 -0
  70. package/dist/client/types/index.js.map +1 -0
  71. package/dist/client/types/oauth.d.ts +89 -0
  72. package/dist/client/types/oauth.d.ts.map +1 -0
  73. package/dist/client/types/oauth.js +4 -0
  74. package/dist/client/types/oauth.js.map +1 -0
  75. package/dist/client/types/observability.d.ts +111 -0
  76. package/dist/client/types/observability.d.ts.map +1 -0
  77. package/dist/client/types/observability.js +4 -0
  78. package/dist/client/types/observability.js.map +1 -0
  79. package/dist/client/types/plugins.d.ts +171 -0
  80. package/dist/client/types/plugins.d.ts.map +1 -0
  81. package/dist/client/types/plugins.js +4 -0
  82. package/dist/client/types/plugins.js.map +1 -0
  83. package/dist/client/types/requests.d.ts +16 -0
  84. package/dist/client/types/requests.d.ts.map +1 -0
  85. package/dist/client/types/requests.js +4 -0
  86. package/dist/client/types/requests.js.map +1 -0
  87. package/dist/client/types/routing.d.ts +50 -0
  88. package/dist/client/types/routing.d.ts.map +1 -0
  89. package/dist/client/types/routing.js +4 -0
  90. package/dist/client/types/routing.js.map +1 -0
  91. package/dist/client/types/server.d.ts +15 -0
  92. package/dist/client/types/server.d.ts.map +1 -0
  93. package/dist/client/types/server.js +4 -0
  94. package/dist/client/types/server.js.map +1 -0
  95. package/dist/index.d.ts +3 -0
  96. package/dist/index.d.ts.map +1 -0
  97. package/dist/index.js +3 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/server/binary.d.ts +35 -0
  100. package/dist/server/binary.d.ts.map +1 -0
  101. package/dist/server/binary.js +108 -0
  102. package/dist/server/binary.js.map +1 -0
  103. package/dist/server/config.d.ts +37 -0
  104. package/dist/server/config.d.ts.map +1 -0
  105. package/dist/server/config.js +103 -0
  106. package/dist/server/config.js.map +1 -0
  107. package/dist/server/cpa-version.d.ts +12 -0
  108. package/dist/server/cpa-version.d.ts.map +1 -0
  109. package/dist/server/cpa-version.js +12 -0
  110. package/dist/server/cpa-version.js.map +1 -0
  111. package/dist/server/errors.d.ts +36 -0
  112. package/dist/server/errors.d.ts.map +1 -0
  113. package/dist/server/errors.js +26 -0
  114. package/dist/server/errors.js.map +1 -0
  115. package/dist/server/index.d.ts +18 -0
  116. package/dist/server/index.d.ts.map +1 -0
  117. package/dist/server/index.js +208 -0
  118. package/dist/server/index.js.map +1 -0
  119. package/dist/server/process.d.ts +27 -0
  120. package/dist/server/process.d.ts.map +1 -0
  121. package/dist/server/process.js +114 -0
  122. package/dist/server/process.js.map +1 -0
  123. package/dist/server/readiness.d.ts +13 -0
  124. package/dist/server/readiness.d.ts.map +1 -0
  125. package/dist/server/readiness.js +56 -0
  126. package/dist/server/readiness.js.map +1 -0
  127. package/dist/server/redact.d.ts +3 -0
  128. package/dist/server/redact.d.ts.map +1 -0
  129. package/dist/server/redact.js +16 -0
  130. package/dist/server/redact.js.map +1 -0
  131. package/dist/server/targets.d.ts +48 -0
  132. package/dist/server/targets.d.ts.map +1 -0
  133. package/dist/server/targets.js +90 -0
  134. package/dist/server/targets.js.map +1 -0
  135. package/dist/server/types.d.ts +87 -0
  136. package/dist/server/types.d.ts.map +1 -0
  137. package/dist/server/types.js +2 -0
  138. package/dist/server/types.js.map +1 -0
  139. package/package.json +83 -4
  140. package/src/client/errors.ts +104 -0
  141. package/src/client/index.ts +95 -0
  142. package/src/client/resources/config.ts +124 -0
  143. package/src/client/resources/credentials.ts +133 -0
  144. package/src/client/resources/download.ts +21 -0
  145. package/src/client/resources/logs.ts +46 -0
  146. package/src/client/resources/oauth.ts +105 -0
  147. package/src/client/resources/plugins.ts +70 -0
  148. package/src/client/resources/requests.ts +20 -0
  149. package/src/client/resources/routing.ts +29 -0
  150. package/src/client/resources/server.ts +52 -0
  151. package/src/client/resources/usage.ts +27 -0
  152. package/src/client/transport.ts +215 -0
  153. package/src/client/types/common.ts +38 -0
  154. package/src/client/types/config.ts +248 -0
  155. package/src/client/types/credentials.ts +195 -0
  156. package/src/client/types/index.ts +9 -0
  157. package/src/client/types/oauth.ts +71 -0
  158. package/src/client/types/observability.ts +93 -0
  159. package/src/client/types/plugins.ts +136 -0
  160. package/src/client/types/requests.ts +19 -0
  161. package/src/client/types/routing.ts +44 -0
  162. package/src/client/types/server.ts +18 -0
  163. package/src/index.ts +2 -0
  164. package/src/server/binary.ts +128 -0
  165. package/src/server/config.ts +128 -0
  166. package/src/server/cpa-version.ts +11 -0
  167. package/src/server/errors.ts +48 -0
  168. package/src/server/index.ts +236 -0
  169. package/src/server/process.ts +124 -0
  170. package/src/server/readiness.ts +58 -0
  171. package/src/server/redact.ts +14 -0
  172. package/src/server/targets.ts +133 -0
  173. package/src/server/types.ts +93 -0
@@ -0,0 +1,70 @@
1
+ import type {
2
+ DeletePluginResponse,
3
+ PluginInstallQuery,
4
+ PluginInstallResponse,
5
+ PluginListResponse,
6
+ PluginQuotaResetResponse,
7
+ PluginStoreListResponse,
8
+ QuotaFetchResponse,
9
+ } from '../types/plugins.ts';
10
+ import type { RequestOptions, Transport } from '../transport.ts';
11
+
12
+ /** `/v8/management/plugins*`. */
13
+ export class PluginsResource {
14
+ readonly #t: Transport;
15
+
16
+ constructor(transport: Transport) {
17
+ this.#t = transport;
18
+ }
19
+
20
+ async list(options: RequestOptions = {}): Promise<PluginListResponse> {
21
+ return (await this.#t.request<PluginListResponse>({ ...options, method: 'GET', path: '/plugins' })).data;
22
+ }
23
+
24
+ /** Deletes the plugin file and its `plugins.configs` entry. */
25
+ async delete(id: string, options: RequestOptions = {}): Promise<DeletePluginResponse> {
26
+ return (await this.#t.request<DeletePluginResponse>({ ...options, method: 'DELETE', path: `/plugins/${encodeURIComponent(id)}` })).data;
27
+ }
28
+
29
+ /** Lists plugins from every store source (network access on the server). */
30
+ async store(options: RequestOptions = {}): Promise<PluginStoreListResponse> {
31
+ return (await this.#t.request<PluginStoreListResponse>({ ...options, method: 'GET', path: '/plugins/store' })).data;
32
+ }
33
+
34
+ /**
35
+ * Installs a store plugin by plugin id (not `store_id`) and enables it.
36
+ * The global `plugins.enabled` switch is left unchanged.
37
+ */
38
+ async install(id: string, query: PluginInstallQuery = {}, options: RequestOptions = {}): Promise<PluginInstallResponse> {
39
+ return (
40
+ await this.#t.request<PluginInstallResponse>({
41
+ ...options,
42
+ method: 'POST',
43
+ path: `/plugins/store/${encodeURIComponent(id)}/install`,
44
+ query: { ...query },
45
+ })
46
+ ).data;
47
+ }
48
+
49
+ /**
50
+ * Fetches quota for a credential from the plugin's quota provider.
51
+ * (`POST /plugins/:id/quota` runs the same handler and is not exposed separately.)
52
+ */
53
+ async quota(id: string, authIndex: string, options: RequestOptions = {}): Promise<QuotaFetchResponse> {
54
+ return (
55
+ await this.#t.request<QuotaFetchResponse>({ ...options, method: 'GET', path: `/plugins/${encodeURIComponent(id)}/quota`, query: { auth_index: authIndex } })
56
+ ).data;
57
+ }
58
+
59
+ /** Resets plugin quota state for a credential, then its routing cooldown. */
60
+ async resetQuota(id: string, authIndex: string, options: RequestOptions = {}): Promise<PluginQuotaResetResponse> {
61
+ return (
62
+ await this.#t.request<PluginQuotaResetResponse>({
63
+ ...options,
64
+ method: 'DELETE',
65
+ path: `/plugins/${encodeURIComponent(id)}/quota`,
66
+ query: { auth_index: authIndex },
67
+ })
68
+ ).data;
69
+ }
70
+ }
@@ -0,0 +1,20 @@
1
+ import type { ApiCallRequest, ApiCallResponse } from '../types/requests.ts';
2
+ import type { RequestOptions, Transport } from '../transport.ts';
3
+
4
+ /** `/v8/management/requests/*`. */
5
+ export class RequestsResource {
6
+ readonly #t: Transport;
7
+
8
+ constructor(transport: Transport) {
9
+ this.#t = transport;
10
+ }
11
+
12
+ /**
13
+ * Makes an HTTP request from the server, optionally with a credential's
14
+ * proxy and token (`$TOKEN$` in headers or `data`). Resolves whenever the
15
+ * upstream call completed, whatever its status; check `status_code`.
16
+ */
17
+ async apiCall(request: ApiCallRequest, options: RequestOptions = {}): Promise<ApiCallResponse> {
18
+ return (await this.#t.request<ApiCallResponse>({ ...options, method: 'POST', path: '/requests/api-call', json: request })).data;
19
+ }
20
+ }
@@ -0,0 +1,29 @@
1
+ import type { CooldownResetResponse, ModelDefinitionsResponse, StaticModelChannel } from '../types/routing.ts';
2
+ import type { RequestOptions, Transport } from '../transport.ts';
3
+
4
+ /** `/v8/management/routing/*`. */
5
+ export class RoutingResource {
6
+ readonly #t: Transport;
7
+
8
+ constructor(transport: Transport) {
9
+ this.#t = transport;
10
+ }
11
+
12
+ /** Clears quota and cooldown state for one credential. */
13
+ async resetCooldown(authIndex: string, options: RequestOptions = {}): Promise<CooldownResetResponse> {
14
+ return (
15
+ await this.#t.request<CooldownResetResponse>({ ...options, method: 'POST', path: '/routing/cooldown/reset', json: { auth_index: authIndex } })
16
+ ).data;
17
+ }
18
+
19
+ /** Built-in model definitions for a channel (or a plugin provider). */
20
+ async modelDefinitions(channel: StaticModelChannel | (string & {}), options: RequestOptions = {}): Promise<ModelDefinitionsResponse> {
21
+ return (
22
+ await this.#t.request<ModelDefinitionsResponse>({
23
+ ...options,
24
+ method: 'GET',
25
+ path: `/routing/model-definitions/${encodeURIComponent(channel)}`,
26
+ })
27
+ ).data;
28
+ }
29
+ }
@@ -0,0 +1,52 @@
1
+ import { CpaHttpError } from '../errors.ts';
2
+ import type { HealthzResponse, LatestVersionResponse, ServerInfo } from '../types/server.ts';
3
+ import type { RequestOptions, Transport } from '../transport.ts';
4
+
5
+ function readInfo(headers: Headers): ServerInfo | undefined {
6
+ const version = headers.get('X-CPA-VERSION');
7
+ if (version === null) return undefined;
8
+ return {
9
+ version,
10
+ commit: headers.get('X-CPA-COMMIT') ?? '',
11
+ buildDate: headers.get('X-CPA-BUILD-DATE') ?? '',
12
+ supportPlugin: headers.get('X-CPA-SUPPORT-PLUGIN') === '1',
13
+ };
14
+ }
15
+
16
+ /** Server information: `/healthz`, version headers and `/v8/management/server/*`. */
17
+ export class ServerResource {
18
+ readonly #t: Transport;
19
+
20
+ constructor(transport: Transport) {
21
+ this.#t = transport;
22
+ }
23
+
24
+ /** `GET /healthz` (no management key needed). */
25
+ async health(options: RequestOptions = {}): Promise<HealthzResponse> {
26
+ return (await this.#t.request<HealthzResponse>({ ...options, method: 'GET', path: '/healthz', root: true, anonymous: true })).data;
27
+ }
28
+
29
+ /**
30
+ * Version, commit, build date and plugin support of the running server,
31
+ * read from the `X-CPA-*` headers of a cheap authenticated request.
32
+ */
33
+ async info(options: RequestOptions = {}): Promise<ServerInfo> {
34
+ let headers: Headers;
35
+ try {
36
+ headers = (await this.#t.request<unknown>({ ...options, method: 'GET', path: '/config/config-version' })).headers;
37
+ } catch (error) {
38
+ // A config without `config-version` answers 404, which still carries the headers.
39
+ if (!(error instanceof CpaHttpError) || error.status !== 404) throw error;
40
+ headers = error.headers;
41
+ }
42
+ const info = readInfo(headers);
43
+ if (!info) throw new TypeError('The server did not send X-CPA-VERSION; is management enabled?');
44
+ return info;
45
+ }
46
+
47
+ /** Latest CLIProxyAPI release tag on GitHub (e.g. `"v8.0.43"`), fetched by the server. */
48
+ async latestVersion(options: RequestOptions = {}): Promise<string> {
49
+ const res = await this.#t.request<LatestVersionResponse>({ ...options, method: 'GET', path: '/server/latest-version' });
50
+ return res.data['latest-version'];
51
+ }
52
+ }
@@ -0,0 +1,27 @@
1
+ import type { ApiKeyUsageResponse, UsageQueueResponse } from '../types/observability.ts';
2
+ import type { RequestOptions, Transport } from '../transport.ts';
3
+
4
+ /** `/v8/management/observability/usage/*`. */
5
+ export class UsageResource {
6
+ readonly #t: Transport;
7
+
8
+ constructor(transport: Transport) {
9
+ this.#t = transport;
10
+ }
11
+
12
+ /** Request counters per config API key. Map keys are `"<base-url>|<api-key>"` with the raw key. */
13
+ async apiKeys(options: RequestOptions = {}): Promise<ApiKeyUsageResponse> {
14
+ return (await this.#t.request<ApiKeyUsageResponse>({ ...options, method: 'GET', path: '/observability/usage/api-keys' })).data;
15
+ }
16
+
17
+ /**
18
+ * Removes and returns up to `count` (default 1) of the oldest queued usage
19
+ * records. Destructive: concurrent consumers split the records between them.
20
+ * Records are only queued while `observability.usage.usage-statistics-enabled` is on.
21
+ */
22
+ async popQueue(count?: number, options: RequestOptions = {}): Promise<UsageQueueResponse> {
23
+ return (
24
+ await this.#t.request<UsageQueueResponse>({ ...options, method: 'GET', path: '/observability/usage/queue', query: { count } })
25
+ ).data;
26
+ }
27
+ }
@@ -0,0 +1,215 @@
1
+ import { CpaAbortError, CpaHttpError, CpaNetworkError, CpaResponseError, CpaTimeoutError } from './errors.ts';
2
+
3
+ export const MANAGEMENT_PREFIX = '/v8/management';
4
+
5
+ export type FetchLike = (input: Request) => Promise<Response>;
6
+
7
+ export type QueryValue = string | number | boolean | null | undefined | readonly (string | number | boolean)[];
8
+ export type Query = Readonly<Record<string, QueryValue>>;
9
+
10
+ export interface ClientOptions {
11
+ /**
12
+ * Server root, e.g. `http://127.0.0.1:8317` or `https://example.com/cpa`.
13
+ * A path prefix (reverse proxy) is kept; `/v8/management` is appended to it.
14
+ */
15
+ baseUrl: string | URL;
16
+ /** Management key (`management.secret-key` or `MANAGEMENT_PASSWORD`), not a model API key. */
17
+ managementKey: string;
18
+ /** Custom fetch. Defaults to the global `fetch`. */
19
+ fetch?: (input: Request) => Promise<Response>;
20
+ /** Headers sent with every request. */
21
+ headers?: HeadersInit;
22
+ /** Default per-request timeout in ms. `0` or omitted disables it. */
23
+ timeoutMs?: number;
24
+ }
25
+
26
+ /** Per-call options accepted by every client method. */
27
+ export interface RequestOptions {
28
+ signal?: AbortSignal | undefined;
29
+ /** Overrides the client's default timeout; `0` disables it. */
30
+ timeoutMs?: number | undefined;
31
+ headers?: HeadersInit | undefined;
32
+ }
33
+
34
+ export type ResponseType = 'json' | 'text' | 'blob' | 'response';
35
+
36
+ export interface RawRequest extends RequestOptions {
37
+ method: string;
38
+ /** Path below `/v8/management` (e.g. `/config`), already encoded. */
39
+ path: string;
40
+ query?: Query;
41
+ /** Serialised with `JSON.stringify`, including strings and `null`. */
42
+ json?: unknown;
43
+ /** Sent as-is. Mutually exclusive with `json`. */
44
+ body?: BodyInit;
45
+ /** Content type for `body`. FormData sets its own. */
46
+ contentType?: string;
47
+ responseType?: ResponseType;
48
+ /** Skip the management key, for unauthenticated routes. */
49
+ anonymous?: boolean;
50
+ /** Resolve `path` against the server root instead of `/v8/management`. */
51
+ root?: boolean;
52
+ }
53
+
54
+ export interface TransportResponse<T> {
55
+ data: T;
56
+ status: number;
57
+ headers: Headers;
58
+ }
59
+
60
+ function appendQuery(url: URL, query: Query | undefined): void {
61
+ if (!query) return;
62
+ for (const [key, value] of Object.entries(query)) {
63
+ if (value === undefined || value === null) continue;
64
+ if (Array.isArray(value)) for (const item of value) url.searchParams.append(key, String(item));
65
+ else url.searchParams.append(key, String(value));
66
+ }
67
+ }
68
+
69
+ /** Joins the base URL and a management path while keeping any base path prefix. */
70
+ export function managementUrl(baseUrl: string | URL, path: string, root = false): URL {
71
+ const base = new URL(baseUrl);
72
+ const prefix = base.pathname.replace(/\/+$/, '');
73
+ const url = new URL(base.origin);
74
+ url.pathname = `${prefix}${root ? '' : MANAGEMENT_PREFIX}${path}`;
75
+ return url;
76
+ }
77
+
78
+ export class Transport {
79
+ readonly #baseUrl: URL;
80
+ readonly #managementKey: string;
81
+ readonly #fetch: FetchLike;
82
+ readonly #headers: Headers;
83
+ readonly #timeoutMs: number;
84
+
85
+ constructor(options: ClientOptions) {
86
+ if (!options.baseUrl) throw new TypeError('baseUrl is required.');
87
+ if (!options.managementKey) throw new TypeError('managementKey is required.');
88
+ this.#baseUrl = new URL(options.baseUrl);
89
+ if (this.#baseUrl.protocol !== 'http:' && this.#baseUrl.protocol !== 'https:') {
90
+ throw new TypeError(`baseUrl must be http(s), got ${this.#baseUrl.protocol}`);
91
+ }
92
+ this.#managementKey = options.managementKey;
93
+ const custom = options.fetch;
94
+ this.#fetch = custom ? (request) => custom(request) : (request) => fetch(request);
95
+ this.#headers = new Headers(options.headers);
96
+ this.#timeoutMs = options.timeoutMs ?? 0;
97
+ }
98
+
99
+ get baseUrl(): URL {
100
+ return new URL(this.#baseUrl);
101
+ }
102
+
103
+ async request<T>(req: RawRequest): Promise<TransportResponse<T>> {
104
+ const url = managementUrl(this.#baseUrl, req.path, req.root);
105
+ appendQuery(url, req.query);
106
+ const target = { method: req.method, url: url.toString() };
107
+
108
+ const headers = new Headers(this.#headers);
109
+ new Headers(req.headers).forEach((value, key) => headers.set(key, value));
110
+ if (!req.anonymous) headers.set('Authorization', `Bearer ${this.#managementKey}`);
111
+
112
+ let body: BodyInit | undefined;
113
+ if ('json' in req) {
114
+ body = JSON.stringify(req.json);
115
+ headers.set('Content-Type', 'application/json');
116
+ } else if (req.body !== undefined) {
117
+ body = req.body;
118
+ if (req.contentType) headers.set('Content-Type', req.contentType);
119
+ }
120
+ if (!headers.has('Accept')) headers.set('Accept', req.responseType === 'json' || !req.responseType ? 'application/json' : '*/*');
121
+
122
+ const timeoutMs = req.timeoutMs ?? this.#timeoutMs;
123
+ const userSignal = req.signal;
124
+ let timedOut = false;
125
+ const classify = (error: unknown): Error => {
126
+ if (userSignal?.aborted) return new CpaAbortError(`${target.method} ${target.url} was aborted`, target, { cause: userSignal.reason });
127
+ if (timedOut) return new CpaTimeoutError(target, timeoutMs, { cause: error });
128
+ return new CpaNetworkError(`${target.method} ${target.url} failed: ${(error as Error)?.message ?? String(error)}`, target, {
129
+ cause: error,
130
+ });
131
+ };
132
+ if (userSignal?.aborted) throw classify(userSignal.reason);
133
+
134
+ // One controller owned by this call, rather than AbortSignal.any/timeout: on some Node
135
+ // versions signals derived that way can be garbage-collected before they fire.
136
+ const controller = new AbortController();
137
+ let rejectAborted: (error: Error) => void = () => {};
138
+ const aborted = new Promise<never>((_, reject) => {
139
+ rejectAborted = reject;
140
+ });
141
+ aborted.catch(() => {});
142
+ const abort = (reason: unknown): void => {
143
+ controller.abort(reason);
144
+ rejectAborted(classify(reason));
145
+ };
146
+ const onUserAbort = (): void => abort(userSignal!.reason);
147
+ userSignal?.addEventListener('abort', onUserAbort, { once: true });
148
+ let timer: ReturnType<typeof setTimeout> | undefined;
149
+ if (timeoutMs > 0) {
150
+ timer = setTimeout(() => {
151
+ timedOut = true;
152
+ abort(new DOMException(`Timed out after ${timeoutMs} ms`, 'TimeoutError'));
153
+ }, timeoutMs);
154
+ }
155
+ const release = (): void => {
156
+ clearTimeout(timer);
157
+ userSignal?.removeEventListener('abort', onUserAbort);
158
+ };
159
+ // Every await below races the abort, so a fetch that ignores its signal still settles.
160
+ const guard = <V>(promise: Promise<V>): Promise<V> => Promise.race([promise, aborted]);
161
+
162
+ const init: RequestInit = { method: req.method, headers, redirect: 'manual', signal: controller.signal };
163
+ if (body !== undefined) init.body = body;
164
+
165
+ const responseType = req.responseType ?? 'json';
166
+ try {
167
+ let response: Response;
168
+ try {
169
+ response = await guard(this.#fetch(new Request(url, init)));
170
+ } catch (error) {
171
+ throw error instanceof CpaAbortError || error instanceof CpaTimeoutError ? error : classify(error);
172
+ }
173
+
174
+ if (!response.ok) {
175
+ throw new CpaHttpError(target, response, await guard(response.text()));
176
+ }
177
+ if (responseType === 'response') {
178
+ return { data: response as T, status: response.status, headers: response.headers };
179
+ }
180
+ if (responseType === 'blob') {
181
+ return { data: (await guard(response.blob())) as T, status: response.status, headers: response.headers };
182
+ }
183
+ const text = await guard(response.text());
184
+ if (responseType === 'text') return { data: text as T, status: response.status, headers: response.headers };
185
+ if (text === '') return { data: undefined as T, status: response.status, headers: response.headers };
186
+ try {
187
+ return { data: JSON.parse(text) as T, status: response.status, headers: response.headers };
188
+ } catch (cause) {
189
+ throw new CpaResponseError(`${target.method} ${target.url} returned invalid JSON`, target, response.status, text, { cause });
190
+ }
191
+ } catch (error) {
192
+ if (error instanceof CpaHttpError || error instanceof CpaResponseError || error instanceof CpaAbortError || error instanceof CpaTimeoutError) {
193
+ throw error;
194
+ }
195
+ throw classify(error);
196
+ } finally {
197
+ // For `response`, the caller reads the body; the timeout covers the headers only,
198
+ // while the caller's own signal keeps working through the request's signal.
199
+ if (responseType === 'response') clearTimeout(timer);
200
+ else release();
201
+ }
202
+ }
203
+ }
204
+
205
+ /** Encodes `a/b/c` or `['a', 'b', 'c']` as URL path segments. */
206
+ export function encodePath(path: string | readonly string[], what = 'path'): string {
207
+ const segments = typeof path === 'string' ? path.split('/').filter((s, i, all) => s !== '' || (i > 0 && i < all.length - 1)) : [...path];
208
+ for (const segment of segments) {
209
+ if (segment === '') throw new TypeError(`${what} contains an empty segment.`);
210
+ if (typeof path !== 'string' && segment.includes('/')) {
211
+ throw new TypeError(`${what} segment "${segment}" contains "/", which CPA cannot address; edit its parent instead.`);
212
+ }
213
+ }
214
+ return segments.map(encodeURIComponent).join('/');
215
+ }
@@ -0,0 +1,38 @@
1
+ // Shared response types.
2
+ // Derived from CLIProxyAPI v8.0.43; see docs/api-coverage.md.
3
+
4
+ export interface StatusErrorBody { status: 'error'; error: string }
5
+ export interface OkStatus { status: 'ok' }
6
+
7
+ export type AuthStatus = 'unknown' | 'active' | 'pending' | 'refreshing' | 'error' | 'disabled';
8
+ export type SIWCStatus = 'off' | 'valid' | 'invalid';
9
+ export type CooldownReason =
10
+ | 'credential_quota' | 'quota' | 'cloudflare_challenge' | 'model_not_supported'
11
+ | 'invalid_grant' | 'unauthorized' | 'payment_required' | 'not_found'
12
+ | 'transient_error' | 'unknown';
13
+
14
+ /** Any JSON value. */
15
+ export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
16
+
17
+ /** Ten-minute request bucket; `time` is `"HH:MM-HH:MM"` in the server's local time. */
18
+ export interface RecentRequestBucket { time: string; success: number; failed: number }
19
+
20
+ export interface PartialFailure { name: string; error: string }
21
+
22
+ /** Error body from the auth middleware and most ops handlers. */
23
+ export interface CpaSimpleError { error: string }
24
+ /** Error body from latest-version, plugins, plugin store, plugin delete. */
25
+ export interface CpaCodedError {
26
+ error: string; // machine code, e.g. "plugin_not_found"
27
+ message: string;
28
+ [extra: string]: unknown; // restart_required, path, file_deleted, sources, retry_after, retry_at, ...
29
+ }
30
+ export type CpaErrorBody = CpaSimpleError | CpaCodedError;
31
+
32
+ /** Auth outcomes: 401 missing/invalid key; 403 banned / remote disabled / key not set; 404 management disabled (empty body). */
33
+ export type ManagementAuthErrorMessage =
34
+ | "missing management key"
35
+ | "invalid management key"
36
+ | "remote management disabled"
37
+ | "remote management key not set"
38
+ | `IP banned due to too many failed attempts. Try again in ${string}`;