@devopsplaybook.io/common-utils 1.10.1-beta.23.b1906d7 → 1.11.0-beta.24.d8f9aef

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 (69) hide show
  1. package/README.md +73 -27
  2. package/dist/src/ConfigBase.d.ts +31 -0
  3. package/dist/src/ConfigBase.js +58 -16
  4. package/dist/src/DbUtils.d.ts +20 -3
  5. package/dist/src/DbUtils.js +93 -2
  6. package/dist/src/DbUtilsNoTelemetry.d.ts +4 -1
  7. package/dist/src/DbUtilsNoTelemetry.js +62 -6
  8. package/dist/src/PostgresDbUtils.d.ts +41 -10
  9. package/dist/src/PostgresDbUtils.js +357 -304
  10. package/dist/src/SqlDbUtils.d.ts +12 -4
  11. package/dist/src/SqlDbUtils.js +76 -30
  12. package/dist/src/users/Auth.d.ts +11 -1
  13. package/dist/src/users/Auth.js +160 -44
  14. package/dist/src/users/User.d.ts +10 -0
  15. package/dist/src/users/User.js +30 -10
  16. package/dist/src/users/UserApiToken.d.ts +4 -0
  17. package/dist/src/users/UserApiToken.js +11 -0
  18. package/dist/src/users/UsersApiTokensData.d.ts +12 -0
  19. package/dist/src/users/UsersApiTokensData.js +130 -33
  20. package/dist/src/users/UsersData.d.ts +20 -1
  21. package/dist/src/users/UsersData.js +150 -52
  22. package/dist/src/users/UsersRoutes.js +178 -61
  23. package/dist/src/users/index.d.ts +8 -0
  24. package/dist/src/users/index.js +24 -0
  25. package/package.json +58 -1
  26. package/.github/workflows/main-build.yml +0 -18
  27. package/.github/workflows/pr-check.yml +0 -27
  28. package/.github/workflows/reusable-merge-build.yml +0 -197
  29. package/.github/workflows/reusable-npm-merge.yml +0 -135
  30. package/.github/workflows/reusable-npm-pr.yml +0 -183
  31. package/.github/workflows/reusable-npm-upgrade.yml +0 -92
  32. package/.github/workflows/reusable-pr-verify.yml +0 -181
  33. package/AGENTS.md +0 -105
  34. package/index.ts +0 -18
  35. package/jest.config.js +0 -17
  36. package/prettierrc.json +0 -5
  37. package/src/ConfigBase.spec.ts +0 -108
  38. package/src/ConfigBase.ts +0 -297
  39. package/src/DbUtils.spec.ts +0 -23
  40. package/src/DbUtils.ts +0 -116
  41. package/src/DbUtilsNoTelemetry.spec.ts +0 -168
  42. package/src/DbUtilsNoTelemetry.ts +0 -117
  43. package/src/LLM.spec.ts +0 -303
  44. package/src/LLM.ts +0 -204
  45. package/src/Notifications.spec.ts +0 -265
  46. package/src/Notifications.ts +0 -201
  47. package/src/OTelContext.spec.ts +0 -58
  48. package/src/OTelContext.ts +0 -63
  49. package/src/PostgresDbUtils.spec.ts +0 -153
  50. package/src/PostgresDbUtils.ts +0 -666
  51. package/src/SqlDbUtils.spec.ts +0 -108
  52. package/src/SqlDbUtils.ts +0 -152
  53. package/src/SystemCommand.spec.ts +0 -18
  54. package/src/SystemCommand.ts +0 -23
  55. package/src/Timeout.spec.ts +0 -18
  56. package/src/Timeout.ts +0 -12
  57. package/src/users/Auth.spec.ts +0 -268
  58. package/src/users/Auth.ts +0 -202
  59. package/src/users/User.ts +0 -75
  60. package/src/users/UserApiToken.ts +0 -55
  61. package/src/users/UserPassword.spec.ts +0 -28
  62. package/src/users/UserPassword.ts +0 -20
  63. package/src/users/UserSession.ts +0 -9
  64. package/src/users/UsersApiTokensData.spec.ts +0 -158
  65. package/src/users/UsersApiTokensData.ts +0 -125
  66. package/src/users/UsersData.ts +0 -141
  67. package/src/users/UsersRoutes.ts +0 -374
  68. package/tsconfig.json +0 -15
  69. package/tsconfig.spec.json +0 -8
