@hardlydifficult/rest-client 1.0.3 → 1.0.4

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 (2) hide show
  1. package/README.md +361 -0
  2. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,361 @@
1
+ # @hardlydifficult/rest-client
2
+
3
+ A typed REST client library with declarative operation definitions, OAuth2/bearer/custom token authentication, automatic retry logic, Zod-based param validation, and structured error handling.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @hardlydifficult/rest-client
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```typescript
14
+ import { RestClient, defineOperation } from "@hardlydifficult/rest-client";
15
+ import { z } from "zod";
16
+
17
+ // Define a REST operation
18
+ const GetUser = defineOperation<{ id: string }, User>({
19
+ params: z.object({ id: z.string() }),
20
+ method: "GET",
21
+ url: (p, base) => `${base}/users/${p.id}`,
22
+ });
23
+
24
+ // Create a client with OAuth2 auth
25
+ class MyApi extends RestClient {
26
+ getUser = this.bind(GetUser);
27
+ }
28
+
29
+ const api = new MyApi({
30
+ baseUrl: "https://api.example.com",
31
+ auth: {
32
+ type: "oauth2",
33
+ tokenUrl: "https://auth.example.com/token",
34
+ clientId: "client-id",
35
+ clientSecret: "secret",
36
+ },
37
+ });
38
+
39
+ // Make a request
40
+ const user = await api.getUser({ id: "123" });
41
+ // => User object with id, name, etc.
42
+ ```
43
+
44
+ ## Core Classes
45
+
46
+ ### RestClient
47
+
48
+ Opinionated REST client with OAuth2, retries, and declarative operations.
49
+
50
+ #### Constructor
51
+
52
+ ```typescript
53
+ new RestClient(config: RestClientConfig)
54
+ ```
55
+
56
+ | Property | Type | Description |
57
+ |----------|------|-------------|
58
+ | `baseUrl` | `string` | Base URL for all requests (required) |
59
+ | `auth` | `AuthConfig?` | Authentication configuration (optional, default: `{ type: "none" }`) |
60
+ | `retry` | `RetryConfig?` | Retry settings (optional, default: `{ maxAttempts: 3, delayMs: 6000 }`) |
61
+ | `defaultHeaders` | `Record<string, string>?` | Default HTTP headers (optional) |
62
+ | `logger` | `RestClientLogger?` | Logger instance (optional) |
63
+
64
+ #### Methods
65
+
66
+ ```typescript
67
+ bind<Params, Response>(
68
+ config: OperationConfig<Params, Response>
69
+ ): (params: Params) => Promise<Response>
70
+ ```
71
+
72
+ Bind a declarative operation to the client.
73
+
74
+ ```typescript
75
+ await authenticate(): Promise<string>
76
+ ```
77
+
78
+ Authenticate and set the bearer token.
79
+
80
+ ```typescript
81
+ clearToken(): void
82
+ ```
83
+
84
+ Clear the cached token, forcing re-authentication on next request.
85
+
86
+ ```typescript
87
+ getBaseUrl(): string
88
+ ```
89
+
90
+ Get the base URL.
91
+
92
+ ```typescript
93
+ getLogger(): RestClientLogger | undefined
94
+ ```
95
+
96
+ Get the logger instance.
97
+
98
+ ```typescript
99
+ await get<T>(url: string): Promise<T>
100
+ ```
101
+
102
+ Perform a GET request.
103
+
104
+ ```typescript
105
+ await post<T>(url: string, data?: unknown): Promise<T>
106
+ ```
107
+
108
+ Perform a POST request.
109
+
110
+ ```typescript
111
+ await delete<T>(url: string): Promise<T>
112
+ ```
113
+
114
+ Perform a DELETE request.
115
+
116
+ ```typescript
117
+ await patch<T>(url: string, data?: unknown): Promise<T>
118
+ ```
119
+
120
+ Perform a PATCH request.
121
+
122
+ ```typescript
123
+ await put<T>(url: string, data?: unknown): Promise<T>
124
+ ```
125
+
126
+ Perform a PUT request.
127
+
128
+ ```typescript
129
+ getTokenExpiryTime(): number | null
130
+ ```
131
+
132
+ Token expiry timestamp (ms since epoch), or `null` if unavailable.
133
+
134
+ ```typescript
135
+ getTokenIssuedAt(): number | null
136
+ ```
137
+
138
+ Token issue timestamp (ms since epoch), or `null` if unavailable.
139
+
140
+ ```typescript
141
+ getTokenLifetimeMs(): number | null
142
+ ```
143
+
144
+ Token lifetime in milliseconds, or `null` if unavailable.
145
+
146
+ ### HttpClient
147
+
148
+ HTTP client with automatic retries and structured error formatting.
149
+
150
+ #### Constructor
151
+
152
+ ```typescript
153
+ new HttpClient(options?: {
154
+ logger?: RestClientLogger;
155
+ retry?: RetryConfig;
156
+ defaultHeaders?: Record<string, string>;
157
+ })
158
+ ```
159
+
160
+ #### Methods
161
+
162
+ ```typescript
163
+ setBearerToken(token: string): void
164
+ ```
165
+
166
+ Set the Authorization header with Bearer token.
167
+
168
+ ```typescript
169
+ clearBearerToken(): void
170
+ ```
171
+
172
+ Clear the Authorization header.
173
+
174
+ ```typescript
175
+ await get<T>(url: string): Promise<T>
176
+ ```
177
+
178
+ Perform a GET request.
179
+
180
+ ```typescript
181
+ await post<T>(url: string, data?: unknown): Promise<T>
182
+ ```
183
+
184
+ Perform a POST request.
185
+
186
+ ```typescript
187
+ await delete<T>(url: string): Promise<T>
188
+ ```
189
+
190
+ Perform a DELETE request.
191
+
192
+ ```typescript
193
+ await patch<T>(url: string, data?: unknown): Promise<T>
194
+ ```
195
+
196
+ Perform a PATCH request.
197
+
198
+ ```typescript
199
+ await put<T>(url: string, data?: unknown): Promise<T>
200
+ ```
201
+
202
+ Perform a PUT request.
203
+
204
+ #### Retry Behavior
205
+
206
+ - Automatically retries on 5xx errors and network failures
207
+ - Default: 3 attempts with 6000ms delay between retries
208
+ - Custom retryable status codes or conditions可通过 `retryableStatuses` and `isRetryable`
209
+
210
+ ### AuthenticationManager
211
+
212
+ Manages token lifecycle for all supported auth strategies.
213
+
214
+ #### Constructor
215
+
216
+ ```typescript
217
+ new AuthenticationManager(config: AuthConfig, logger?: RestClientLogger)
218
+ ```
219
+
220
+ #### Methods
221
+
222
+ ```typescript
223
+ await authenticate(): Promise<string>
224
+ ```
225
+
226
+ Authenticate and return the bearer token.
227
+
228
+ ```typescript
229
+ clearToken(): void
230
+ ```
231
+
232
+ Clear cached token and timing info.
233
+
234
+ ```typescript
235
+ getTokenExpiryTime(): number | null
236
+ ```
237
+
238
+ Token expiry timestamp (ms since epoch), or `null` if unavailable.
239
+
240
+ ```typescript
241
+ getTokenIssuedAt(): number | null
242
+ ```
243
+
244
+ Token issue timestamp (ms since epoch), or `null` if unavailable.
245
+
246
+ ```typescript
247
+ getTokenLifetimeMs(): number | null
248
+ ```
249
+
250
+ Token lifetime in milliseconds, or `null` if unavailable.
251
+
252
+ #### Supported Auth Types
253
+
254
+ | Type | Config | Description |
255
+ |------|--------|-------------|
256
+ | `bearer` | `{ type: "bearer", token: string }` | Static bearer token |
257
+ | `generator` | `{ type: "generator", generate: () => Promise<string>, cacheDurationMs?: number }` | Async token generator with optional cache |
258
+ | `oauth2` | `{ type: "oauth2", tokenUrl: string, clientId: string, clientSecret?: string, audience?: string, scope?: string, grantType?: "client_credentials" \| "password", username?: string, password?: string }` | OAuth2 client credentials or password grant |
259
+ | `none` | `{ type: "none" }` | No authentication (default) |
260
+
261
+ ### defineOperation
262
+
263
+ Define a REST operation declaratively.
264
+
265
+ ```typescript
266
+ const GetUser = defineOperation<{ id: string }, User>({
267
+ params: z.object({ id: z.string() }),
268
+ method: "GET",
269
+ url: (p, base) => `${base}/users/${p.id}`,
270
+ });
271
+ ```
272
+
273
+ #### Config Fields
274
+
275
+ | Field | Type | Required | Description |
276
+ |-------|------|----------|-------------|
277
+ | `params` | `z.ZodType<Params>` | Yes | Zod schema for request parameters |
278
+ | `method` | `HttpMethod` | Yes | HTTP method: `"GET"`, `"POST"`, `"DELETE"`, `"PATCH"`, `"PUT"` |
279
+ | `url` | `(params: Params, baseUrl: string) => string` | Yes | URL builder function |
280
+ | `body` | `(params: Params) => Record<string, unknown> \| string \| undefined` | No | Optional body builder |
281
+ | `transform` | `(response: Response) => Response` | No | Optional response transformer |
282
+
283
+ ### validateParams
284
+
285
+ Validate params against a Zod schema, throwing `ValidationError` on failure.
286
+
287
+ ```typescript
288
+ validateParams<T>(params: T, schema: z.ZodType<T>): T
289
+ ```
290
+
291
+ ```typescript
292
+ const validated = validateParams({ id: "123" }, z.object({ id: z.string() }));
293
+ // => { id: "123" }
294
+ ```
295
+
296
+ ## Error Handling
297
+
298
+ All errors extend `RestClientError` and include a `code` and optional `context`.
299
+
300
+ | Error Type | Code | Description |
301
+ |------------|------|-------------|
302
+ | `ConfigurationError` | `"CONFIGURATION_ERROR"` | Invalid client configuration (e.g., missing `baseUrl`) |
303
+ | `AuthenticationError` | `"AUTHENTICATION_ERROR"` | Authentication failure |
304
+ | `HttpError` | `"HTTP_ERROR"` | HTTP error with status, statusText, and response data |
305
+ | `ValidationError` | `"VALIDATION_ERROR"` | Parameter validation failure (via Zod) |
306
+ | `NetworkError` | `"NETWORK_ERROR"` | Network-level errors (e.g., DNS failure, connection refused) |
307
+
308
+ Example:
309
+
310
+ ```typescript
311
+ try {
312
+ await api.getUser({ id: "123" });
313
+ } catch (error) {
314
+ if (error instanceof HttpError) {
315
+ console.log(error.status, error.message);
316
+ }
317
+ }
318
+ ```
319
+
320
+ ## Types
321
+
322
+ ### AuthConfig
323
+
324
+ Union type of auth configurations: `OAuth2Auth | BearerAuth | TokenGeneratorAuth | NoAuth`
325
+
326
+ ### RetryConfig
327
+
328
+ | Property | Type | Default | Description |
329
+ |----------|------|---------|-------------|
330
+ | `maxAttempts` | `number` | `3` | Number of retries after the initial attempt |
331
+ | `delayMs` | `number` | `6000` | Delay between retries in milliseconds |
332
+ | `retryableStatuses` | `readonly number[]?` | `undefined` | Additional HTTP status codes to retry on |
333
+ | `isRetryable` | `(status: number, body: Record<string, unknown>) => boolean?` | `undefined` | Custom retryable check |
334
+
335
+ ### RestClientLogger
336
+
337
+ Minimal logger interface (compatible with `@hardlydifficult/logger`):
338
+
339
+ ```typescript
340
+ interface RestClientLogger {
341
+ debug?(message: string, context?: Record<string, unknown>): void;
342
+ info?(message: string, context?: Record<string, unknown>): void;
343
+ warn?(message: string, context?: Record<string, unknown>): void;
344
+ error?(message: string, context?: Record<string, unknown>): void;
345
+ }
346
+ ```
347
+
348
+ All methods are optional — only implement what you need.
349
+
350
+ ## Appendix
351
+
352
+ ### Retryable Status Codes
353
+
354
+ By default, the client retries on:
355
+
356
+ - All 5xx status codes (500–599)
357
+ - Network errors (no response)
358
+ - Custom status codes via `retryableStatuses`
359
+ - Custom conditions via `isRetryable`
360
+
361
+ OAuth2 token refresh is automatically triggered when the token is near expiry (half its lifetime or 5 minutes, whichever is smaller).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/rest-client",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [