@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.
- package/LICENSE +21 -0
- package/README.md +142 -2
- package/dist/client/errors.d.ts +70 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +91 -0
- package/dist/client/errors.js.map +1 -0
- package/dist/client/index.d.ts +46 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +65 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/resources/config.d.ts +61 -0
- package/dist/client/resources/config.d.ts.map +1 -0
- package/dist/client/resources/config.js +80 -0
- package/dist/client/resources/config.js.map +1 -0
- package/dist/client/resources/credentials.d.ts +64 -0
- package/dist/client/resources/credentials.d.ts.map +1 -0
- package/dist/client/resources/credentials.js +94 -0
- package/dist/client/resources/credentials.js.map +1 -0
- package/dist/client/resources/download.d.ts +8 -0
- package/dist/client/resources/download.d.ts.map +1 -0
- package/dist/client/resources/download.js +17 -0
- package/dist/client/resources/download.js.map +1 -0
- package/dist/client/resources/logs.d.ts +23 -0
- package/dist/client/resources/logs.d.ts.map +1 -0
- package/dist/client/resources/logs.js +37 -0
- package/dist/client/resources/logs.js.map +1 -0
- package/dist/client/resources/oauth.d.ts +43 -0
- package/dist/client/resources/oauth.d.ts.map +1 -0
- package/dist/client/resources/oauth.js +70 -0
- package/dist/client/resources/oauth.js.map +1 -0
- package/dist/client/resources/plugins.d.ts +25 -0
- package/dist/client/resources/plugins.d.ts.map +1 -0
- package/dist/client/resources/plugins.js +47 -0
- package/dist/client/resources/plugins.js.map +1 -0
- package/dist/client/resources/requests.d.ts +14 -0
- package/dist/client/resources/requests.d.ts.map +1 -0
- package/dist/client/resources/requests.js +16 -0
- package/dist/client/resources/requests.js.map +1 -0
- package/dist/client/resources/routing.d.ts +12 -0
- package/dist/client/resources/routing.d.ts.map +1 -0
- package/dist/client/resources/routing.js +20 -0
- package/dist/client/resources/routing.js.map +1 -0
- package/dist/client/resources/server.d.ts +17 -0
- package/dist/client/resources/server.d.ts.map +1 -0
- package/dist/client/resources/server.js +49 -0
- package/dist/client/resources/server.js.map +1 -0
- package/dist/client/resources/usage.d.ts +16 -0
- package/dist/client/resources/usage.d.ts.map +1 -0
- package/dist/client/resources/usage.js +20 -0
- package/dist/client/resources/usage.js.map +1 -0
- package/dist/client/transport.d.ts +60 -0
- package/dist/client/transport.d.ts.map +1 -0
- package/dist/client/transport.js +170 -0
- package/dist/client/transport.js.map +1 -0
- package/dist/client/types/common.d.ts +38 -0
- package/dist/client/types/common.d.ts.map +1 -0
- package/dist/client/types/common.js +4 -0
- package/dist/client/types/common.js.map +1 -0
- package/dist/client/types/config.d.ts +407 -0
- package/dist/client/types/config.d.ts.map +1 -0
- package/dist/client/types/config.js +9 -0
- package/dist/client/types/config.js.map +1 -0
- package/dist/client/types/credentials.d.ts +240 -0
- package/dist/client/types/credentials.d.ts.map +1 -0
- package/dist/client/types/credentials.js +4 -0
- package/dist/client/types/credentials.js.map +1 -0
- package/dist/client/types/index.d.ts +10 -0
- package/dist/client/types/index.d.ts.map +1 -0
- package/dist/client/types/index.js +2 -0
- package/dist/client/types/index.js.map +1 -0
- package/dist/client/types/oauth.d.ts +89 -0
- package/dist/client/types/oauth.d.ts.map +1 -0
- package/dist/client/types/oauth.js +4 -0
- package/dist/client/types/oauth.js.map +1 -0
- package/dist/client/types/observability.d.ts +111 -0
- package/dist/client/types/observability.d.ts.map +1 -0
- package/dist/client/types/observability.js +4 -0
- package/dist/client/types/observability.js.map +1 -0
- package/dist/client/types/plugins.d.ts +171 -0
- package/dist/client/types/plugins.d.ts.map +1 -0
- package/dist/client/types/plugins.js +4 -0
- package/dist/client/types/plugins.js.map +1 -0
- package/dist/client/types/requests.d.ts +16 -0
- package/dist/client/types/requests.d.ts.map +1 -0
- package/dist/client/types/requests.js +4 -0
- package/dist/client/types/requests.js.map +1 -0
- package/dist/client/types/routing.d.ts +50 -0
- package/dist/client/types/routing.d.ts.map +1 -0
- package/dist/client/types/routing.js +4 -0
- package/dist/client/types/routing.js.map +1 -0
- package/dist/client/types/server.d.ts +15 -0
- package/dist/client/types/server.d.ts.map +1 -0
- package/dist/client/types/server.js +4 -0
- package/dist/client/types/server.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server/binary.d.ts +35 -0
- package/dist/server/binary.d.ts.map +1 -0
- package/dist/server/binary.js +108 -0
- package/dist/server/binary.js.map +1 -0
- package/dist/server/config.d.ts +37 -0
- package/dist/server/config.d.ts.map +1 -0
- package/dist/server/config.js +103 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/cpa-version.d.ts +12 -0
- package/dist/server/cpa-version.d.ts.map +1 -0
- package/dist/server/cpa-version.js +12 -0
- package/dist/server/cpa-version.js.map +1 -0
- package/dist/server/errors.d.ts +36 -0
- package/dist/server/errors.d.ts.map +1 -0
- package/dist/server/errors.js +26 -0
- package/dist/server/errors.js.map +1 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +208 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/process.d.ts +27 -0
- package/dist/server/process.d.ts.map +1 -0
- package/dist/server/process.js +114 -0
- package/dist/server/process.js.map +1 -0
- package/dist/server/readiness.d.ts +13 -0
- package/dist/server/readiness.d.ts.map +1 -0
- package/dist/server/readiness.js +56 -0
- package/dist/server/readiness.js.map +1 -0
- package/dist/server/redact.d.ts +3 -0
- package/dist/server/redact.d.ts.map +1 -0
- package/dist/server/redact.js +16 -0
- package/dist/server/redact.js.map +1 -0
- package/dist/server/targets.d.ts +48 -0
- package/dist/server/targets.d.ts.map +1 -0
- package/dist/server/targets.js +90 -0
- package/dist/server/targets.js.map +1 -0
- package/dist/server/types.d.ts +87 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/server/types.js.map +1 -0
- package/package.json +83 -4
- package/src/client/errors.ts +104 -0
- package/src/client/index.ts +95 -0
- package/src/client/resources/config.ts +124 -0
- package/src/client/resources/credentials.ts +133 -0
- package/src/client/resources/download.ts +21 -0
- package/src/client/resources/logs.ts +46 -0
- package/src/client/resources/oauth.ts +105 -0
- package/src/client/resources/plugins.ts +70 -0
- package/src/client/resources/requests.ts +20 -0
- package/src/client/resources/routing.ts +29 -0
- package/src/client/resources/server.ts +52 -0
- package/src/client/resources/usage.ts +27 -0
- package/src/client/transport.ts +215 -0
- package/src/client/types/common.ts +38 -0
- package/src/client/types/config.ts +248 -0
- package/src/client/types/credentials.ts +195 -0
- package/src/client/types/index.ts +9 -0
- package/src/client/types/oauth.ts +71 -0
- package/src/client/types/observability.ts +93 -0
- package/src/client/types/plugins.ts +136 -0
- package/src/client/types/requests.ts +19 -0
- package/src/client/types/routing.ts +44 -0
- package/src/client/types/server.ts +18 -0
- package/src/index.ts +2 -0
- package/src/server/binary.ts +128 -0
- package/src/server/config.ts +128 -0
- package/src/server/cpa-version.ts +11 -0
- package/src/server/errors.ts +48 -0
- package/src/server/index.ts +236 -0
- package/src/server/process.ts +124 -0
- package/src/server/readiness.ts +58 -0
- package/src/server/redact.ts +14 -0
- package/src/server/targets.ts +133 -0
- package/src/server/types.ts +93 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Base class for every error thrown by the client. */
|
|
2
|
+
export class CpaClientError extends Error {
|
|
3
|
+
override readonly name: string = 'CpaClientError';
|
|
4
|
+
readonly method: string;
|
|
5
|
+
readonly url: string;
|
|
6
|
+
|
|
7
|
+
constructor(message: string, request: { method: string; url: string }, options?: { cause?: unknown }) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.method = request.method;
|
|
10
|
+
this.url = request.url;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The server answered with a non-2xx status.
|
|
16
|
+
*
|
|
17
|
+
* `body` is the raw response text. `data` is the parsed JSON body when there
|
|
18
|
+
* was one; CPA errors usually look like `{ "error": "<code>", "message"?: "..." }`.
|
|
19
|
+
*/
|
|
20
|
+
export class CpaHttpError extends CpaClientError {
|
|
21
|
+
override readonly name = 'CpaHttpError';
|
|
22
|
+
readonly status: number;
|
|
23
|
+
readonly statusText: string;
|
|
24
|
+
readonly headers: Headers;
|
|
25
|
+
readonly body: string;
|
|
26
|
+
readonly data: unknown;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
request: { method: string; url: string },
|
|
30
|
+
response: { status: number; statusText: string; headers: Headers },
|
|
31
|
+
body: string,
|
|
32
|
+
) {
|
|
33
|
+
const data = parseJsonOrUndefined(body);
|
|
34
|
+
const detail = describeErrorBody(data) ?? (body.trim().slice(0, 200) || response.statusText);
|
|
35
|
+
super(`${request.method} ${request.url} failed with ${response.status}${detail ? `: ${detail}` : ''}`, request);
|
|
36
|
+
this.status = response.status;
|
|
37
|
+
this.statusText = response.statusText;
|
|
38
|
+
this.headers = response.headers;
|
|
39
|
+
this.body = body;
|
|
40
|
+
this.data = data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The `error` field of a CPA error body, e.g. `"not_found"` or `"invalid management key"`. */
|
|
44
|
+
get errorCode(): string | undefined {
|
|
45
|
+
const value = (this.data as { error?: unknown } | undefined)?.error;
|
|
46
|
+
return typeof value === 'string' ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The `message` field of a CPA error body, when present. */
|
|
50
|
+
get errorMessage(): string | undefined {
|
|
51
|
+
const value = (this.data as { message?: unknown } | undefined)?.message;
|
|
52
|
+
return typeof value === 'string' ? value : undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The request could not be sent or the response could not be read. */
|
|
57
|
+
export class CpaNetworkError extends CpaClientError {
|
|
58
|
+
override readonly name = 'CpaNetworkError';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The request exceeded its timeout. */
|
|
62
|
+
export class CpaTimeoutError extends CpaClientError {
|
|
63
|
+
override readonly name = 'CpaTimeoutError';
|
|
64
|
+
readonly timeoutMs: number;
|
|
65
|
+
|
|
66
|
+
constructor(request: { method: string; url: string }, timeoutMs: number, options?: { cause?: unknown }) {
|
|
67
|
+
super(`${request.method} ${request.url} timed out after ${timeoutMs} ms`, request, options);
|
|
68
|
+
this.timeoutMs = timeoutMs;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The caller's AbortSignal was aborted. `cause` is the signal's reason. */
|
|
73
|
+
export class CpaAbortError extends CpaClientError {
|
|
74
|
+
override readonly name = 'CpaAbortError';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A 2xx response body did not match the expected format (e.g. invalid JSON). */
|
|
78
|
+
export class CpaResponseError extends CpaClientError {
|
|
79
|
+
override readonly name = 'CpaResponseError';
|
|
80
|
+
readonly status: number;
|
|
81
|
+
readonly body: string;
|
|
82
|
+
|
|
83
|
+
constructor(message: string, request: { method: string; url: string }, status: number, body: string, options?: { cause?: unknown }) {
|
|
84
|
+
super(message, request, options);
|
|
85
|
+
this.status = status;
|
|
86
|
+
this.body = body;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseJsonOrUndefined(text: string): unknown {
|
|
91
|
+
if (!text) return undefined;
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(text);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function describeErrorBody(data: unknown): string | undefined {
|
|
100
|
+
if (typeof data !== 'object' || data === null) return undefined;
|
|
101
|
+
const { error, message } = data as { error?: unknown; message?: unknown };
|
|
102
|
+
const parts = [error, message].filter((v): v is string => typeof v === 'string' && v !== '');
|
|
103
|
+
return parts.length ? parts.join(': ') : undefined;
|
|
104
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ConfigResource } from './resources/config.ts';
|
|
2
|
+
import { CredentialsResource } from './resources/credentials.ts';
|
|
3
|
+
import { LogsResource } from './resources/logs.ts';
|
|
4
|
+
import { OAuthResource } from './resources/oauth.ts';
|
|
5
|
+
import { PluginsResource } from './resources/plugins.ts';
|
|
6
|
+
import { RequestsResource } from './resources/requests.ts';
|
|
7
|
+
import { RoutingResource } from './resources/routing.ts';
|
|
8
|
+
import { ServerResource } from './resources/server.ts';
|
|
9
|
+
import { UsageResource } from './resources/usage.ts';
|
|
10
|
+
import { Transport, type ClientOptions, type RawRequest, type TransportResponse } from './transport.ts';
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
CpaAbortError,
|
|
14
|
+
CpaClientError,
|
|
15
|
+
CpaHttpError,
|
|
16
|
+
CpaNetworkError,
|
|
17
|
+
CpaResponseError,
|
|
18
|
+
CpaTimeoutError,
|
|
19
|
+
} from './errors.ts';
|
|
20
|
+
export type { ClientOptions, FetchLike, Query, QueryValue, RawRequest, RequestOptions, ResponseType, TransportResponse } from './transport.ts';
|
|
21
|
+
export type { ConfigPath, ConfigWriteOptions, ConfigWriteResult, Versioned } from './resources/config.ts';
|
|
22
|
+
export type { CredentialContent, CredentialFile, CredentialSelector } from './resources/credentials.ts';
|
|
23
|
+
export type { DownloadedFile } from './resources/download.ts';
|
|
24
|
+
export type { WaitForOAuthOptions } from './resources/oauth.ts';
|
|
25
|
+
export {
|
|
26
|
+
ConfigResource,
|
|
27
|
+
CredentialsResource,
|
|
28
|
+
LogsResource,
|
|
29
|
+
OAuthResource,
|
|
30
|
+
PluginsResource,
|
|
31
|
+
RequestsResource,
|
|
32
|
+
RoutingResource,
|
|
33
|
+
ServerResource,
|
|
34
|
+
UsageResource,
|
|
35
|
+
};
|
|
36
|
+
export type * from './types/index.ts';
|
|
37
|
+
|
|
38
|
+
/** Typed client for the CLIProxyAPI v8 management API (`/v8/management`). */
|
|
39
|
+
export class CpaClient {
|
|
40
|
+
readonly #transport: Transport;
|
|
41
|
+
readonly config: ConfigResource;
|
|
42
|
+
readonly credentials: CredentialsResource;
|
|
43
|
+
readonly oauth: OAuthResource;
|
|
44
|
+
readonly logs: LogsResource;
|
|
45
|
+
readonly usage: UsageResource;
|
|
46
|
+
readonly routing: RoutingResource;
|
|
47
|
+
readonly requests: RequestsResource;
|
|
48
|
+
readonly plugins: PluginsResource;
|
|
49
|
+
readonly server: ServerResource;
|
|
50
|
+
|
|
51
|
+
constructor(options: ClientOptions) {
|
|
52
|
+
this.#transport = new Transport(options);
|
|
53
|
+
this.config = new ConfigResource(this.#transport);
|
|
54
|
+
this.credentials = new CredentialsResource(this.#transport);
|
|
55
|
+
this.oauth = new OAuthResource(this.#transport);
|
|
56
|
+
this.logs = new LogsResource(this.#transport);
|
|
57
|
+
this.usage = new UsageResource(this.#transport);
|
|
58
|
+
this.routing = new RoutingResource(this.#transport);
|
|
59
|
+
this.requests = new RequestsResource(this.#transport);
|
|
60
|
+
this.plugins = new PluginsResource(this.#transport);
|
|
61
|
+
this.server = new ServerResource(this.#transport);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Server root URL this client talks to. */
|
|
65
|
+
get baseUrl(): URL {
|
|
66
|
+
return this.#transport.baseUrl;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* OpenAI-compatible model API base URL (`<baseUrl>/v1`), for existing SDKs.
|
|
71
|
+
* Authenticate those requests with a model API key from `access.api-keys`,
|
|
72
|
+
* not with the management key.
|
|
73
|
+
*/
|
|
74
|
+
get modelBaseUrl(): string {
|
|
75
|
+
return modelApiBaseUrl(this.#transport.baseUrl);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Escape hatch for routes without a typed method. `path` is relative to `/v8/management`. */
|
|
79
|
+
request<T = unknown>(request: RawRequest): Promise<TransportResponse<T>> {
|
|
80
|
+
return this.#transport.request<T>(request);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createClient(options: ClientOptions): CpaClient {
|
|
85
|
+
return new CpaClient(options);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `<baseUrl>/v1`, keeping any path prefix. */
|
|
89
|
+
export function modelApiBaseUrl(baseUrl: string | URL): string {
|
|
90
|
+
const url = new URL(baseUrl);
|
|
91
|
+
url.pathname = `${url.pathname.replace(/\/+$/, '')}/v1`;
|
|
92
|
+
url.search = '';
|
|
93
|
+
url.hash = '';
|
|
94
|
+
return url.toString();
|
|
95
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { JsonValue } from '../types/common.ts';
|
|
2
|
+
import type { CpaConfig } from '../types/config.ts';
|
|
3
|
+
import { encodePath, type RequestOptions, type Transport } from '../transport.ts';
|
|
4
|
+
|
|
5
|
+
/** A value together with the ETag of the config file it was read from or written to. */
|
|
6
|
+
export interface Versioned<T> {
|
|
7
|
+
data: T;
|
|
8
|
+
/** Strong ETag of the on-disk file. Pass it as `ifMatch` for optimistic concurrency. */
|
|
9
|
+
etag: string | undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConfigWriteOptions extends RequestOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Only write if the file still has this ETag (or `*`). A mismatch fails with
|
|
15
|
+
* a 412 `CpaHttpError` (`errorCode === 'precondition_failed'`).
|
|
16
|
+
*
|
|
17
|
+
* Writing a plaintext `management.secret-key` makes CPA rewrite the file
|
|
18
|
+
* with a bcrypt hash a few seconds later, so read again before the next
|
|
19
|
+
* conditional write.
|
|
20
|
+
*/
|
|
21
|
+
ifMatch?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConfigWriteResult {
|
|
25
|
+
status: 'ok';
|
|
26
|
+
'config-version': 8;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A config path: `'routing/retry'` or `['routing', 'retry']`. Segments are
|
|
31
|
+
* dash-case YAML keys; list indices are not addressable (lists are replaced whole).
|
|
32
|
+
*/
|
|
33
|
+
export type ConfigPath = string | readonly string[];
|
|
34
|
+
|
|
35
|
+
function withIfMatch(options: ConfigWriteOptions): RequestOptions {
|
|
36
|
+
const { ifMatch, ...rest } = options;
|
|
37
|
+
if (ifMatch === undefined) return rest;
|
|
38
|
+
const headers = new Headers(rest.headers);
|
|
39
|
+
headers.set('If-Match', ifMatch);
|
|
40
|
+
return { ...rest, headers };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function nodePath(path: ConfigPath): string {
|
|
44
|
+
const encoded = encodePath(path, 'config path');
|
|
45
|
+
if (encoded === '') throw new TypeError('config path must not be empty; use the whole-document methods instead.');
|
|
46
|
+
return `/config/${encoded}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `/v8/management/config*`: the native configuration document. */
|
|
50
|
+
export class ConfigResource {
|
|
51
|
+
readonly #t: Transport;
|
|
52
|
+
|
|
53
|
+
constructor(transport: Transport) {
|
|
54
|
+
this.#t = transport;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async #read<T>(path: string, responseType: 'json' | 'text', options: RequestOptions): Promise<Versioned<T>> {
|
|
58
|
+
const res = await this.#t.request<T>({ ...options, method: 'GET', path, responseType });
|
|
59
|
+
return { data: res.data, etag: res.headers.get('ETag') ?? undefined };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #write(method: string, path: string, body: { json: unknown } | { yaml: string } | undefined, options: ConfigWriteOptions) {
|
|
63
|
+
const res = await this.#t.request<ConfigWriteResult>({
|
|
64
|
+
...withIfMatch(options),
|
|
65
|
+
method,
|
|
66
|
+
path,
|
|
67
|
+
...(body === undefined ? {} : 'json' in body ? { json: body.json } : { body: body.yaml, contentType: 'application/yaml' }),
|
|
68
|
+
});
|
|
69
|
+
return { data: res.data, etag: res.headers.get('ETag') ?? undefined } satisfies Versioned<ConfigWriteResult>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The whole config as JSON, migrated to v8 in memory. TURN `username` and
|
|
74
|
+
* `credential` are removed and `auth_index` is added to API keys; writing it
|
|
75
|
+
* back is still safe. Use {@link getYaml} for a lossless copy.
|
|
76
|
+
*/
|
|
77
|
+
get(options: RequestOptions = {}): Promise<Versioned<CpaConfig>> {
|
|
78
|
+
return this.#read('/config', 'json', options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Replaces the whole document. Omitted sections are deleted. */
|
|
82
|
+
replace(config: CpaConfig, options: ConfigWriteOptions = {}): Promise<Versioned<ConfigWriteResult>> {
|
|
83
|
+
return this.#write('PUT', '/config', { json: config }, options);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Deep-merges into the document. Objects merge key by key; arrays and
|
|
88
|
+
* scalars replace. `null` is stored as null, it does not delete — use
|
|
89
|
+
* {@link delete} to remove a key.
|
|
90
|
+
*/
|
|
91
|
+
patch(partial: CpaConfig, options: ConfigWriteOptions = {}): Promise<Versioned<ConfigWriteResult>> {
|
|
92
|
+
return this.#write('PATCH', '/config', { json: partial }, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The config file as YAML, with comments and without redaction. */
|
|
96
|
+
getYaml(options: RequestOptions = {}): Promise<Versioned<string>> {
|
|
97
|
+
return this.#read('/config.yaml', 'text', options);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Replaces the whole document with YAML (JSON is accepted too). */
|
|
101
|
+
replaceYaml(yaml: string, options: ConfigWriteOptions = {}): Promise<Versioned<ConfigWriteResult>> {
|
|
102
|
+
return this.#write('PUT', '/config.yaml', { yaml }, options);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reads one node, e.g. `getPath('server/port')`. A missing node is a 404 `CpaHttpError`. */
|
|
106
|
+
getPath<T = JsonValue>(path: ConfigPath, options: RequestOptions = {}): Promise<Versioned<T>> {
|
|
107
|
+
return this.#read(nodePath(path), 'json', options);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Replaces or creates one node. Missing parent objects are created. */
|
|
111
|
+
set(path: ConfigPath, value: JsonValue, options: ConfigWriteOptions = {}): Promise<Versioned<ConfigWriteResult>> {
|
|
112
|
+
return this.#write('PUT', nodePath(path), { json: value }, options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Deep-merges into one node (see {@link patch}). */
|
|
116
|
+
patchPath(path: ConfigPath, value: JsonValue, options: ConfigWriteOptions = {}): Promise<Versioned<ConfigWriteResult>> {
|
|
117
|
+
return this.#write('PATCH', nodePath(path), { json: value }, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Removes one node and any parents left empty. A missing node is a 404 `CpaHttpError`. */
|
|
121
|
+
delete(path: ConfigPath, options: ConfigWriteOptions = {}): Promise<Versioned<ConfigWriteResult>> {
|
|
122
|
+
return this.#write('DELETE', nodePath(path), undefined, options);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AuthFileEntry,
|
|
3
|
+
CoreAuth,
|
|
4
|
+
CredentialModelsResponse,
|
|
5
|
+
DeleteCredentialsResponse,
|
|
6
|
+
ForceRefreshResult,
|
|
7
|
+
ListCredentialsQuery,
|
|
8
|
+
ListCredentialsResponse,
|
|
9
|
+
PatchCredentialFieldsRequest,
|
|
10
|
+
PatchCredentialFieldsResponse,
|
|
11
|
+
PatchCredentialStatusRequest,
|
|
12
|
+
PatchCredentialStatusResponse,
|
|
13
|
+
SIWCStatusResponse,
|
|
14
|
+
UploadCredentialsResponse,
|
|
15
|
+
} from '../types/credentials.ts';
|
|
16
|
+
import type { RequestOptions, Transport } from '../transport.ts';
|
|
17
|
+
|
|
18
|
+
/** Credential file content: a parsed JSON object, JSON text, or raw bytes. */
|
|
19
|
+
export type CredentialContent = Record<string, unknown> | string | Blob | Uint8Array<ArrayBuffer> | ArrayBuffer;
|
|
20
|
+
|
|
21
|
+
export interface CredentialFile {
|
|
22
|
+
/** File name in the auth directory; must end with `.json`. */
|
|
23
|
+
name: string;
|
|
24
|
+
content: CredentialContent;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Identifies a credential by auth ID / file name or by `auth_index`. */
|
|
28
|
+
export type CredentialSelector = { name: string; auth_index?: string } | { auth_index: string; name?: string };
|
|
29
|
+
|
|
30
|
+
function toBlob(content: CredentialContent): Blob {
|
|
31
|
+
if (content instanceof Blob) return content;
|
|
32
|
+
if (typeof content === 'string') return new Blob([content], { type: 'application/json' });
|
|
33
|
+
if (content instanceof ArrayBuffer || content instanceof Uint8Array) return new Blob([content], { type: 'application/json' });
|
|
34
|
+
return new Blob([JSON.stringify(content)], { type: 'application/json' });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** `/v8/management/credentials*`: OAuth and API-key credential files. */
|
|
38
|
+
export class CredentialsResource {
|
|
39
|
+
readonly #t: Transport;
|
|
40
|
+
|
|
41
|
+
constructor(transport: Transport) {
|
|
42
|
+
this.#t = transport;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Lists credentials. Passing `page` or `page_size` enables pagination fields in the response. */
|
|
46
|
+
async list(query: ListCredentialsQuery = {}, options: RequestOptions = {}): Promise<ListCredentialsResponse> {
|
|
47
|
+
return (await this.#t.request<ListCredentialsResponse>({ ...options, method: 'GET', path: '/credentials', query: { ...query } })).data;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Finds one credential by auth ID / file name, or returns undefined. */
|
|
51
|
+
async get(name: string, options: RequestOptions = {}): Promise<AuthFileEntry | undefined> {
|
|
52
|
+
const { files } = await this.list({ name }, options);
|
|
53
|
+
return files[0];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Uploads (or overwrites) one credential file. */
|
|
57
|
+
async upload(file: CredentialFile, options: RequestOptions = {}): Promise<UploadCredentialsResponse> {
|
|
58
|
+
const res = await this.#t.request<UploadCredentialsResponse>({
|
|
59
|
+
...options,
|
|
60
|
+
method: 'POST',
|
|
61
|
+
path: '/credentials',
|
|
62
|
+
query: { name: file.name },
|
|
63
|
+
body: toBlob(file.content),
|
|
64
|
+
contentType: 'application/json',
|
|
65
|
+
});
|
|
66
|
+
return res.data;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Uploads several credential files in one multipart request. When some
|
|
71
|
+
* files fail the server answers 207 and this resolves with `status: 'partial'`.
|
|
72
|
+
*/
|
|
73
|
+
async uploadMany(files: readonly CredentialFile[], options: RequestOptions = {}): Promise<UploadCredentialsResponse> {
|
|
74
|
+
if (files.length === 0) throw new TypeError('uploadMany needs at least one file.');
|
|
75
|
+
const form = new FormData();
|
|
76
|
+
for (const file of files) form.append('file', toBlob(file.content), file.name);
|
|
77
|
+
return (await this.#t.request<UploadCredentialsResponse>({ ...options, method: 'POST', path: '/credentials', body: form })).data;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Deletes credentials by auth ID or file name. Partial failures resolve with `status: 'partial'`. */
|
|
81
|
+
async delete(names: string | readonly string[], options: RequestOptions = {}): Promise<DeleteCredentialsResponse> {
|
|
82
|
+
const list = typeof names === 'string' ? [names] : [...names];
|
|
83
|
+
if (list.length === 0) throw new TypeError('delete needs at least one name.');
|
|
84
|
+
return (await this.#t.request<DeleteCredentialsResponse>({ ...options, method: 'DELETE', path: '/credentials', query: { name: list } }))
|
|
85
|
+
.data;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Deletes every `*.json` file in the auth directory. */
|
|
89
|
+
async deleteAll(options: RequestOptions = {}): Promise<{ status: 'ok'; deleted: number }> {
|
|
90
|
+
return (await this.#t.request<{ status: 'ok'; deleted: number }>({ ...options, method: 'DELETE', path: '/credentials', query: { all: 'true' } }))
|
|
91
|
+
.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Models available through one credential. */
|
|
95
|
+
async models(name: string, options: RequestOptions = {}): Promise<CredentialModelsResponse> {
|
|
96
|
+
return (await this.#t.request<CredentialModelsResponse>({ ...options, method: 'GET', path: '/credentials/models', query: { name } })).data;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Downloads a credential file by file name. The content includes tokens. */
|
|
100
|
+
async download(name: string, options: RequestOptions = {}): Promise<Blob> {
|
|
101
|
+
return (await this.#t.request<Blob>({ ...options, method: 'GET', path: '/credentials/download', query: { name }, responseType: 'blob' })).data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Enables or disables a credential. */
|
|
105
|
+
async setStatus(request: PatchCredentialStatusRequest, options: RequestOptions = {}): Promise<PatchCredentialStatusResponse> {
|
|
106
|
+
return (await this.#t.request<PatchCredentialStatusResponse>({ ...options, method: 'PATCH', path: '/credentials/status', json: request })).data;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sets metadata fields on a credential file. Keys may be dotted paths;
|
|
111
|
+
* `headers` is merged and an empty string removes a header.
|
|
112
|
+
*/
|
|
113
|
+
async patchFields(request: PatchCredentialFieldsRequest, options: RequestOptions = {}): Promise<PatchCredentialFieldsResponse> {
|
|
114
|
+
return (await this.#t.request<PatchCredentialFieldsResponse>({ ...options, method: 'PATCH', path: '/credentials/fields', json: request })).data;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Forces a token refresh for one credential. The returned auth record contains raw tokens. */
|
|
118
|
+
async refresh(selector: CredentialSelector, options: RequestOptions = {}): Promise<{ ok: true; auth: CoreAuth }> {
|
|
119
|
+
return (await this.#t.request<{ ok: true; auth: CoreAuth }>({ ...options, method: 'POST', path: '/credentials/refresh', json: selector })).data;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Forces a token refresh for every enabled credential that supports it. */
|
|
123
|
+
async refreshAll(options: RequestOptions = {}): Promise<{ ok: true; results: ForceRefreshResult[] }> {
|
|
124
|
+
return (
|
|
125
|
+
await this.#t.request<{ ok: true; results: ForceRefreshResult[] }>({ ...options, method: 'POST', path: '/credentials/refresh', json: { all: true } })
|
|
126
|
+
).data;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Sign in with ChatGPT binding state of a Codex credential. */
|
|
130
|
+
async siwcStatus(selector: CredentialSelector, options: RequestOptions = {}): Promise<SIWCStatusResponse> {
|
|
131
|
+
return (await this.#t.request<SIWCStatusResponse>({ ...options, method: 'GET', path: '/credentials/siwc/status', query: { ...selector } })).data;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A file returned as an attachment. */
|
|
2
|
+
export interface DownloadedFile {
|
|
3
|
+
/** File name from `Content-Disposition`, when the server sent one. */
|
|
4
|
+
filename: string | undefined;
|
|
5
|
+
blob: Blob;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function attachmentFilename(headers: Headers): string | undefined {
|
|
9
|
+
const disposition = headers.get('Content-Disposition');
|
|
10
|
+
if (!disposition) return undefined;
|
|
11
|
+
const star = /filename\*\s*=\s*(?:UTF-8'')?([^;]+)/i.exec(disposition);
|
|
12
|
+
if (star?.[1]) {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(star[1].trim().replace(/^"|"$/g, ''));
|
|
15
|
+
} catch {
|
|
16
|
+
// Fall through to the plain parameter.
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const plain = /filename\s*=\s*("([^"]*)"|[^;]+)/i.exec(disposition);
|
|
20
|
+
return plain ? (plain[2] ?? plain[1]!).trim() : undefined;
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DeleteLogsResponse, GetLogsQuery, GetLogsResponse, RequestErrorLogsResponse } from '../types/observability.ts';
|
|
2
|
+
import type { RequestOptions, Transport } from '../transport.ts';
|
|
3
|
+
import { attachmentFilename, type DownloadedFile } from './download.ts';
|
|
4
|
+
|
|
5
|
+
/** `/v8/management/observability/logs*`. */
|
|
6
|
+
export class LogsResource {
|
|
7
|
+
readonly #t: Transport;
|
|
8
|
+
|
|
9
|
+
constructor(transport: Transport) {
|
|
10
|
+
this.#t = transport;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Reads the main log. Requires `observability.logs.logging-to-file`.
|
|
15
|
+
* For incremental reads pass the previous `next-cursor` as `cursor`; when
|
|
16
|
+
* the response has `cursor-reset: true`, replace what you have instead of appending.
|
|
17
|
+
*/
|
|
18
|
+
async get(query: GetLogsQuery = {}, options: RequestOptions = {}): Promise<GetLogsResponse> {
|
|
19
|
+
return (await this.#t.request<GetLogsResponse>({ ...options, method: 'GET', path: '/observability/logs', query: { ...query } })).data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Truncates the main log and removes rotated main log files. */
|
|
23
|
+
async clear(options: RequestOptions = {}): Promise<DeleteLogsResponse> {
|
|
24
|
+
return (await this.#t.request<DeleteLogsResponse>({ ...options, method: 'DELETE', path: '/observability/logs' })).data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Lists `error-*.log` request error files, newest first. */
|
|
28
|
+
async listErrorLogs(options: RequestOptions = {}): Promise<RequestErrorLogsResponse> {
|
|
29
|
+
return (await this.#t.request<RequestErrorLogsResponse>({ ...options, method: 'GET', path: '/observability/logs/errors' })).data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Downloads one request error log by file name. */
|
|
33
|
+
async downloadErrorLog(name: string, options: RequestOptions = {}): Promise<DownloadedFile> {
|
|
34
|
+
return this.#download(`/observability/logs/errors/${encodeURIComponent(name)}`, options);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Downloads the request log for a request ID (CPA matches on its last 8 characters). */
|
|
38
|
+
async downloadRequestLog(requestId: string, options: RequestOptions = {}): Promise<DownloadedFile> {
|
|
39
|
+
return this.#download(`/observability/logs/requests/${encodeURIComponent(requestId)}`, options);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async #download(path: string, options: RequestOptions): Promise<DownloadedFile> {
|
|
43
|
+
const res = await this.#t.request<Blob>({ ...options, method: 'GET', path, responseType: 'blob' });
|
|
44
|
+
return { filename: attachmentFilename(res.headers), blob: res.data };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CancelOAuthSessionResponse,
|
|
3
|
+
OAuthAuthUrlRequest,
|
|
4
|
+
OAuthAuthUrlResponse,
|
|
5
|
+
OAuthCallbackRequest,
|
|
6
|
+
OAuthCallbackResponse,
|
|
7
|
+
OAuthStatusResponse,
|
|
8
|
+
SIWCBindResponse,
|
|
9
|
+
VertexImportResponse,
|
|
10
|
+
} from '../types/oauth.ts';
|
|
11
|
+
import type { SIWCStatusResponse } from '../types/credentials.ts';
|
|
12
|
+
import type { RequestOptions, Transport } from '../transport.ts';
|
|
13
|
+
import type { CredentialContent, CredentialSelector } from './credentials.ts';
|
|
14
|
+
|
|
15
|
+
export interface WaitForOAuthOptions extends RequestOptions {
|
|
16
|
+
/** Poll interval. Default 2 000 ms. */
|
|
17
|
+
intervalMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** `/v8/management/oauth/*`: native login flows. */
|
|
21
|
+
export class OAuthResource {
|
|
22
|
+
readonly #t: Transport;
|
|
23
|
+
|
|
24
|
+
constructor(transport: Transport) {
|
|
25
|
+
this.#t = transport;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Starts a login and returns the URL to open. Device flows (xai, meta, kimi)
|
|
30
|
+
* also return `user_code`. Poll {@link status} with the returned `state`.
|
|
31
|
+
*/
|
|
32
|
+
async start(request: OAuthAuthUrlRequest, options: RequestOptions = {}): Promise<OAuthAuthUrlResponse> {
|
|
33
|
+
return (await this.#t.request<OAuthAuthUrlResponse>({ ...options, method: 'POST', path: '/oauth/auth-url', json: request })).data;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Session state: `wait` while pending, `ok` when done, `error` with a message otherwise. */
|
|
37
|
+
async status(state: string, options: RequestOptions = {}): Promise<OAuthStatusResponse> {
|
|
38
|
+
return (await this.#t.request<OAuthStatusResponse>({ ...options, method: 'GET', path: '/oauth/status', query: { state } })).data;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Polls {@link status} until the session leaves `wait`. Abort with `signal`. */
|
|
42
|
+
async waitForCompletion(state: string, options: WaitForOAuthOptions = {}): Promise<Exclude<OAuthStatusResponse, { status: 'wait' }>> {
|
|
43
|
+
const { intervalMs = 2_000, ...requestOptions } = options;
|
|
44
|
+
while (true) {
|
|
45
|
+
const result = await this.status(state, requestOptions);
|
|
46
|
+
if (result.status !== 'wait') return result;
|
|
47
|
+
await new Promise<void>((resolve, reject) => {
|
|
48
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
49
|
+
requestOptions.signal?.addEventListener(
|
|
50
|
+
'abort',
|
|
51
|
+
() => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
reject(requestOptions.signal!.reason);
|
|
54
|
+
},
|
|
55
|
+
{ once: true },
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Cancels a pending session. `cancelled` is false if it was unknown or already finished. */
|
|
62
|
+
async cancel(state: string, options: RequestOptions = {}): Promise<CancelOAuthSessionResponse> {
|
|
63
|
+
return (await this.#t.request<CancelOAuthSessionResponse>({ ...options, method: 'DELETE', path: '/oauth/session', query: { state } })).data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Completes a browser flow by handing over the final redirect URL (or
|
|
68
|
+
* `state` + `code`). Useful when the provider's loopback callback cannot
|
|
69
|
+
* reach the server. This route does not require the management key.
|
|
70
|
+
*/
|
|
71
|
+
async callback(request: OAuthCallbackRequest, options: RequestOptions = {}): Promise<OAuthCallbackResponse> {
|
|
72
|
+
return (await this.#t.request<OAuthCallbackResponse>({ ...options, method: 'POST', path: '/oauth/callback', json: request, anonymous: true }))
|
|
73
|
+
.data;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Imports a Google Cloud service account key as a Vertex credential. */
|
|
77
|
+
async importVertex(
|
|
78
|
+
input: { serviceAccount: CredentialContent; location?: string; fileName?: string },
|
|
79
|
+
options: RequestOptions = {},
|
|
80
|
+
): Promise<VertexImportResponse> {
|
|
81
|
+
const form = new FormData();
|
|
82
|
+
const content = input.serviceAccount;
|
|
83
|
+
const blob =
|
|
84
|
+
content instanceof Blob
|
|
85
|
+
? content
|
|
86
|
+
: new Blob([typeof content === 'string' || content instanceof ArrayBuffer || content instanceof Uint8Array ? content : JSON.stringify(content)], {
|
|
87
|
+
type: 'application/json',
|
|
88
|
+
});
|
|
89
|
+
form.append('file', blob, input.fileName ?? 'service-account.json');
|
|
90
|
+
if (input.location !== undefined) form.append('location', input.location);
|
|
91
|
+
return (
|
|
92
|
+
await this.#t.request<VertexImportResponse>({ ...options, method: 'POST', path: '/oauth/import', query: { provider: 'vertex' }, body: form })
|
|
93
|
+
).data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Starts binding Sign in with ChatGPT to a file-backed Codex OAuth credential. */
|
|
97
|
+
async siwcBind(name: string, options: RequestOptions = {}): Promise<SIWCBindResponse> {
|
|
98
|
+
return (await this.#t.request<SIWCBindResponse>({ ...options, method: 'POST', path: '/oauth/siwc/bind', json: { name } })).data;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Same as `credentials.siwcStatus`, via the oauth route. */
|
|
102
|
+
async siwcStatus(selector: CredentialSelector, options: RequestOptions = {}): Promise<SIWCStatusResponse> {
|
|
103
|
+
return (await this.#t.request<SIWCStatusResponse>({ ...options, method: 'GET', path: '/oauth/siwc/status', query: { ...selector } })).data;
|
|
104
|
+
}
|
|
105
|
+
}
|