@icedq/cli 0.1.3 → 0.2.1

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.
@@ -1,183 +1,183 @@
1
- import https from 'node:https';
2
- import http from 'node:http';
3
- import { URL } from 'node:url';
4
- import FormData from 'form-data';
5
- import { ApiError } from './errors.js';
6
- import { log } from './logger.js';
7
-
8
- const STATUS_RETRY_5XX = 3;
9
- const STATUS_RETRY_DELAY_MS = 5000;
10
-
11
- const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
12
-
13
- export class IcedqApiClient {
14
- constructor({ baseUrl, orgId, accountId, workspaceId, auth, verifySsl = true }) {
15
- this.baseUrl = baseUrl.replace(/\/+$/, '');
16
- this.orgId = orgId;
17
- this.accountId = accountId;
18
- this.workspaceId = workspaceId;
19
- this.auth = auth;
20
- this.verifySsl = verifySsl;
21
- }
22
-
23
- _buildHeaders(extra = {}, { includeWorkspace = true } = {}) {
24
- const headers = {
25
- Accept: 'application/json',
26
- 'Org-Id': this.orgId,
27
- 'Account-Id': this.accountId,
28
- ...extra
29
- };
30
- if (includeWorkspace && this.workspaceId) {
31
- headers['Workspace-Id'] = this.workspaceId;
32
- }
33
- return headers;
34
- }
35
-
36
- async get(path, opts = {}) {
37
- return this._request('GET', path, opts);
38
- }
39
-
40
- async post(path, body, opts = {}) {
41
- return this._request('POST', path, { ...opts, jsonBody: body });
42
- }
43
-
44
- async postMultipart(path, parts, opts = {}) {
45
- const form = new FormData();
46
- for (const [name, value] of Object.entries(parts)) {
47
- if (value && typeof value === 'object' && value.buffer && value.filename) {
48
- form.append(name, value.buffer, {
49
- filename: value.filename,
50
- contentType: value.contentType || 'application/octet-stream'
51
- });
52
- } else if (value && typeof value === 'object' && value.json !== undefined) {
53
- form.append(name, JSON.stringify(value.json), { contentType: 'application/json' });
54
- } else {
55
- form.append(name, value);
56
- }
57
- }
58
- return this._request('POST', path, { ...opts, multipart: form });
59
- }
60
-
61
- async getBinary(path, opts = {}) {
62
- return this._request('GET', path, { ...opts, expectBinary: true });
63
- }
64
-
65
- async _request(method, path, opts = {}) {
66
- const includeWorkspace = opts.includeWorkspace !== false;
67
- let attempt = 0;
68
- let last401 = false;
69
-
70
- while (true) {
71
- attempt++;
72
- const token = await this.auth.getToken();
73
- const headers = this._buildHeaders(
74
- {
75
- Authorization: `Bearer ${token}`,
76
- ...(opts.headers || {})
77
- },
78
- { includeWorkspace }
79
- );
80
-
81
- let bodyBuffer;
82
- if (opts.multipart) {
83
- Object.assign(headers, opts.multipart.getHeaders());
84
- bodyBuffer = opts.multipart.getBuffer();
85
- } else if (opts.jsonBody !== undefined) {
86
- headers['Content-Type'] = 'application/json';
87
- bodyBuffer = Buffer.from(JSON.stringify(opts.jsonBody));
88
- headers['Content-Length'] = bodyBuffer.length;
89
- }
90
-
91
- const url = path.startsWith('http')
92
- ? path
93
- : `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
94
-
95
- log.debug('iceDQ request', { method, url, attempt });
96
-
97
- let response;
98
- try {
99
- response = await this._doRequest(method, url, headers, bodyBuffer, !!opts.expectBinary);
100
- } catch (err) {
101
- throw new ApiError(`Network error calling ${url}: ${err.message}`, { endpoint: path });
102
- }
103
-
104
- if (response.status === 401 && !last401) {
105
- log.warn('iceDQ 401 — refreshing token and retrying once');
106
- last401 = true;
107
- await this.auth.forceRefresh();
108
- continue;
109
- }
110
-
111
- if (response.status >= 500 && opts.retryOn5xx && attempt <= STATUS_RETRY_5XX) {
112
- log.warn(`iceDQ ${response.status} — retrying after ${STATUS_RETRY_DELAY_MS}ms`, { attempt });
113
- await sleep(STATUS_RETRY_DELAY_MS);
114
- continue;
115
- }
116
-
117
- if (response.status < 200 || response.status >= 300) {
118
- const parsed = parseErrorBody(response.body);
119
- throw new ApiError(`HTTP ${response.status} from ${path}: ${parsed.summary}`, {
120
- status: response.status,
121
- code: parsed.code,
122
- messages: parsed.messages,
123
- body: response.body,
124
- endpoint: path
125
- });
126
- }
127
-
128
- if (opts.expectBinary) return response.buffer;
129
- if (!response.body) return {};
130
- try {
131
- return JSON.parse(response.body);
132
- } catch {
133
- return response.body;
134
- }
135
- }
136
- }
137
-
138
- _doRequest(method, urlString, headers, bodyBuffer, expectBinary) {
139
- return new Promise((resolve, reject) => {
140
- const url = new URL(urlString);
141
- const isHttps = url.protocol === 'https:';
142
- const lib = isHttps ? https : http;
143
- const reqOpts = { method, headers };
144
- if (isHttps && !this.verifySsl) reqOpts.rejectUnauthorized = false;
145
-
146
- const req = lib.request(url, reqOpts, (res) => {
147
- const chunks = [];
148
- res.on('data', (c) => chunks.push(c));
149
- res.on('end', () => {
150
- if (expectBinary) {
151
- resolve({ status: res.statusCode, buffer: Buffer.concat(chunks) });
152
- } else {
153
- resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') });
154
- }
155
- });
156
- });
157
- req.on('error', reject);
158
- if (bodyBuffer) req.write(bodyBuffer);
159
- req.end();
160
- });
161
- }
162
- }
163
-
164
- function parseErrorBody(body) {
165
- if (!body) return { summary: '(empty body)', code: undefined, messages: [] };
166
- try {
167
- const parsed = JSON.parse(body);
168
- if (parsed && typeof parsed === 'object') {
169
- const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
170
- const summary =
171
- messages
172
- .map((m) => `${m.fieldName ? m.fieldName + ': ' : ''}${m.violation || m.message || ''}`)
173
- .filter(Boolean)
174
- .join('; ') || parsed.message || JSON.stringify(parsed);
175
- return { summary, code: parsed.code, messages };
176
- }
177
- } catch {
178
- // not JSON, fall through
179
- }
180
- return { summary: body.slice(0, 200), code: undefined, messages: [] };
181
- }
182
-
183
- export const _internal = { parseErrorBody };
1
+ import https from 'node:https';
2
+ import http from 'node:http';
3
+ import { URL } from 'node:url';
4
+ import FormData from 'form-data';
5
+ import { ApiError } from './errors.js';
6
+ import { log } from './logger.js';
7
+
8
+ const STATUS_RETRY_5XX = 3;
9
+ const STATUS_RETRY_DELAY_MS = 5000;
10
+
11
+ const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
12
+
13
+ export class IcedqApiClient {
14
+ constructor({ baseUrl, orgId, accountId, workspaceId, auth, verifySsl = true }) {
15
+ this.baseUrl = baseUrl.replace(/\/+$/, '');
16
+ this.orgId = orgId;
17
+ this.accountId = accountId;
18
+ this.workspaceId = workspaceId;
19
+ this.auth = auth;
20
+ this.verifySsl = verifySsl;
21
+ }
22
+
23
+ _buildHeaders(extra = {}, { includeWorkspace = true } = {}) {
24
+ const headers = {
25
+ Accept: 'application/json',
26
+ 'Org-Id': this.orgId,
27
+ 'Account-Id': this.accountId,
28
+ ...extra
29
+ };
30
+ if (includeWorkspace && this.workspaceId) {
31
+ headers['Workspace-Id'] = this.workspaceId;
32
+ }
33
+ return headers;
34
+ }
35
+
36
+ async get(path, opts = {}) {
37
+ return this._request('GET', path, opts);
38
+ }
39
+
40
+ async post(path, body, opts = {}) {
41
+ return this._request('POST', path, { ...opts, jsonBody: body });
42
+ }
43
+
44
+ async postMultipart(path, parts, opts = {}) {
45
+ const form = new FormData();
46
+ for (const [name, value] of Object.entries(parts)) {
47
+ if (value && typeof value === 'object' && value.buffer && value.filename) {
48
+ form.append(name, value.buffer, {
49
+ filename: value.filename,
50
+ contentType: value.contentType || 'application/octet-stream'
51
+ });
52
+ } else if (value && typeof value === 'object' && value.json !== undefined) {
53
+ form.append(name, JSON.stringify(value.json), { contentType: 'application/json' });
54
+ } else {
55
+ form.append(name, value);
56
+ }
57
+ }
58
+ return this._request('POST', path, { ...opts, multipart: form });
59
+ }
60
+
61
+ async getBinary(path, opts = {}) {
62
+ return this._request('GET', path, { ...opts, expectBinary: true });
63
+ }
64
+
65
+ async _request(method, path, opts = {}) {
66
+ const includeWorkspace = opts.includeWorkspace !== false;
67
+ let attempt = 0;
68
+ let last401 = false;
69
+
70
+ while (true) {
71
+ attempt++;
72
+ const token = await this.auth.getToken();
73
+ const headers = this._buildHeaders(
74
+ {
75
+ Authorization: `Bearer ${token}`,
76
+ ...(opts.headers || {})
77
+ },
78
+ { includeWorkspace }
79
+ );
80
+
81
+ let bodyBuffer;
82
+ if (opts.multipart) {
83
+ Object.assign(headers, opts.multipart.getHeaders());
84
+ bodyBuffer = opts.multipart.getBuffer();
85
+ } else if (opts.jsonBody !== undefined) {
86
+ headers['Content-Type'] = 'application/json';
87
+ bodyBuffer = Buffer.from(JSON.stringify(opts.jsonBody));
88
+ headers['Content-Length'] = bodyBuffer.length;
89
+ }
90
+
91
+ const url = path.startsWith('http')
92
+ ? path
93
+ : `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
94
+
95
+ log.debug('iceDQ request', { method, url, attempt });
96
+
97
+ let response;
98
+ try {
99
+ response = await this._doRequest(method, url, headers, bodyBuffer, !!opts.expectBinary);
100
+ } catch (err) {
101
+ throw new ApiError(`Network error calling ${url}: ${err.message}`, { endpoint: path });
102
+ }
103
+
104
+ if (response.status === 401 && !last401) {
105
+ log.warn('iceDQ 401 — refreshing token and retrying once');
106
+ last401 = true;
107
+ await this.auth.forceRefresh();
108
+ continue;
109
+ }
110
+
111
+ if (response.status >= 500 && opts.retryOn5xx && attempt <= STATUS_RETRY_5XX) {
112
+ log.warn(`iceDQ ${response.status} — retrying after ${STATUS_RETRY_DELAY_MS}ms`, { attempt });
113
+ await sleep(STATUS_RETRY_DELAY_MS);
114
+ continue;
115
+ }
116
+
117
+ if (response.status < 200 || response.status >= 300) {
118
+ const parsed = parseErrorBody(response.body);
119
+ throw new ApiError(`HTTP ${response.status} from ${path}: ${parsed.summary}`, {
120
+ status: response.status,
121
+ code: parsed.code,
122
+ messages: parsed.messages,
123
+ body: response.body,
124
+ endpoint: path
125
+ });
126
+ }
127
+
128
+ if (opts.expectBinary) return response.buffer;
129
+ if (!response.body) return {};
130
+ try {
131
+ return JSON.parse(response.body);
132
+ } catch {
133
+ return response.body;
134
+ }
135
+ }
136
+ }
137
+
138
+ _doRequest(method, urlString, headers, bodyBuffer, expectBinary) {
139
+ return new Promise((resolve, reject) => {
140
+ const url = new URL(urlString);
141
+ const isHttps = url.protocol === 'https:';
142
+ const lib = isHttps ? https : http;
143
+ const reqOpts = { method, headers };
144
+ if (isHttps && !this.verifySsl) reqOpts.rejectUnauthorized = false;
145
+
146
+ const req = lib.request(url, reqOpts, (res) => {
147
+ const chunks = [];
148
+ res.on('data', (c) => chunks.push(c));
149
+ res.on('end', () => {
150
+ if (expectBinary) {
151
+ resolve({ status: res.statusCode, buffer: Buffer.concat(chunks) });
152
+ } else {
153
+ resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8') });
154
+ }
155
+ });
156
+ });
157
+ req.on('error', reject);
158
+ if (bodyBuffer) req.write(bodyBuffer);
159
+ req.end();
160
+ });
161
+ }
162
+ }
163
+
164
+ function parseErrorBody(body) {
165
+ if (!body) return { summary: '(empty body)', code: undefined, messages: [] };
166
+ try {
167
+ const parsed = JSON.parse(body);
168
+ if (parsed && typeof parsed === 'object') {
169
+ const messages = Array.isArray(parsed.messages) ? parsed.messages : [];
170
+ const summary =
171
+ messages
172
+ .map((m) => `${m.fieldName ? m.fieldName + ': ' : ''}${m.violation || m.message || ''}`)
173
+ .filter(Boolean)
174
+ .join('; ') || parsed.message || JSON.stringify(parsed);
175
+ return { summary, code: parsed.code, messages };
176
+ }
177
+ } catch {
178
+ // not JSON, fall through
179
+ }
180
+ return { summary: body.slice(0, 200), code: undefined, messages: [] };
181
+ }
182
+
183
+ export const _internal = { parseErrorBody };