@@ -1,117 +0,0 @@
1
- import { StandardLogger, ModuleLogger } from "@devopsplaybook.io/otel-utils";
2
- import {
3
- DbUtilsGetDatabase,
4
- DbUtilsGetType,
5
- convertToPostgresPlaceholders,
6
- } from "./DbUtils";
7
-
8
- let logger: ModuleLogger;
9
-
10
- /**
11
- * Injects the OTel logger instance used by no-telemetry DB operations.
12
- * Must be called once at startup.
13
- */
14
- export function DbUtilsNoTelemetrySetLogger(loggerIn: StandardLogger): void {
15
- logger = loggerIn.createModuleLogger("DbUtilsNoTelemetry");
16
- }
17
-
18
- /**
19
- * Execute a multi-row INSERT with a flat parameter array.
20
- * Builds: INSERT INTO <tableCols> VALUES (?,?...),(?,?...),...
21
- *
22
- * @returns Number of rows inserted.
23
- */
24
- export function DbUtilsNoTelemetryBatchInsert(
25
- tableCols: string,
26
- numCols: number,
27
- rows: any[][],
28
- ): number | Promise<number> {
29
- if (rows.length === 0) return 0;
30
- const rowSQL = `(${Array.from({ length: numCols }, () => "?").join(",")})`;
31
- const multiValues = Array.from({ length: rows.length }, () => rowSQL).join(
32
- ",",
33
- );
34
- const sql = `INSERT ${tableCols} VALUES ${multiValues}`;
35
- return DbUtilsNoTelemetryExecSQL(sql, rows.flat());
36
- }
37
-
38
- /**
39
- * Execute a write SQL statement **without** creating an OTel span.
40
- * Use this on high-throughput paths where span overhead matters.
41
- *
42
- * @returns Number of rows changed.
43
- */
44
- export function DbUtilsNoTelemetryExecSQL(
45
- sql: string,
46
- params: unknown[] = [],
47
- ): number | Promise<number> {
48
- const dbType = DbUtilsGetType();
49
- if (dbType === "postgres") {
50
- const pgSql = convertToPostgresPlaceholders(sql);
51
- return new Promise((resolve, reject) => {
52
- (DbUtilsGetDatabase() as any).query(
53
- pgSql,
54
- params,
55
- (error: Error | null, result: { rowCount: number | null }) => {
56
- if (error) {
57
- logger.error(`SQL INSERT ERROR: ${sql.substring(0, 200)}`, error);
58
- reject(error);
59
- } else {
60
- resolve(result.rowCount || 0);
61
- }
62
- },
63
- );
64
- });
65
- }
66
- // SQLite (better-sqlite3) – synchronous
67
- const stmt = (
68
- DbUtilsGetDatabase() as {
69
- prepare: (sql: string) => {
70
- run: (params: unknown[]) => { changes: number };
71
- };
72
- }
73
- ).prepare(sql);
74
- const result = stmt.run(params);
75
- return result.changes;
76
- }
77
-
78
- /**
79
- * Execute a read SQL query **without** creating an OTel span.
80
- * Use this on high-throughput paths where span overhead matters.
81
- *
82
- * @returns Array of row objects.
83
- */
84
- export function DbUtilsNoTelemetryQuerySQL(
85
- sql: string,
86
- params: unknown[] = [],
87
- debug = false,
88
- ): any[] | Promise<any[]> {
89
- if (debug) {
90
- console.log(sql);
91
- }
92
- const dbType = DbUtilsGetType();
93
- if (dbType === "postgres") {
94
- const pgSql = convertToPostgresPlaceholders(sql);
95
- return new Promise((resolve, reject) => {
96
- (DbUtilsGetDatabase() as any).query(
97
- pgSql,
98
- params,
99
- (error: Error | null, result: { rows: unknown[] }) => {
100
- if (error) {
101
- logger.error(`SQL ERROR: ${sql}`, error);
102
- reject(error);
103
- } else {
104
- resolve(result.rows);
105
- }
106
- },
107
- );
108
- });
109
- }
110
- // SQLite (better-sqlite3) – synchronous
111
- const stmt = (
112
- DbUtilsGetDatabase() as {
113
- prepare: (sql: string) => { all: (params: unknown[]) => unknown[] };
114
- }
115
- ).prepare(sql);
116
- return stmt.all(params);
117
- }
package/src/LLM.spec.ts DELETED
@@ -1,303 +0,0 @@
1
- jest.mock("axios", () => ({
2
- create: jest.fn(),
3
- isAxiosError: (err: unknown) =>
4
- typeof err === "object" &&
5
- err !== null &&
6
- (err as { isAxiosError?: boolean }).isAxiosError === true,
7
- }));
8
-
9
- import axios from "axios";
10
- import { LLMClient, LLMLogger } from "./LLM";
11
-
12
- const mockedCreate = axios.create as jest.MockedFunction<typeof axios.create>;
13
-
14
- /** Logger double that records every call. */
15
- function createMockLogger(): LLMLogger & {
16
- info: jest.Mock;
17
- error: jest.Mock;
18
- } {
19
- return {
20
- info: jest.fn(),
21
- error: jest.fn(),
22
- };
23
- }
24
-
25
- describe("LLMClient", () => {
26
- let mockPost: jest.Mock;
27
- let mockLogger: ReturnType<typeof createMockLogger>;
28
-
29
- beforeEach(() => {
30
- jest.clearAllMocks();
31
- mockPost = jest.fn();
32
- mockedCreate.mockReturnValue({ post: mockPost } as never);
33
- mockLogger = createMockLogger();
34
- });
35
-
36
- function createEnabledClient(): LLMClient {
37
- return new LLMClient({
38
- apiKey: "key",
39
- apiUrl: "https://api.example.com/chat/completions",
40
- model: "test-model",
41
- logger: mockLogger,
42
- });
43
- }
44
-
45
- describe("constructor", () => {
46
- it("should be enabled when apiKey, apiUrl and model are set", () => {
47
- const client = createEnabledClient();
48
-
49
- expect(client.isEnabled()).toBe(true);
50
- expect(mockedCreate).toHaveBeenCalledTimes(1);
51
- });
52
-
53
- it("should create the HTTP client with auth header and timeout", () => {
54
- new LLMClient({
55
- apiKey: "key",
56
- apiUrl: "https://api.example.com/chat/completions",
57
- model: "test-model",
58
- timeoutMs: 42000,
59
- logger: mockLogger,
60
- });
61
-
62
- expect(mockedCreate).toHaveBeenCalledWith({
63
- baseURL: "https://api.example.com/chat/completions",
64
- headers: {
65
- "Content-Type": "application/json",
66
- Authorization: "Bearer key",
67
- },
68
- timeout: 42000,
69
- });
70
- });
71
-
72
- it("should default the timeout to 120000 ms", () => {
73
- createEnabledClient();
74
-
75
- expect(mockedCreate).toHaveBeenCalledWith(
76
- expect.objectContaining({ timeout: 120000 }),
77
- );
78
- });
79
-
80
- it.each([
81
- ["apiKey", { apiKey: "" }],
82
- ["apiUrl", { apiUrl: "" }],
83
- ["model", { model: "" }],
84
- ])("should be disabled when %s is empty", (_name, override) => {
85
- const client = new LLMClient({
86
- apiKey: "key",
87
- apiUrl: "https://api.example.com/chat/completions",
88
- model: "test-model",
89
- logger: mockLogger,
90
- ...override,
91
- });
92
-
93
- expect(client.isEnabled()).toBe(false);
94
- expect(mockedCreate).not.toHaveBeenCalled();
95
- });
96
-
97
- it("should log the integration status exactly once at construction", () => {
98
- createEnabledClient();
99
-
100
- expect(mockLogger.info).toHaveBeenCalledTimes(1);
101
- expect(mockLogger.info).toHaveBeenCalledWith(
102
- "LLM integration enabled (model: test-model)",
103
- );
104
- });
105
-
106
- it("should log the disabled status when misconfigured", () => {
107
- new LLMClient({
108
- apiKey: "",
109
- apiUrl: "",
110
- model: "",
111
- logger: mockLogger,
112
- });
113
-
114
- expect(mockLogger.info).toHaveBeenCalledWith(
115
- "LLM integration disabled (apiKey, apiUrl or model not set)",
116
- );
117
- });
118
- });
119
-
120
- describe("request", () => {
121
- it("should throw when the client is disabled", async () => {
122
- const client = new LLMClient({
123
- apiKey: "",
124
- apiUrl: "",
125
- model: "",
126
- logger: mockLogger,
127
- });
128
-
129
- await expect(
130
- client.request([{ role: "user", content: "hello" }]),
131
- ).rejects.toThrow("LLM integration disabled");
132
- expect(mockPost).not.toHaveBeenCalled();
133
- });
134
-
135
- it("should send model and messages without response_format by default", async () => {
136
- mockPost.mockResolvedValue({
137
- data: {
138
- choices: [{ message: { content: "hi there" } }],
139
- usage: { total_tokens: 12 },
140
- },
141
- });
142
- const client = createEnabledClient();
143
-
144
- const response = await client.request([
145
- { role: "system", content: "be brief" },
146
- { role: "user", content: "hello" },
147
- ]);
148
-
149
- expect(mockPost).toHaveBeenCalledWith("", {
150
- model: "test-model",
151
- messages: [
152
- { role: "system", content: "be brief" },
153
- { role: "user", content: "hello" },
154
- ],
155
- stream: false,
156
- });
157
- expect(response).toEqual({ content: "hi there", totalTokens: 12 });
158
- });
159
-
160
- it("should add response_format json_object in jsonMode", async () => {
161
- mockPost.mockResolvedValue({
162
- data: {
163
- choices: [{ message: { content: "{}" } }],
164
- usage: { total_tokens: 1 },
165
- },
166
- });
167
- const client = createEnabledClient();
168
-
169
- await client.request([{ role: "user", content: "hello" }], {
170
- jsonMode: true,
171
- });
172
-
173
- expect(mockPost).toHaveBeenCalledWith(
174
- "",
175
- expect.objectContaining({
176
- response_format: { type: "json_object" },
177
- }),
178
- );
179
- });
180
-
181
- it("should override the model per request", async () => {
182
- mockPost.mockResolvedValue({
183
- data: {
184
- choices: [{ message: { content: "ok" } }],
185
- usage: { total_tokens: 1 },
186
- },
187
- });
188
- const client = createEnabledClient();
189
-
190
- await client.request([{ role: "user", content: "hello" }], {
191
- model: "other-model",
192
- });
193
-
194
- expect(mockPost).toHaveBeenCalledWith(
195
- "",
196
- expect.objectContaining({ model: "other-model" }),
197
- );
198
- });
199
-
200
- it("should override the timeout per request", async () => {
201
- mockPost.mockResolvedValue({
202
- data: {
203
- choices: [{ message: { content: "ok" } }],
204
- usage: { total_tokens: 1 },
205
- },
206
- });
207
- const client = createEnabledClient();
208
-
209
- await client.request([{ role: "user", content: "hello" }], {
210
- timeoutMs: 600000,
211
- });
212
-
213
- expect(mockPost).toHaveBeenCalledWith(
214
- "",
215
- expect.anything(),
216
- expect.objectContaining({ timeout: 600000 }),
217
- );
218
- });
219
-
220
- it("should disable the timeout when timeoutMs is 0", async () => {
221
- mockPost.mockResolvedValue({
222
- data: {
223
- choices: [{ message: { content: "ok" } }],
224
- usage: { total_tokens: 1 },
225
- },
226
- });
227
- const client = createEnabledClient();
228
-
229
- await client.request([{ role: "user", content: "hello" }], {
230
- timeoutMs: 0,
231
- });
232
-
233
- expect(mockPost).toHaveBeenCalledWith(
234
- "",
235
- expect.anything(),
236
- expect.objectContaining({ timeout: 0 }),
237
- );
238
- });
239
-
240
- it("should not pass a request config when no timeout override is given", async () => {
241
- mockPost.mockResolvedValue({
242
- data: {
243
- choices: [{ message: { content: "ok" } }],
244
- usage: { total_tokens: 1 },
245
- },
246
- });
247
- const client = createEnabledClient();
248
-
249
- await client.request([{ role: "user", content: "hello" }]);
250
-
251
- expect(mockPost).toHaveBeenCalledTimes(1);
252
- expect(mockPost.mock.calls[0]).toHaveLength(2);
253
- });
254
-
255
- it("should default totalTokens to 0 and content to empty string", async () => {
256
- mockPost.mockResolvedValue({
257
- data: { choices: [{ message: { content: null } }] },
258
- });
259
- const client = createEnabledClient();
260
-
261
- const response = await client.request([
262
- { role: "user", content: "hello" },
263
- ]);
264
-
265
- expect(response).toEqual({ content: "", totalTokens: 0 });
266
- });
267
-
268
- it("should throw when the response has no choices", async () => {
269
- mockPost.mockResolvedValue({ data: {} });
270
- const client = createEnabledClient();
271
-
272
- await expect(
273
- client.request([{ role: "user", content: "hello" }]),
274
- ).rejects.toThrow("LLM response has no choices");
275
- });
276
-
277
- it("should surface the provider error message", async () => {
278
- const axiosError = Object.assign(new Error("Request failed 401"), {
279
- isAxiosError: true,
280
- response: { data: { error: { message: "Invalid API key" } } },
281
- });
282
- mockPost.mockRejectedValue(axiosError);
283
- const client = createEnabledClient();
284
-
285
- await expect(
286
- client.request([{ role: "user", content: "hello" }]),
287
- ).rejects.toThrow("Invalid API key");
288
- expect(mockLogger.error).toHaveBeenCalledWith(
289
- "LLMClient: request failed (Invalid API key)",
290
- axiosError,
291
- );
292
- });
293
-
294
- it("should keep the original message for non-provider errors", async () => {
295
- mockPost.mockRejectedValue(new Error("Network down"));
296
- const client = createEnabledClient();
297
-
298
- await expect(
299
- client.request([{ role: "user", content: "hello" }]),
300
- ).rejects.toThrow("Network down");
301
- });
302
- });
303
- });
package/src/LLM.ts DELETED
@@ -1,204 +0,0 @@
1
- import axios, { AxiosInstance } from "axios";
2
-
3
- /**
4
- * A single chat message in OpenAI-compatible format.
5
- */
6
- export interface LLMMessage {
7
- /** Message role, e.g. "system", "user", "assistant" */
8
- role: string;
9
- /** Message content */
10
- content: string;
11
- }
12
-
13
- /**
14
- * Minimal logger interface expected by the LLM client.
15
- * Matches the subset of the OTel logger used by devopsplaybook.io projects.
16
- */
17
- export interface LLMLogger {
18
- info(message: string): void;
19
- error(message: string, err?: unknown): void;
20
- }
21
-
22
- /**
23
- * Configuration for {@link LLMClient}.
24
- */
25
- export interface LLMClientConfig {
26
- /** API key used for Bearer authentication */
27
- apiKey: string;
28
- /** Chat completions endpoint URL (e.g., "https://api.deepseek.com/chat/completions") */
29
- apiUrl: string;
30
- /** Model name to use (e.g., "deepseek-chat") */
31
- model: string;
32
- /** Request timeout in milliseconds (defaults to 120000) */
33
- timeoutMs?: number;
34
- /** Optional logger; falls back to console when omitted */
35
- logger?: LLMLogger;
36
- }
37
-
38
- /**
39
- * Per-request options for {@link LLMClient.request}.
40
- */
41
- export interface LLMRequestOptions {
42
- /** Request JSON output (`response_format: json_object`). Defaults to false. */
43
- jsonMode?: boolean;
44
- /** Override the configured model for this call */
45
- model?: string;
46
- /**
47
- * Override the configured timeout for this call, in milliseconds.
48
- * Set `0` to disable the timeout entirely, for slow models or very large
49
- * completions that legitimately exceed the client-level timeout.
50
- * When omitted, the client-level `timeoutMs` applies.
51
- */
52
- timeoutMs?: number;
53
- }
54
-
55
- /**
56
- * Normalized response from a chat completion call.
57
- */
58
- export interface LLMResponse {
59
- /** Content of the first choice (empty string when the model returned none) */
60
- content: string;
61
- /** Total tokens used, as reported by the provider (0 when unavailable) */
62
- totalTokens: number;
63
- }
64
-
65
- /** Console fallback used when no logger is injected. */
66
- const consoleLogger: LLMLogger = {
67
- info: (message: string) => console.log(message),
68
- error: (message: string, err?: unknown) => console.error(message, err),
69
- };
70
-
71
- /**
72
- * Client for OpenAI-compatible chat completions APIs (DeepSeek, Moonshot,
73
- * Ollama, etc.).
74
- *
75
- * The client follows the same fail-safe pattern as {@link NotificationsClient}:
76
- *
77
- * - It is disabled when `apiKey`, `apiUrl` or `model` is missing, so a
78
- * partially configured parent application still starts.
79
- * - The integration status is logged exactly once, at construction time.
80
- * - Calling `request` on a disabled client throws, since an LLM call that
81
- * silently returns nothing is rarely what the caller wants. Check
82
- * `isEnabled()` before relying on the client.
83
- * - Provider errors are rethrown as `Error` with the provider message when
84
- * available, so callers get actionable failure reasons.
85
- *
86
- * @example
87
- * ```ts
88
- * const llm = new LLMClient({
89
- * apiKey: config.LLM_API_KEY,
90
- * apiUrl: config.LLM_API_URL,
91
- * model: config.LLM_MODEL,
92
- * logger: OTelLogger().createModuleLogger("llm"),
93
- * });
94
- *
95
- * if (llm.isEnabled()) {
96
- * const response = await llm.request([
97
- * { role: "system", content: "You summarize text." },
98
- * { role: "user", content: someText },
99
- * ]);
100
- * console.log(response.content, response.totalTokens);
101
- * }
102
- * ```
103
- */
104
- export class LLMClient {
105
- private client: AxiosInstance | null = null;
106
- private readonly enabled: boolean;
107
- private readonly model: string;
108
- private readonly timeoutMs: number;
109
- private readonly logger: LLMLogger;
110
-
111
- constructor(config: LLMClientConfig) {
112
- this.enabled = !!(config.apiKey && config.apiUrl && config.model);
113
- this.model = config.model;
114
- this.timeoutMs = config.timeoutMs || 120000;
115
- this.logger = config.logger || consoleLogger;
116
-
117
- if (this.enabled) {
118
- this.client = axios.create({
119
- baseURL: config.apiUrl,
120
- headers: {
121
- "Content-Type": "application/json",
122
- Authorization: `Bearer ${config.apiKey}`,
123
- },
124
- timeout: this.timeoutMs,
125
- });
126
- this.logger.info(`LLM integration enabled (model: ${this.model})`);
127
- } else {
128
- this.logger.info(
129
- "LLM integration disabled (apiKey, apiUrl or model not set)",
130
- );
131
- }
132
- }
133
-
134
- /**
135
- * Check whether the client is properly configured.
136
- */
137
- public isEnabled(): boolean {
138
- return this.enabled;
139
- }
140
-
141
- /**
142
- * Send a chat completion request.
143
- *
144
- * @param messages The chat messages to send.
145
- * @param options Optional per-request overrides (JSON mode, model,
146
- * timeout). `timeoutMs: 0` disables the request timeout.
147
- * @returns The first choice content and total token usage.
148
- * @throws When the client is disabled, the provider returns an error, or
149
- * the response has no choices.
150
- */
151
- public async request(
152
- messages: LLMMessage[],
153
- options?: LLMRequestOptions,
154
- ): Promise<LLMResponse> {
155
- if (!this.enabled || !this.client) {
156
- throw new Error(
157
- "LLM integration disabled (apiKey, apiUrl or model not set)",
158
- );
159
- }
160
-
161
- const model = options?.model || this.model;
162
- const body: Record<string, any> = {
163
- model,
164
- messages,
165
- stream: false,
166
- };
167
- if (options?.jsonMode) {
168
- body.response_format = { type: "json_object" };
169
- }
170
-
171
- try {
172
- const response =
173
- options?.timeoutMs !== undefined
174
- ? await this.client.post("", body, { timeout: options.timeoutMs })
175
- : await this.client.post("", body);
176
- const choice = response.data?.choices?.[0];
177
- if (!choice) {
178
- throw new Error("LLM response has no choices");
179
- }
180
- return {
181
- content: choice.message?.content || "",
182
- totalTokens: response.data?.usage?.total_tokens || 0,
183
- };
184
- } catch (err) {
185
- const message = this.extractErrorMessage(err);
186
- this.logger.error(`LLMClient: request failed (${message})`, err);
187
- throw new Error(message, { cause: err });
188
- }
189
- }
190
-
191
- /**
192
- * Extract a readable error message, preferring the provider's error
193
- * payload when the failure comes from an HTTP response.
194
- */
195
- private extractErrorMessage(err: unknown): string {
196
- if (axios.isAxiosError(err)) {
197
- const providerMessage = (err.response?.data as any)?.error?.message;
198
- if (providerMessage) {
199
- return providerMessage;
200
- }
201
- }
202
- return err instanceof Error ? err.message : String(err);
203
- }
204
- }