@vibesdotdev/client 0.1.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.
Files changed (117) hide show
  1. package/SPEC.md +107 -0
  2. package/dist/factory.d.ts +34 -0
  3. package/dist/factory.d.ts.map +1 -0
  4. package/dist/factory.js +80 -0
  5. package/dist/factory.js.map +1 -0
  6. package/dist/index.d.ts +10 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +6 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lib/client/core.d.ts +8 -0
  11. package/dist/lib/client/core.d.ts.map +1 -0
  12. package/dist/lib/client/core.js +5 -0
  13. package/dist/lib/client/core.js.map +1 -0
  14. package/dist/lib/client/internal/base-client.d.ts +19 -0
  15. package/dist/lib/client/internal/base-client.d.ts.map +1 -0
  16. package/dist/lib/client/internal/base-client.js +92 -0
  17. package/dist/lib/client/internal/base-client.js.map +1 -0
  18. package/dist/lib/client/internal/base-helpers.d.ts +9 -0
  19. package/dist/lib/client/internal/base-helpers.d.ts.map +1 -0
  20. package/dist/lib/client/internal/base-helpers.js +79 -0
  21. package/dist/lib/client/internal/base-helpers.js.map +1 -0
  22. package/dist/lib/client/internal/base-types.d.ts +35 -0
  23. package/dist/lib/client/internal/base-types.d.ts.map +1 -0
  24. package/dist/lib/client/internal/base-types.js +15 -0
  25. package/dist/lib/client/internal/base-types.js.map +1 -0
  26. package/dist/lib/client/internal/endpoint.d.ts +22 -0
  27. package/dist/lib/client/internal/endpoint.d.ts.map +1 -0
  28. package/dist/lib/client/internal/endpoint.js +35 -0
  29. package/dist/lib/client/internal/endpoint.js.map +1 -0
  30. package/dist/lib/client/internal/generator.d.ts +20 -0
  31. package/dist/lib/client/internal/generator.d.ts.map +1 -0
  32. package/dist/lib/client/internal/generator.js +173 -0
  33. package/dist/lib/client/internal/generator.js.map +1 -0
  34. package/dist/lib/client/internal/index.d.ts +5 -0
  35. package/dist/lib/client/internal/index.d.ts.map +1 -0
  36. package/dist/lib/client/internal/index.js +4 -0
  37. package/dist/lib/client/internal/index.js.map +1 -0
  38. package/dist/lib/client/internal/node/http2-fetch.node.d.ts +2 -0
  39. package/dist/lib/client/internal/node/http2-fetch.node.d.ts.map +1 -0
  40. package/dist/lib/client/internal/node/http2-fetch.node.js +131 -0
  41. package/dist/lib/client/internal/node/http2-fetch.node.js.map +1 -0
  42. package/dist/lib/client/internal/request-builder.d.ts +15 -0
  43. package/dist/lib/client/internal/request-builder.d.ts.map +1 -0
  44. package/dist/lib/client/internal/request-builder.js +158 -0
  45. package/dist/lib/client/internal/request-builder.js.map +1 -0
  46. package/dist/lib/client/internal/sse-stream.d.ts +23 -0
  47. package/dist/lib/client/internal/sse-stream.d.ts.map +1 -0
  48. package/dist/lib/client/internal/sse-stream.js +110 -0
  49. package/dist/lib/client/internal/sse-stream.js.map +1 -0
  50. package/dist/lib/client/internal/vibes-client.d.ts +32 -0
  51. package/dist/lib/client/internal/vibes-client.d.ts.map +1 -0
  52. package/dist/lib/client/internal/vibes-client.js +120 -0
  53. package/dist/lib/client/internal/vibes-client.js.map +1 -0
  54. package/dist/lib/client/internal/wrap-fetch.d.ts +6 -0
  55. package/dist/lib/client/internal/wrap-fetch.d.ts.map +1 -0
  56. package/dist/lib/client/internal/wrap-fetch.js +46 -0
  57. package/dist/lib/client/internal/wrap-fetch.js.map +1 -0
  58. package/dist/lib/client/node.d.ts +5 -0
  59. package/dist/lib/client/node.d.ts.map +1 -0
  60. package/dist/lib/client/node.js +33 -0
  61. package/dist/lib/client/node.js.map +1 -0
  62. package/dist/lib/client/types.d.ts +145 -0
  63. package/dist/lib/client/types.d.ts.map +1 -0
  64. package/dist/lib/client/types.js +21 -0
  65. package/dist/lib/client/types.js.map +1 -0
  66. package/dist/plugin.d.ts +19 -0
  67. package/dist/plugin.d.ts.map +1 -0
  68. package/dist/plugin.js +80 -0
  69. package/dist/plugin.js.map +1 -0
  70. package/dist/schemas.d.ts +90 -0
  71. package/dist/schemas.d.ts.map +1 -0
  72. package/dist/schemas.js +9 -0
  73. package/dist/schemas.js.map +1 -0
  74. package/dist/sse-client.d.ts +39 -0
  75. package/dist/sse-client.d.ts.map +1 -0
  76. package/dist/sse-client.js +124 -0
  77. package/dist/sse-client.js.map +1 -0
  78. package/dist/tools/api-request/api-request.descriptor.d.ts +48 -0
  79. package/dist/tools/api-request/api-request.descriptor.d.ts.map +1 -0
  80. package/dist/tools/api-request/api-request.descriptor.js +27 -0
  81. package/dist/tools/api-request/api-request.descriptor.js.map +1 -0
  82. package/dist/tools/api-request/api-request.impl.consumer.d.ts +13 -0
  83. package/dist/tools/api-request/api-request.impl.consumer.d.ts.map +1 -0
  84. package/dist/tools/api-request/api-request.impl.consumer.js +51 -0
  85. package/dist/tools/api-request/api-request.impl.consumer.js.map +1 -0
  86. package/dist/tools/api-request/index.d.ts +5 -0
  87. package/dist/tools/api-request/index.d.ts.map +1 -0
  88. package/dist/tools/api-request/index.js +4 -0
  89. package/dist/tools/api-request/index.js.map +1 -0
  90. package/dist/tools/api-request/schemas/index.d.ts +33 -0
  91. package/dist/tools/api-request/schemas/index.d.ts.map +1 -0
  92. package/dist/tools/api-request/schemas/index.js +24 -0
  93. package/dist/tools/api-request/schemas/index.js.map +1 -0
  94. package/package.json +99 -0
  95. package/src/factory.ts +114 -0
  96. package/src/index.ts +15 -0
  97. package/src/lib/client/core.ts +13 -0
  98. package/src/lib/client/internal/base-client.ts +107 -0
  99. package/src/lib/client/internal/base-helpers.ts +74 -0
  100. package/src/lib/client/internal/base-types.ts +42 -0
  101. package/src/lib/client/internal/endpoint.ts +51 -0
  102. package/src/lib/client/internal/generator.ts +181 -0
  103. package/src/lib/client/internal/index.ts +4 -0
  104. package/src/lib/client/internal/node/http2-fetch.node.ts +138 -0
  105. package/src/lib/client/internal/request-builder.ts +147 -0
  106. package/src/lib/client/internal/sse-stream.ts +130 -0
  107. package/src/lib/client/internal/vibes-client.ts +167 -0
  108. package/src/lib/client/internal/wrap-fetch.ts +59 -0
  109. package/src/lib/client/node.ts +36 -0
  110. package/src/lib/client/types.ts +156 -0
  111. package/src/plugin.ts +104 -0
  112. package/src/schemas.ts +91 -0
  113. package/src/sse-client.ts +155 -0
  114. package/src/tools/api-request/api-request.descriptor.ts +28 -0
  115. package/src/tools/api-request/api-request.impl.consumer.ts +66 -0
  116. package/src/tools/api-request/index.ts +4 -0
  117. package/src/tools/api-request/schemas/index.ts +29 -0
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@vibesdotdev/client",
3
+ "version": "0.1.0",
4
+ "description": "Base API client with runtime kind integration and per-app client factories",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "bun": "./src/index.ts",
12
+ "import": "./dist/index.js",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./plugin": {
16
+ "types": "./dist/plugin.d.ts",
17
+ "bun": "./src/plugin.ts",
18
+ "import": "./dist/plugin.js",
19
+ "default": "./dist/plugin.js"
20
+ },
21
+ "./factory": {
22
+ "types": "./dist/factory.d.ts",
23
+ "bun": "./src/factory.ts",
24
+ "import": "./dist/factory.js",
25
+ "default": "./dist/factory.js"
26
+ },
27
+ "./schemas": {
28
+ "types": "./dist/schemas.d.ts",
29
+ "bun": "./src/schemas.ts",
30
+ "import": "./dist/schemas.js",
31
+ "default": "./dist/schemas.js"
32
+ },
33
+ "./core": {
34
+ "types": "./dist/lib/client/core.d.ts",
35
+ "bun": "./src/lib/client/core.ts",
36
+ "import": "./dist/lib/client/core.js",
37
+ "default": "./dist/lib/client/core.js"
38
+ },
39
+ "./types": {
40
+ "types": "./dist/lib/client/types.d.ts",
41
+ "bun": "./src/lib/client/types.ts",
42
+ "import": "./dist/lib/client/types.js",
43
+ "default": "./dist/lib/client/types.js"
44
+ },
45
+ "./node": {
46
+ "types": "./dist/lib/client/node.d.ts",
47
+ "bun": "./src/lib/client/node.ts",
48
+ "import": "./dist/lib/client/node.js",
49
+ "default": "./dist/lib/client/node.js"
50
+ }
51
+ },
52
+ "publishConfig": {
53
+ "registry": "https://registry.npmjs.org",
54
+ "access": "public"
55
+ },
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/vibesdotdev/monorepo.git",
59
+ "directory": "packages/client"
60
+ },
61
+ "dependencies": {
62
+ "@vibesdotdev/runtime": "0.0.1",
63
+ "@vibesdotdev/logging": "0.0.1",
64
+ "zod": "^4.3.6"
65
+ },
66
+ "scripts": {
67
+ "build": "tsc -p tsconfig.json",
68
+ "test": "bun test",
69
+ "check": "bun --bun tsc -p tsconfig.json --noEmit"
70
+ },
71
+ "license": "MIT",
72
+ "files": [
73
+ "dist",
74
+ "src",
75
+ "bin",
76
+ "README.md",
77
+ "SPEC.md",
78
+ "LICENSE",
79
+ "!src/**/__tests__/**",
80
+ "!src/**/__stubs__/**",
81
+ "!src/**/*.test.ts",
82
+ "!src/**/*.test.tsx",
83
+ "!src/**/*.spec.ts",
84
+ "!src/**/*.spec.tsx",
85
+ "!dist/**/__tests__/**",
86
+ "!dist/**/__stubs__/**",
87
+ "!dist/**/*.test.js",
88
+ "!dist/**/*.test.js.map",
89
+ "!dist/**/*.test.d.ts",
90
+ "!dist/**/*.test.d.ts.map",
91
+ "!dist/**/*.spec.js",
92
+ "!dist/**/*.spec.js.map",
93
+ "!dist/**/*.spec.d.ts",
94
+ "!dist/**/*.spec.d.ts.map"
95
+ ],
96
+ "vibes": {
97
+ "visibility": "public-framework"
98
+ }
99
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,114 @@
1
+ import { VibesClient } from './lib/client/core.ts';
2
+ import type { ClientConfig, AuthConfig } from './lib/client/types.ts';
3
+ import type {
4
+ ApiClientImplementation,
5
+ ApiRequestOptions,
6
+ ApiStreamEvent,
7
+ ApiStreamOptions
8
+ } from './schemas.ts';
9
+
10
+ export interface ApiClientConfig {
11
+ /** Unique identifier for this client instance */
12
+ id?: string;
13
+ /** Base URL for API requests */
14
+ baseUrl?: string;
15
+ /** Authentication configuration */
16
+ auth?: AuthConfig;
17
+ /** Custom fetch implementation */
18
+ fetch?: typeof fetch;
19
+ /** Request timeout in milliseconds */
20
+ timeout?: number;
21
+ /** Enable debug logging */
22
+ debug?: boolean;
23
+ /** Custom headers applied to all requests */
24
+ headers?: Record<string, string>;
25
+ }
26
+
27
+ /**
28
+ * Wrap a VibesClient to satisfy the ApiClientImplementation interface.
29
+ */
30
+ function wrapVibesClient(client: VibesClient): ApiClientImplementation {
31
+ return {
32
+ get<T = unknown>(endpoint: string, options?: ApiRequestOptions): Promise<T> {
33
+ return client.get<T>(endpoint, { method: 'GET', path: endpoint, ...options });
34
+ },
35
+ post<T = unknown>(endpoint: string, body?: unknown, options?: ApiRequestOptions): Promise<T> {
36
+ return client.post<T>(endpoint, body, { method: 'POST', path: endpoint, ...options });
37
+ },
38
+ put<T = unknown>(endpoint: string, body?: unknown, options?: ApiRequestOptions): Promise<T> {
39
+ return client.put<T>(endpoint, body, { method: 'PUT', path: endpoint, ...options });
40
+ },
41
+ patch<T = unknown>(endpoint: string, body?: unknown, options?: ApiRequestOptions): Promise<T> {
42
+ return client.request<T>(endpoint, { method: 'PATCH', body, ...options });
43
+ },
44
+ delete<T = unknown>(endpoint: string, options?: ApiRequestOptions): Promise<T> {
45
+ return client.delete<T>(endpoint, { method: 'DELETE', path: endpoint, ...options });
46
+ },
47
+ request<T = unknown>(
48
+ endpoint: string,
49
+ options?: ApiRequestOptions & { method?: string; body?: unknown }
50
+ ): Promise<T> {
51
+ return client.request<T>(endpoint, {
52
+ method: options?.method ?? 'GET',
53
+ ...options
54
+ });
55
+ },
56
+ stream<T = unknown>(
57
+ endpoint: string,
58
+ options?: ApiStreamOptions
59
+ ): AsyncIterableIterator<ApiStreamEvent<T>> {
60
+ return client.stream<T>(endpoint, {
61
+ method: options?.method ?? 'POST',
62
+ body: options?.body,
63
+ headers: options?.headers,
64
+ params: options?.params,
65
+ query: options?.query,
66
+ timeout: options?.timeout
67
+ }) as AsyncIterableIterator<ApiStreamEvent<T>>;
68
+ }
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Create an API client instance.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const client = vibesApiClient({ baseUrl: 'https://api.vibes.dev' });
78
+ * const data = await client.get('/users');
79
+ * ```
80
+ */
81
+ export function vibesApiClient(config: ApiClientConfig = {}): ApiClientImplementation {
82
+ const clientConfig: ClientConfig = {
83
+ baseUrl: config.baseUrl,
84
+ auth: config.auth,
85
+ fetch: config.fetch,
86
+ timeout: config.timeout,
87
+ debug: config.debug
88
+ };
89
+
90
+ if (config.headers && Object.keys(config.headers).length > 0) {
91
+ const headers = config.headers;
92
+ clientConfig.hooks = {
93
+ beforeRequest: async (request: Request): Promise<Request> => {
94
+ const updated = new Headers(request.headers);
95
+ for (const [key, value] of Object.entries(headers)) {
96
+ if (value !== undefined) {
97
+ updated.set(key, value);
98
+ }
99
+ }
100
+ return new Request(request, { headers: updated });
101
+ }
102
+ };
103
+ }
104
+
105
+ const vibes = new VibesClient(clientConfig);
106
+ return wrapVibesClient(vibes);
107
+ }
108
+
109
+ /**
110
+ * Create an API client from an existing VibesClient instance.
111
+ */
112
+ export function fromVibesClient(client: VibesClient): ApiClientImplementation {
113
+ return wrapVibesClient(client);
114
+ }
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { vibesApiClient, fromVibesClient } from './factory.ts';
2
+ export type { ApiClientConfig } from './factory.ts';
3
+ export type {
4
+ ApiClientDescriptor,
5
+ ApiClientImplementation,
6
+ ApiRequestOptions,
7
+ ApiStreamOptions,
8
+ ApiStreamEvent
9
+ } from './schemas.ts';
10
+ export { ApiClientDescriptorSchema } from './schemas.ts';
11
+ export { default as clientPlugin } from './plugin.ts';
12
+ export { SSEClient, createSSEClient } from './sse-client.ts';
13
+ export type { SSEClientOptions, SSEConnectionStatus } from './sse-client.ts';
14
+ export { VibesClient } from './lib/client/core.ts';
15
+ export type { ClientConfig, AuthConfig, ClientHooks, ClientError } from './lib/client/types.ts';
@@ -0,0 +1,13 @@
1
+ export { VibesClient } from './internal/vibes-client.ts';
2
+ export type { SSEEvent } from './internal/sse-stream.ts';
3
+ export type {
4
+ ClientConfig,
5
+ AuthConfig,
6
+ ClientHooks,
7
+ ClientUser,
8
+ RequestOptions
9
+ } from './types.ts';
10
+ export { ClientError } from './types.ts';
11
+ export { BaseAPIClient } from './internal/base-client.ts';
12
+ export type { BaseAPIConfig, EndpointOptions } from './internal/base-types.ts';
13
+ export { Endpoint } from './internal/endpoint.ts';
@@ -0,0 +1,107 @@
1
+ import { type BaseAPIConfig, type EndpointOptions, APIError } from './base-types.ts';
2
+ import { buildHeaders, buildUrl, createApiError } from './base-helpers.ts';
3
+ import { Endpoint } from './endpoint.ts';
4
+
5
+ export abstract class BaseAPIClient {
6
+ protected config: Required<Omit<BaseAPIConfig, 'auth'>> & {
7
+ auth: BaseAPIConfig['auth'];
8
+ };
9
+ private abortControllers = new Set<AbortController>();
10
+
11
+ constructor(config: BaseAPIConfig) {
12
+ const merged: Required<Omit<BaseAPIConfig, 'auth'>> & {
13
+ auth: BaseAPIConfig['auth'];
14
+ } = {
15
+ baseUrl: config.baseUrl,
16
+ fetch: (config.fetch || globalThis.fetch?.bind(globalThis)) as typeof fetch,
17
+ headers: config.headers || {},
18
+ timeout: config.timeout || 30000,
19
+ debug: config.debug || false,
20
+ hooks: config.hooks || {},
21
+ auth: config.auth ?? null
22
+ };
23
+ if (!merged.fetch)
24
+ throw new Error('Fetch is not available. Please provide a fetch implementation.');
25
+ this.config = merged;
26
+ }
27
+
28
+ protected endpoint(path: string): Endpoint {
29
+ const cleanPath = path.startsWith('/') ? path : `/${path}`;
30
+ return new Endpoint(this, cleanPath);
31
+ }
32
+
33
+ async request<T = unknown>(
34
+ request: {
35
+ method: string;
36
+ path: string;
37
+ body?: unknown;
38
+ } & EndpointOptions<T>
39
+ ): Promise<T> {
40
+ const url = buildUrl(this.config.baseUrl, request.path, request.query);
41
+ const controller = new AbortController();
42
+ this.abortControllers.add(controller);
43
+ try {
44
+ const authForHeaders = this.config.auth ?? { type: 'none' as const };
45
+ const headers = await buildHeaders({
46
+ auth: authForHeaders,
47
+ headers: { ...this.config.headers, ...request.headers },
48
+ includeJsonDefaults: request.body !== undefined
49
+ });
50
+ const init: RequestInit = {
51
+ method: request.method,
52
+ headers,
53
+ signal: controller.signal
54
+ };
55
+ const timeout = setTimeout(() => controller.abort(), request.timeout ?? this.config.timeout);
56
+ if (request.body !== undefined) init.body = JSON.stringify(request.body);
57
+ let req = new Request(url.toString(), init);
58
+ if (this.config.hooks?.beforeRequest) req = await this.config.hooks.beforeRequest(req);
59
+ const response = await this.config.fetch(req, init);
60
+ clearTimeout(timeout);
61
+ const finalResponse = this.config.hooks?.afterResponse
62
+ ? await this.config.hooks.afterResponse(response)
63
+ : response;
64
+ if (!finalResponse.ok) {
65
+ const error = await createApiError(finalResponse, req);
66
+ if (this.config.hooks?.onError) await this.config.hooks.onError(error);
67
+ throw error;
68
+ }
69
+ let data: unknown;
70
+ if (finalResponse.status === 204 || finalResponse.headers.get('Content-Length') === '0')
71
+ data = undefined;
72
+ else if (finalResponse.headers.get('Content-Type')?.includes('application/json'))
73
+ data = await finalResponse.json();
74
+ else data = await finalResponse.text();
75
+ if (request.transform) data = request.transform(data);
76
+ if (request.schema) {
77
+ const result = request.schema.safeParse(data);
78
+ if (!result.success) {
79
+ throw new APIError(
80
+ `Response validation failed: ${result.error.message}`,
81
+ 422,
82
+ finalResponse,
83
+ req,
84
+ data
85
+ );
86
+ }
87
+ return result.data;
88
+ }
89
+ return data as T;
90
+ } finally {
91
+ this.abortControllers.delete(controller);
92
+ }
93
+ }
94
+
95
+ cancelAll(): void {
96
+ for (const controller of this.abortControllers) controller.abort();
97
+ this.abortControllers.clear();
98
+ }
99
+
100
+ setAuth(auth: BaseAPIConfig['auth']): void {
101
+ this.config.auth = auth ?? null;
102
+ }
103
+
104
+ updateConfig(config: Partial<BaseAPIConfig>): void {
105
+ Object.assign(this.config, config);
106
+ }
107
+ }
@@ -0,0 +1,74 @@
1
+ import { type BaseAPIConfig, type EndpointOptions, APIError } from './base-types.ts';
2
+
3
+ export function buildUrl(baseUrl: string, path: string, query?: EndpointOptions['query']): URL {
4
+ const url = new URL(path, baseUrl);
5
+ if (query) {
6
+ for (const [key, value] of Object.entries(query)) {
7
+ if (value === undefined) continue;
8
+ if (Array.isArray(value)) value.forEach((v) => url.searchParams.append(key, String(v)));
9
+ else url.searchParams.set(key, String(value));
10
+ }
11
+ }
12
+ return url;
13
+ }
14
+
15
+ export async function buildHeaders(args: {
16
+ auth: NonNullable<BaseAPIConfig['auth']>;
17
+ headers: Record<string, string>;
18
+ includeJsonDefaults: boolean;
19
+ }): Promise<Headers> {
20
+ const requestHeaders = new Headers(args.headers);
21
+ if (args.includeJsonDefaults && !requestHeaders.has('Content-Type')) {
22
+ requestHeaders.set('Content-Type', 'application/json');
23
+ }
24
+ if (!requestHeaders.has('Accept')) requestHeaders.set('Accept', 'application/json');
25
+
26
+ const auth = args.auth;
27
+ if (auth) {
28
+ const token = auth.credentials || (await auth.provider?.());
29
+ if (token) {
30
+ switch (auth.type) {
31
+ case 'bearer':
32
+ requestHeaders.set('Authorization', `Bearer ${token}`);
33
+ break;
34
+ case 'apiKey': {
35
+ const headerName = auth.headerName || 'X-API-Key';
36
+ requestHeaders.set(headerName, token);
37
+ break;
38
+ }
39
+ case 'basic':
40
+ requestHeaders.set('Authorization', `Basic ${token}`);
41
+ break;
42
+ case 'custom':
43
+ if (auth.headerName && auth.prefix) {
44
+ requestHeaders.set(auth.headerName, `${auth.prefix} ${token}`);
45
+ }
46
+ break;
47
+ }
48
+ }
49
+ }
50
+ return requestHeaders;
51
+ }
52
+
53
+ export async function createApiError(response: Response, request: Request): Promise<APIError> {
54
+ let message = `${response.status} ${response.statusText}`;
55
+ let body: unknown;
56
+ try {
57
+ const contentType = response.headers.get('Content-Type');
58
+ if (contentType?.includes('application/json')) {
59
+ body = await response.json();
60
+ if (typeof body === 'object' && body !== null) {
61
+ const errorBody = body as { message?: unknown; error?: unknown };
62
+ if (typeof errorBody.message === 'string') message = errorBody.message;
63
+ else if (typeof errorBody.error === 'string') message = errorBody.error;
64
+ else if (errorBody.error !== undefined) message = JSON.stringify(errorBody.error);
65
+ }
66
+ } else {
67
+ const text = await response.text();
68
+ if (text) { message = text; body = text; }
69
+ }
70
+ } catch {
71
+ // ignore parsing errors
72
+ }
73
+ return new APIError(message, response.status, response, request, body);
74
+ }
@@ -0,0 +1,42 @@
1
+ import type { z } from 'zod/v4';
2
+
3
+ export interface BaseAPIConfig {
4
+ baseUrl: string;
5
+ fetch?: typeof fetch;
6
+ headers?: Record<string, string>;
7
+ timeout?: number;
8
+ auth?: {
9
+ type: 'none' | 'bearer' | 'apiKey' | 'basic' | 'custom';
10
+ credentials?: string;
11
+ headerName?: string;
12
+ prefix?: string;
13
+ provider?: () => Promise<string | null>;
14
+ } | null;
15
+ debug?: boolean;
16
+ hooks?: {
17
+ beforeRequest?: (request: Request) => Promise<Request> | Request;
18
+ afterResponse?: (response: Response) => Promise<Response> | Response;
19
+ onError?: (error: APIError) => Promise<void> | void;
20
+ };
21
+ }
22
+
23
+ export class APIError extends Error {
24
+ constructor(
25
+ message: string,
26
+ public readonly status: number,
27
+ public readonly response?: Response,
28
+ public readonly request?: Request,
29
+ public readonly body?: unknown
30
+ ) {
31
+ super(message);
32
+ this.name = 'APIError';
33
+ }
34
+ }
35
+
36
+ export interface EndpointOptions<T = unknown> {
37
+ headers?: Record<string, string>;
38
+ query?: Record<string, string | number | boolean | string[] | undefined>;
39
+ timeout?: number;
40
+ schema?: z.ZodSchema<T>;
41
+ transform?: (data: unknown) => T;
42
+ }
@@ -0,0 +1,51 @@
1
+ import type { EndpointOptions } from './base-types.ts';
2
+
3
+ type RequestExecutor = {
4
+ request<T = unknown>(
5
+ options: {
6
+ method: string;
7
+ path: string;
8
+ body?: unknown;
9
+ } & EndpointOptions<T>
10
+ ): Promise<T>;
11
+ };
12
+
13
+ export class Endpoint {
14
+ constructor(
15
+ private client: RequestExecutor,
16
+ private path: string
17
+ ) {}
18
+
19
+ endpoint(subPath: string): Endpoint {
20
+ const cleanPath = subPath.startsWith('/') ? subPath : `/${subPath}`;
21
+ return new Endpoint(this.client, `${this.path}${cleanPath}`);
22
+ }
23
+
24
+ methods<T extends Record<string, (...args: never[]) => unknown>>(methods: T): T {
25
+ const bound = {} as { [K in keyof T]: T[K] };
26
+ for (const key of Object.keys(methods) as Array<keyof T>) {
27
+ bound[key] = methods[key].bind(this) as T[typeof key];
28
+ }
29
+ return bound as T;
30
+ }
31
+
32
+ get<T = unknown>(path = '', options?: EndpointOptions<T>): Promise<T> {
33
+ return this.client.request<T>({ method: 'GET', path: `${this.path}${path}`, ...options });
34
+ }
35
+
36
+ post<T = unknown>(path = '', body?: unknown, options?: EndpointOptions<T>): Promise<T> {
37
+ return this.client.request<T>({ method: 'POST', path: `${this.path}${path}`, body, ...options });
38
+ }
39
+
40
+ put<T = unknown>(path = '', body?: unknown, options?: EndpointOptions<T>): Promise<T> {
41
+ return this.client.request<T>({ method: 'PUT', path: `${this.path}${path}`, body, ...options });
42
+ }
43
+
44
+ patch<T = unknown>(path = '', body?: unknown, options?: EndpointOptions<T>): Promise<T> {
45
+ return this.client.request<T>({ method: 'PATCH', path: `${this.path}${path}`, body, ...options });
46
+ }
47
+
48
+ delete<T = unknown>(path = '', options?: EndpointOptions<T>): Promise<T> {
49
+ return this.client.request<T>({ method: 'DELETE', path: `${this.path}${path}`, ...options });
50
+ }
51
+ }