@boltic/cli 1.0.44-dev1.4 → 1.0.45
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/api/integration.js +66 -40
- package/api/serverless.js +21 -39
- package/commands/login.js +4 -4
- package/commands/serverless.js +4 -4
- package/helper/error.js +95 -35
- package/helper/secure-storage.js +40 -143
- package/helper/serverless.js +4 -4
- package/package.json +2 -1
package/api/integration.js
CHANGED
|
@@ -3,7 +3,8 @@ import FormData from "form-data";
|
|
|
3
3
|
import fs from "fs";
|
|
4
4
|
import https from "https";
|
|
5
5
|
import { handleError } from "../helper/error.js";
|
|
6
|
-
import {
|
|
6
|
+
import { getSecret } from "../helper/secure-storage.js";
|
|
7
|
+
import { logApi, logApiRequest, logApiResponse } from "../helper/verbose.js";
|
|
7
8
|
|
|
8
9
|
const getHttpsAgentForUrl = (baseUrl) => {
|
|
9
10
|
try {
|
|
@@ -22,20 +23,33 @@ const getHttpsAgentForUrl = (baseUrl) => {
|
|
|
22
23
|
return undefined;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const buildAuthHeaders = async (token, session) => {
|
|
27
|
+
const pat = await getSecret("pat");
|
|
28
|
+
|
|
29
|
+
// If PAT exists, prefer PAT-based auth and do not send bearer/session
|
|
30
|
+
if (pat && pat.trim()) {
|
|
31
|
+
return {
|
|
32
|
+
"x-boltic-token": pat.trim(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to existing Bearer + Cookie auth
|
|
37
|
+
const headers = {};
|
|
38
|
+
if (token) {
|
|
39
|
+
headers.Authorization = `Bearer ${token}`;
|
|
40
|
+
}
|
|
28
41
|
if (session) {
|
|
29
|
-
const headers = {};
|
|
30
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
31
42
|
headers.Cookie = session;
|
|
32
|
-
return headers;
|
|
33
43
|
}
|
|
34
|
-
return
|
|
44
|
+
return headers;
|
|
35
45
|
};
|
|
36
46
|
|
|
37
|
-
const ensureAuthenticatedOrExit = (accountId, token) => {
|
|
38
|
-
|
|
47
|
+
const ensureAuthenticatedOrExit = async (accountId, token, session) => {
|
|
48
|
+
const pat = await getSecret("pat");
|
|
49
|
+
const hasPatAuth = pat && pat.trim() && accountId;
|
|
50
|
+
const hasSessionAuth = token && session && accountId;
|
|
51
|
+
|
|
52
|
+
if (!hasPatAuth && !hasSessionAuth) {
|
|
39
53
|
console.error(
|
|
40
54
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
41
55
|
);
|
|
@@ -47,8 +61,8 @@ const ensureAuthenticatedOrExit = (accountId, token) => {
|
|
|
47
61
|
|
|
48
62
|
const getIntegrationGroups = async (apiUrl, accountId, token, session) => {
|
|
49
63
|
try {
|
|
50
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
51
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
64
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
65
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
52
66
|
const axiosOptions = {
|
|
53
67
|
method: "get",
|
|
54
68
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integration-groups`,
|
|
@@ -73,8 +87,8 @@ const getIntegrationGroups = async (apiUrl, accountId, token, session) => {
|
|
|
73
87
|
|
|
74
88
|
const listAllIntegrations = async (apiUrl, token, accountId, session) => {
|
|
75
89
|
try {
|
|
76
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
77
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
90
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
91
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
78
92
|
const axiosOptions = {
|
|
79
93
|
method: "get",
|
|
80
94
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations`,
|
|
@@ -105,8 +119,8 @@ const saveIntegration = async (
|
|
|
105
119
|
integration
|
|
106
120
|
) => {
|
|
107
121
|
try {
|
|
108
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
109
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
122
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
123
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
110
124
|
const response = await axios({
|
|
111
125
|
method: "post",
|
|
112
126
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations`,
|
|
@@ -125,12 +139,14 @@ const saveIntegration = async (
|
|
|
125
139
|
|
|
126
140
|
const editIntegration = async (apiUrl, token, accountId, session, payload) => {
|
|
127
141
|
const { id } = payload;
|
|
142
|
+
const url = `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${id}/edit`;
|
|
128
143
|
try {
|
|
129
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
130
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
144
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
145
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
146
|
+
logApiRequest("post", url, payload);
|
|
131
147
|
const response = await axios({
|
|
132
148
|
method: "post",
|
|
133
|
-
url
|
|
149
|
+
url,
|
|
134
150
|
data: payload,
|
|
135
151
|
headers: {
|
|
136
152
|
"Content-Type": "application/json",
|
|
@@ -138,9 +154,10 @@ const editIntegration = async (apiUrl, token, accountId, session, payload) => {
|
|
|
138
154
|
},
|
|
139
155
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
140
156
|
});
|
|
141
|
-
|
|
157
|
+
logApiResponse(response.status, response.data);
|
|
142
158
|
return response.data.data;
|
|
143
159
|
} catch (error) {
|
|
160
|
+
logApiResponse(error.response?.status, error.response?.data);
|
|
144
161
|
handleError(error);
|
|
145
162
|
}
|
|
146
163
|
};
|
|
@@ -153,8 +170,8 @@ const updateIntegration = async (
|
|
|
153
170
|
integration
|
|
154
171
|
) => {
|
|
155
172
|
try {
|
|
156
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
157
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
173
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
174
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
158
175
|
const { id, ...rest } = integration;
|
|
159
176
|
const response = await axios({
|
|
160
177
|
method: "patch",
|
|
@@ -180,8 +197,8 @@ const getIntegrationById = async (
|
|
|
180
197
|
integrationId
|
|
181
198
|
) => {
|
|
182
199
|
try {
|
|
183
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
184
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
200
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
201
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
185
202
|
const response = await axios({
|
|
186
203
|
method: "get",
|
|
187
204
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${integrationId}`,
|
|
@@ -205,8 +222,8 @@ const getAuthenticationByIntegrationId = async (
|
|
|
205
222
|
integrationId
|
|
206
223
|
) => {
|
|
207
224
|
try {
|
|
208
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
209
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
225
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
226
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
210
227
|
const response = await axios({
|
|
211
228
|
method: "get",
|
|
212
229
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}integrations/${integrationId}/authentication`,
|
|
@@ -229,7 +246,14 @@ const getWebhooksByIntegrationId = async (
|
|
|
229
246
|
session,
|
|
230
247
|
integrationId
|
|
231
248
|
) => {
|
|
232
|
-
|
|
249
|
+
if (!token || !session || !accountId) {
|
|
250
|
+
console.error(
|
|
251
|
+
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
252
|
+
);
|
|
253
|
+
console.log("\n🔹 Please log in first using:");
|
|
254
|
+
console.log("\x1b[32m$ boltic login\x1b[0m\n");
|
|
255
|
+
process.exit(1); // Exit the CLI with an error code
|
|
256
|
+
}
|
|
233
257
|
|
|
234
258
|
try {
|
|
235
259
|
const response = await axios({
|
|
@@ -237,7 +261,8 @@ const getWebhooksByIntegrationId = async (
|
|
|
237
261
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}integrations/${integrationId}/webhooks`,
|
|
238
262
|
headers: {
|
|
239
263
|
"Content-Type": "application/json",
|
|
240
|
-
|
|
264
|
+
Authorization: `Bearer ${token}`,
|
|
265
|
+
Cookie: session,
|
|
241
266
|
},
|
|
242
267
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
243
268
|
});
|
|
@@ -255,8 +280,8 @@ const getConfigurationByIntegrationId = async (
|
|
|
255
280
|
integrationId
|
|
256
281
|
) => {
|
|
257
282
|
try {
|
|
258
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
259
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
283
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
284
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
260
285
|
const response = await axios({
|
|
261
286
|
method: "get",
|
|
262
287
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}integrations/${integrationId}/configuration`,
|
|
@@ -280,8 +305,8 @@ const syncIntegration = async (
|
|
|
280
305
|
integration
|
|
281
306
|
) => {
|
|
282
307
|
try {
|
|
283
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
284
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
308
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
309
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
285
310
|
const response = await axios({
|
|
286
311
|
method: "post",
|
|
287
312
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${integration.integration_id}/deploy`,
|
|
@@ -306,14 +331,15 @@ const sendIntegrationForReview = async (
|
|
|
306
331
|
integration
|
|
307
332
|
) => {
|
|
308
333
|
try {
|
|
309
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
334
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
310
335
|
const response = await axios({
|
|
311
336
|
method: "post",
|
|
312
337
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integration-reviews`,
|
|
313
338
|
data: integration,
|
|
314
339
|
headers: {
|
|
315
340
|
"Content-Type": "application/json",
|
|
316
|
-
|
|
341
|
+
Authorization: `Bearer ${token}`,
|
|
342
|
+
Cookie: session,
|
|
317
343
|
},
|
|
318
344
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
319
345
|
});
|
|
@@ -326,8 +352,8 @@ const sendIntegrationForReview = async (
|
|
|
326
352
|
const purgeCache = async (apiUrl, token, accountId, session, integration) => {
|
|
327
353
|
const { integration_id } = integration;
|
|
328
354
|
try {
|
|
329
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
330
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
355
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
356
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
331
357
|
const response = await axios({
|
|
332
358
|
method: "post",
|
|
333
359
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${integration_id}/cache`,
|
|
@@ -346,8 +372,8 @@ const purgeCache = async (apiUrl, token, accountId, session, integration) => {
|
|
|
346
372
|
|
|
347
373
|
const pullIntegration = async (apiUrl, token, accountId, session, id) => {
|
|
348
374
|
try {
|
|
349
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
350
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
375
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
376
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
351
377
|
const response = await axios({
|
|
352
378
|
method: "get",
|
|
353
379
|
url: `${apiUrl}/service/panel/automation/v1.0/${accountId}/integrations/${id}/pull`,
|
|
@@ -376,8 +402,8 @@ const uploadFileToCloud = async (
|
|
|
376
402
|
}
|
|
377
403
|
|
|
378
404
|
try {
|
|
379
|
-
ensureAuthenticatedOrExit(accountId, token);
|
|
380
|
-
const authHeaders = buildAuthHeaders(token, session);
|
|
405
|
+
await ensureAuthenticatedOrExit(accountId, token, session);
|
|
406
|
+
const authHeaders = await buildAuthHeaders(token, session);
|
|
381
407
|
const form = new FormData();
|
|
382
408
|
form.append("files", fs.createReadStream(filePath));
|
|
383
409
|
|
package/api/serverless.js
CHANGED
|
@@ -22,31 +22,6 @@ const getHttpsAgentForUrl = (baseUrl) => {
|
|
|
22
22
|
return undefined;
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
// When session is absent the token is a PAT → use x-boltic-token header.
|
|
26
|
-
// When session is present the token is a bearer token → use Authorization + Cookie.
|
|
27
|
-
const buildAuthHeaders = (token, session) => {
|
|
28
|
-
if (session) {
|
|
29
|
-
const headers = {};
|
|
30
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
31
|
-
headers.Cookie = session;
|
|
32
|
-
return headers;
|
|
33
|
-
}
|
|
34
|
-
return token ? { "x-boltic-token": token } : {};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Returns true when the user has sufficient credentials for any auth method.
|
|
38
|
-
// Pass accountId when the endpoint requires it; omit (undefined) when it doesn't.
|
|
39
|
-
// Passing null means "required but missing" → returns false.
|
|
40
|
-
const isAuthenticated = (token, session, accountId) => {
|
|
41
|
-
if (accountId !== undefined) {
|
|
42
|
-
// PAT auth: token + accountId (no session needed)
|
|
43
|
-
// Bearer auth: token + session + accountId
|
|
44
|
-
return !!(token && accountId);
|
|
45
|
-
}
|
|
46
|
-
// For endpoints that don't need accountId: PAT needs just token, bearer needs token+session
|
|
47
|
-
return !!token;
|
|
48
|
-
};
|
|
49
|
-
|
|
50
25
|
const listAllServerless = async (
|
|
51
26
|
apiUrl,
|
|
52
27
|
token,
|
|
@@ -54,7 +29,7 @@ const listAllServerless = async (
|
|
|
54
29
|
session,
|
|
55
30
|
query = null
|
|
56
31
|
) => {
|
|
57
|
-
if (!
|
|
32
|
+
if (!token || !session || !accountId) {
|
|
58
33
|
console.error(
|
|
59
34
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
60
35
|
);
|
|
@@ -81,7 +56,8 @@ const listAllServerless = async (
|
|
|
81
56
|
params,
|
|
82
57
|
headers: {
|
|
83
58
|
"Content-Type": "application/json",
|
|
84
|
-
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
Cookie: session,
|
|
85
61
|
},
|
|
86
62
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
87
63
|
};
|
|
@@ -99,7 +75,7 @@ const listAllServerless = async (
|
|
|
99
75
|
};
|
|
100
76
|
|
|
101
77
|
const pullServerless = async (apiUrl, token, accountId, session, id) => {
|
|
102
|
-
if (!
|
|
78
|
+
if (!token || !session || !accountId) {
|
|
103
79
|
console.error(
|
|
104
80
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
105
81
|
);
|
|
@@ -115,7 +91,8 @@ const pullServerless = async (apiUrl, token, accountId, session, id) => {
|
|
|
115
91
|
url,
|
|
116
92
|
headers: {
|
|
117
93
|
"Content-Type": "application/json",
|
|
118
|
-
|
|
94
|
+
Authorization: `Bearer ${token}`,
|
|
95
|
+
Cookie: session,
|
|
119
96
|
},
|
|
120
97
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
121
98
|
});
|
|
@@ -127,7 +104,7 @@ const pullServerless = async (apiUrl, token, accountId, session, id) => {
|
|
|
127
104
|
};
|
|
128
105
|
|
|
129
106
|
const publishServerless = async (apiUrl, token, session, payload) => {
|
|
130
|
-
if (!
|
|
107
|
+
if (!token || !session) {
|
|
131
108
|
console.error(
|
|
132
109
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
133
110
|
);
|
|
@@ -142,7 +119,8 @@ const publishServerless = async (apiUrl, token, session, payload) => {
|
|
|
142
119
|
url: `${apiUrl}/service/panel/serverless/v1.0/apps`,
|
|
143
120
|
headers: {
|
|
144
121
|
"Content-Type": "application/json",
|
|
145
|
-
|
|
122
|
+
Authorization: `Bearer ${token}`,
|
|
123
|
+
Cookie: session,
|
|
146
124
|
},
|
|
147
125
|
data: payload,
|
|
148
126
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
@@ -164,7 +142,7 @@ const updateServerless = async (
|
|
|
164
142
|
serverlessId,
|
|
165
143
|
payload
|
|
166
144
|
) => {
|
|
167
|
-
if (!
|
|
145
|
+
if (!token || !session) {
|
|
168
146
|
console.error(
|
|
169
147
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
170
148
|
);
|
|
@@ -179,7 +157,8 @@ const updateServerless = async (
|
|
|
179
157
|
url: `${apiUrl}/service/panel/serverless/v1.0/apps/${serverlessId}`,
|
|
180
158
|
headers: {
|
|
181
159
|
"Content-Type": "application/json",
|
|
182
|
-
|
|
160
|
+
Authorization: `Bearer ${token}`,
|
|
161
|
+
Cookie: session,
|
|
183
162
|
},
|
|
184
163
|
data: payload,
|
|
185
164
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
@@ -202,7 +181,7 @@ const getServerlessBuilds = async (
|
|
|
202
181
|
serverlessId,
|
|
203
182
|
options = {}
|
|
204
183
|
) => {
|
|
205
|
-
if (!
|
|
184
|
+
if (!token || !session || !accountId) {
|
|
206
185
|
console.error(
|
|
207
186
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
208
187
|
);
|
|
@@ -222,7 +201,8 @@ const getServerlessBuilds = async (
|
|
|
222
201
|
},
|
|
223
202
|
headers: {
|
|
224
203
|
"Content-Type": "application/json",
|
|
225
|
-
|
|
204
|
+
Authorization: `Bearer ${token}`,
|
|
205
|
+
Cookie: session,
|
|
226
206
|
},
|
|
227
207
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
228
208
|
};
|
|
@@ -243,7 +223,7 @@ const getServerlessLogs = async (
|
|
|
243
223
|
serverlessId,
|
|
244
224
|
options = {}
|
|
245
225
|
) => {
|
|
246
|
-
if (!
|
|
226
|
+
if (!token || !session || !accountId) {
|
|
247
227
|
console.error(
|
|
248
228
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
249
229
|
);
|
|
@@ -272,7 +252,8 @@ const getServerlessLogs = async (
|
|
|
272
252
|
params,
|
|
273
253
|
headers: {
|
|
274
254
|
"Content-Type": "application/json",
|
|
275
|
-
|
|
255
|
+
Authorization: `Bearer ${token}`,
|
|
256
|
+
Cookie: session,
|
|
276
257
|
},
|
|
277
258
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
278
259
|
};
|
|
@@ -293,7 +274,7 @@ const getBuildLogs = async (
|
|
|
293
274
|
serverlessId,
|
|
294
275
|
buildId
|
|
295
276
|
) => {
|
|
296
|
-
if (!
|
|
277
|
+
if (!token || !session || !accountId) {
|
|
297
278
|
console.error(
|
|
298
279
|
"\x1b[31mError:\x1b[0m Authentication credentials are required."
|
|
299
280
|
);
|
|
@@ -313,7 +294,8 @@ const getBuildLogs = async (
|
|
|
313
294
|
},
|
|
314
295
|
headers: {
|
|
315
296
|
"Content-Type": "application/json",
|
|
316
|
-
|
|
297
|
+
Authorization: `Bearer ${token}`,
|
|
298
|
+
Cookie: session,
|
|
317
299
|
},
|
|
318
300
|
httpsAgent: getHttpsAgentForUrl(apiUrl),
|
|
319
301
|
};
|
package/commands/login.js
CHANGED
|
@@ -234,17 +234,17 @@ async function handlePatLogin(patFromArg, accountIdFromArg) {
|
|
|
234
234
|
// If both values are provided via CLI flags, do not prompt at all.
|
|
235
235
|
if (pat && accountId) {
|
|
236
236
|
try {
|
|
237
|
-
await storeSecret("
|
|
237
|
+
await storeSecret("pat", pat);
|
|
238
238
|
await storeSecret("account_id", accountId);
|
|
239
239
|
console.log(
|
|
240
240
|
chalk.green(
|
|
241
|
-
"\n✅
|
|
241
|
+
"\n✅ PAT token and Account ID stored securely. They will be used for future organization-related requests.\n"
|
|
242
242
|
)
|
|
243
243
|
);
|
|
244
244
|
} catch (error) {
|
|
245
245
|
console.error(
|
|
246
246
|
chalk.red(
|
|
247
|
-
`\n❌ Failed to store credentials: ${error.message || error}\n`
|
|
247
|
+
`\n❌ Failed to store PAT credentials: ${error.message || error}\n`
|
|
248
248
|
)
|
|
249
249
|
);
|
|
250
250
|
}
|
|
@@ -271,7 +271,7 @@ async function handlePatLogin(patFromArg, accountIdFromArg) {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
try {
|
|
274
|
-
await storeSecret("
|
|
274
|
+
await storeSecret("pat", pat);
|
|
275
275
|
await storeSecret("account_id", accountId);
|
|
276
276
|
console.log(
|
|
277
277
|
chalk.green(
|
package/commands/serverless.js
CHANGED
|
@@ -267,7 +267,7 @@ async function handleCreate(args = []) {
|
|
|
267
267
|
*/
|
|
268
268
|
async function checkServerlessExists(name) {
|
|
269
269
|
const env = await getCurrentEnv();
|
|
270
|
-
if (!env || !env.token) {
|
|
270
|
+
if (!env || !env.token || !env.session) {
|
|
271
271
|
return null; // Can't check without auth, let the create call handle auth error
|
|
272
272
|
}
|
|
273
273
|
|
|
@@ -379,7 +379,7 @@ async function handleCodeTypeCreate(
|
|
|
379
379
|
|
|
380
380
|
// Get authentication credentials
|
|
381
381
|
const env = await getCurrentEnv();
|
|
382
|
-
if (!env || !env.token) {
|
|
382
|
+
if (!env || !env.token || !env.session) {
|
|
383
383
|
console.error(chalk.red("\n❌ Not authenticated. Please login first."));
|
|
384
384
|
console.log(chalk.yellow(" Run: boltic login"));
|
|
385
385
|
return;
|
|
@@ -523,7 +523,7 @@ async function handleGitTypeCreate(
|
|
|
523
523
|
|
|
524
524
|
// Get authentication credentials first
|
|
525
525
|
const env = await getCurrentEnv();
|
|
526
|
-
if (!env || !env.token) {
|
|
526
|
+
if (!env || !env.token || !env.session) {
|
|
527
527
|
console.error(chalk.red("\n❌ Not authenticated. Please login first."));
|
|
528
528
|
console.log(chalk.yellow(" Run: boltic login"));
|
|
529
529
|
// Cleanup the created directory
|
|
@@ -2350,7 +2350,7 @@ async function handleStatus(args = []) {
|
|
|
2350
2350
|
lastStatus = status;
|
|
2351
2351
|
} else if (iteration % 3 === 0) {
|
|
2352
2352
|
// Show a dot every 3 iterations to indicate it's still polling
|
|
2353
|
-
process.stdout.write(chalk.dim("
|
|
2353
|
+
process.stdout.write(chalk.dim(".\n"));
|
|
2354
2354
|
}
|
|
2355
2355
|
|
|
2356
2356
|
// Check if we've reached a terminal state
|
package/helper/error.js
CHANGED
|
@@ -6,10 +6,49 @@ const ErrorType = {
|
|
|
6
6
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
7
7
|
AUTH_ERROR: "AUTH_ERROR",
|
|
8
8
|
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
9
|
+
NOT_FOUND_ERROR: "NOT_FOUND_ERROR",
|
|
9
10
|
CONFIG_ERROR: "CONFIG_ERROR",
|
|
10
11
|
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
11
12
|
};
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Extract the most meaningful error message from an API response body.
|
|
16
|
+
* Checks every known backend format before giving up.
|
|
17
|
+
*/
|
|
18
|
+
const extractApiMessage = (data) => {
|
|
19
|
+
if (!data) return null;
|
|
20
|
+
if (typeof data === "string") return data;
|
|
21
|
+
|
|
22
|
+
// Backend standard: { error: { message } }
|
|
23
|
+
if (data.error?.message) return data.error.message;
|
|
24
|
+
|
|
25
|
+
// Flat: { message }
|
|
26
|
+
if (typeof data.message === "string") return data.message;
|
|
27
|
+
|
|
28
|
+
// Validation array in meta: { error: { meta: { errors: [...] } } }
|
|
29
|
+
if (data.error?.meta?.errors?.length) {
|
|
30
|
+
return data.error.meta.errors
|
|
31
|
+
.map((e) =>
|
|
32
|
+
typeof e === "string" ? e : e.message || JSON.stringify(e)
|
|
33
|
+
)
|
|
34
|
+
.join("; ");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Top-level errors array: { errors: [...] }
|
|
38
|
+
if (Array.isArray(data.errors) && data.errors.length) {
|
|
39
|
+
return data.errors
|
|
40
|
+
.map((e) =>
|
|
41
|
+
typeof e === "string" ? e : e.message || JSON.stringify(e)
|
|
42
|
+
)
|
|
43
|
+
.join("; ");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fallback: { detail } (some frameworks)
|
|
47
|
+
if (typeof data.detail === "string") return data.detail;
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
};
|
|
51
|
+
|
|
13
52
|
// Format error message based on error type and response
|
|
14
53
|
const formatErrorMessage = (error) => {
|
|
15
54
|
if (!error)
|
|
@@ -21,14 +60,14 @@ const formatErrorMessage = (error) => {
|
|
|
21
60
|
// Handle API response errors
|
|
22
61
|
if (error.response) {
|
|
23
62
|
const { status, data } = error.response;
|
|
63
|
+
const apiMessage = extractApiMessage(data);
|
|
24
64
|
|
|
25
65
|
// Authentication errors
|
|
26
66
|
if (status === 401 || status === 403) {
|
|
27
67
|
return {
|
|
28
68
|
type: ErrorType.AUTH_ERROR,
|
|
29
69
|
message:
|
|
30
|
-
|
|
31
|
-
"Authentication failed. Please login again.",
|
|
70
|
+
apiMessage || "Authentication failed. Please login again.",
|
|
32
71
|
};
|
|
33
72
|
}
|
|
34
73
|
|
|
@@ -37,7 +76,25 @@ const formatErrorMessage = (error) => {
|
|
|
37
76
|
return {
|
|
38
77
|
type: ErrorType.VALIDATION_ERROR,
|
|
39
78
|
message:
|
|
40
|
-
|
|
79
|
+
apiMessage || "Invalid request. Please check your input.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Not found errors
|
|
84
|
+
if (status === 404) {
|
|
85
|
+
return {
|
|
86
|
+
type: ErrorType.NOT_FOUND_ERROR,
|
|
87
|
+
message: apiMessage || "The requested resource was not found.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Conflict errors
|
|
92
|
+
if (status === 409) {
|
|
93
|
+
return {
|
|
94
|
+
type: ErrorType.API_ERROR,
|
|
95
|
+
message:
|
|
96
|
+
apiMessage ||
|
|
97
|
+
"A conflict occurred with the current state of the resource.",
|
|
41
98
|
};
|
|
42
99
|
}
|
|
43
100
|
|
|
@@ -46,7 +103,7 @@ const formatErrorMessage = (error) => {
|
|
|
46
103
|
return {
|
|
47
104
|
type: ErrorType.API_ERROR,
|
|
48
105
|
message:
|
|
49
|
-
|
|
106
|
+
apiMessage ||
|
|
50
107
|
"Server error occurred. Please try again later.",
|
|
51
108
|
};
|
|
52
109
|
}
|
|
@@ -54,7 +111,7 @@ const formatErrorMessage = (error) => {
|
|
|
54
111
|
// Default API error
|
|
55
112
|
return {
|
|
56
113
|
type: ErrorType.API_ERROR,
|
|
57
|
-
message:
|
|
114
|
+
message: apiMessage || `API Error: ${status}`,
|
|
58
115
|
};
|
|
59
116
|
}
|
|
60
117
|
|
|
@@ -67,6 +124,14 @@ const formatErrorMessage = (error) => {
|
|
|
67
124
|
};
|
|
68
125
|
}
|
|
69
126
|
|
|
127
|
+
// Timeout errors
|
|
128
|
+
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
|
129
|
+
return {
|
|
130
|
+
type: ErrorType.NETWORK_ERROR,
|
|
131
|
+
message: "Request timed out. Please try again.",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
70
135
|
// Configuration errors
|
|
71
136
|
if (error.code === "ENOENT") {
|
|
72
137
|
return {
|
|
@@ -86,37 +151,32 @@ const formatErrorMessage = (error) => {
|
|
|
86
151
|
const handleError = (error) => {
|
|
87
152
|
const formattedError = formatErrorMessage(error);
|
|
88
153
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
chalk.red("\n❌ Configuration Error:"),
|
|
114
|
-
formattedError.message
|
|
115
|
-
);
|
|
116
|
-
break;
|
|
117
|
-
default:
|
|
118
|
-
console.error(chalk.red("\n❌ Error:"), formattedError.message);
|
|
154
|
+
const labels = {
|
|
155
|
+
[ErrorType.AUTH_ERROR]: "Authentication Error",
|
|
156
|
+
[ErrorType.API_ERROR]: "API Error",
|
|
157
|
+
[ErrorType.NETWORK_ERROR]: "Network Error",
|
|
158
|
+
[ErrorType.VALIDATION_ERROR]: "Validation Error",
|
|
159
|
+
[ErrorType.NOT_FOUND_ERROR]: "Not Found",
|
|
160
|
+
[ErrorType.CONFIG_ERROR]: "Configuration Error",
|
|
161
|
+
[ErrorType.UNKNOWN_ERROR]: "Error",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const label = labels[formattedError.type] || "Error";
|
|
165
|
+
console.error(chalk.red(`\n❌ ${label}:`), formattedError.message);
|
|
166
|
+
|
|
167
|
+
// Always show API response details for debugging when there is a response
|
|
168
|
+
if (error?.response) {
|
|
169
|
+
const { status, data, config } = error.response;
|
|
170
|
+
console.error(
|
|
171
|
+
chalk.gray(
|
|
172
|
+
`\n ${config?.method?.toUpperCase() || "?"} ${config?.url || "?"} → ${status}`
|
|
173
|
+
)
|
|
174
|
+
);
|
|
175
|
+
if (data) {
|
|
176
|
+
console.error(chalk.gray(` Response: ${JSON.stringify(data)}`));
|
|
177
|
+
}
|
|
119
178
|
}
|
|
179
|
+
|
|
120
180
|
process.exit(1);
|
|
121
181
|
};
|
|
122
182
|
|
package/helper/secure-storage.js
CHANGED
|
@@ -1,175 +1,72 @@
|
|
|
1
|
-
import
|
|
2
|
-
import os from "os";
|
|
3
|
-
import path from "path";
|
|
1
|
+
import keytar from "keytar";
|
|
4
2
|
|
|
5
3
|
const SERVICE_NAME = "boltic-cli";
|
|
6
4
|
|
|
7
|
-
// Mapping from credential keys to environment variable names.
|
|
8
|
-
// Checked last (lowest priority) so explicit login always wins.
|
|
9
|
-
const ENV_VAR_MAP = {
|
|
10
|
-
token: "BOLTIC_TOKEN",
|
|
11
|
-
account_id: "BOLTIC_ACCOUNT_ID",
|
|
12
|
-
session: "BOLTIC_SESSION",
|
|
13
|
-
environment: "BOLTIC_ENVIRONMENT",
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
// File-based credential store used when keytar (OS keychain) is unavailable.
|
|
17
|
-
// Credentials are stored as plain JSON, readable only by the current user.
|
|
18
|
-
const CRED_FILE = path.join(os.homedir(), ".boltic", "credentials.json");
|
|
19
|
-
|
|
20
|
-
const readCredFile = () => {
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(fs.readFileSync(CRED_FILE, "utf-8"));
|
|
23
|
-
} catch {
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const writeCredFile = (data) => {
|
|
29
|
-
const dir = path.dirname(CRED_FILE);
|
|
30
|
-
if (!fs.existsSync(dir)) {
|
|
31
|
-
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
32
|
-
}
|
|
33
|
-
fs.writeFileSync(CRED_FILE, JSON.stringify(data, null, 2), {
|
|
34
|
-
mode: 0o600,
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Lazy-load keytar via dynamic import so that a missing native dependency
|
|
39
|
-
// (e.g. libsecret on Linux CI runners) is caught at call time rather than
|
|
40
|
-
// crashing the process at startup with ERR_DLOPEN_FAILED.
|
|
41
|
-
let _keytar;
|
|
42
|
-
let _keytarAttempted = false;
|
|
43
|
-
|
|
44
|
-
const getKeytar = async () => {
|
|
45
|
-
if (_keytarAttempted) return _keytar;
|
|
46
|
-
_keytarAttempted = true;
|
|
47
|
-
try {
|
|
48
|
-
_keytar = (await import("keytar")).default;
|
|
49
|
-
} catch {
|
|
50
|
-
// Native library not available (e.g. libsecret missing on CI runners).
|
|
51
|
-
_keytar = null;
|
|
52
|
-
}
|
|
53
|
-
return _keytar;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
5
|
/**
|
|
57
|
-
* Store a secret value
|
|
58
|
-
*
|
|
6
|
+
* Store a secret value securely using keytar
|
|
7
|
+
* @param {string} key - The key under which to store the secret
|
|
8
|
+
* @param {string} value - The secret value to store
|
|
9
|
+
* @returns {Promise<void>}
|
|
59
10
|
*/
|
|
60
11
|
export const storeSecret = async (key, value) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
} catch {
|
|
67
|
-
// fall through to file store
|
|
68
|
-
}
|
|
12
|
+
try {
|
|
13
|
+
await keytar.setPassword(SERVICE_NAME, key, value);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error(`Error storing secret for ${key}:`, error.message);
|
|
16
|
+
throw error;
|
|
69
17
|
}
|
|
70
|
-
// File-based fallback (CI / headless environments)
|
|
71
|
-
const data = readCredFile();
|
|
72
|
-
data[key] = value;
|
|
73
|
-
writeCredFile(data);
|
|
74
18
|
};
|
|
75
19
|
|
|
76
20
|
/**
|
|
77
|
-
* Retrieve a secret value
|
|
78
|
-
*
|
|
21
|
+
* Retrieve a secret value using keytar
|
|
22
|
+
* @param {string} key - The key of the secret to retrieve
|
|
23
|
+
* @returns {Promise<string|null>} The secret value or null if not found
|
|
79
24
|
*/
|
|
80
25
|
export const getSecret = async (key) => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} catch {
|
|
87
|
-
// fall through
|
|
88
|
-
}
|
|
26
|
+
try {
|
|
27
|
+
return await keytar.getPassword(SERVICE_NAME, key);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error(`Error retrieving secret for ${key}:`, error.message);
|
|
30
|
+
return null;
|
|
89
31
|
}
|
|
90
|
-
// File store fallback
|
|
91
|
-
const val = readCredFile()[key];
|
|
92
|
-
if (val != null) return val;
|
|
93
|
-
// Env var fallback
|
|
94
|
-
return process.env[ENV_VAR_MAP[key]] || null;
|
|
95
32
|
};
|
|
96
33
|
|
|
97
34
|
/**
|
|
98
|
-
* Delete a secret value
|
|
99
|
-
*
|
|
35
|
+
* Delete a secret value using keytar
|
|
36
|
+
* @param {string} key - The key of the secret to delete
|
|
37
|
+
* @returns {Promise<boolean>} True if deletion was successful
|
|
100
38
|
*/
|
|
101
39
|
export const deleteSecret = async (key) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.error(`Error deleting secret for ${key}:`, error.message);
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// File store fallback
|
|
112
|
-
const data = readCredFile();
|
|
113
|
-
if (key in data) {
|
|
114
|
-
delete data[key];
|
|
115
|
-
writeCredFile(data);
|
|
40
|
+
try {
|
|
41
|
+
return await keytar.deletePassword(SERVICE_NAME, key);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`Error deleting secret for ${key}:`, error.message);
|
|
44
|
+
return false;
|
|
116
45
|
}
|
|
117
|
-
return true;
|
|
118
46
|
};
|
|
119
47
|
|
|
120
48
|
/**
|
|
121
|
-
* Retrieve all secrets
|
|
122
|
-
*
|
|
49
|
+
* Retrieve all secrets stored using keytar
|
|
50
|
+
* @returns {Promise<Array<{account: string, password: string}>|null>} An array of secret objects or null if an error occurs
|
|
123
51
|
*/
|
|
52
|
+
|
|
124
53
|
export const getAllSecrets = async () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} catch {
|
|
131
|
-
// fall through
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// File store fallback
|
|
135
|
-
const fileData = readCredFile();
|
|
136
|
-
if (Object.keys(fileData).length > 0) {
|
|
137
|
-
return Object.entries(fileData).map(([account, password]) => ({
|
|
138
|
-
account,
|
|
139
|
-
password,
|
|
140
|
-
}));
|
|
141
|
-
}
|
|
142
|
-
// Env var fallback
|
|
143
|
-
const secrets = [];
|
|
144
|
-
for (const [key, envVar] of Object.entries(ENV_VAR_MAP)) {
|
|
145
|
-
if (process.env[envVar]) {
|
|
146
|
-
secrets.push({ account: key, password: process.env[envVar] });
|
|
147
|
-
}
|
|
54
|
+
try {
|
|
55
|
+
return await keytar.findCredentials(SERVICE_NAME);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Error retrieving all secrets:`, error.message);
|
|
58
|
+
return null;
|
|
148
59
|
}
|
|
149
|
-
return secrets.length > 0 ? secrets : null;
|
|
150
60
|
};
|
|
151
61
|
|
|
152
62
|
export const deleteAllSecrets = async () => {
|
|
153
63
|
try {
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
secrets.map(({ account }) =>
|
|
161
|
-
keytar.deletePassword(SERVICE_NAME, account)
|
|
162
|
-
)
|
|
163
|
-
);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
} catch {
|
|
167
|
-
// fall through to file store
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
// File store fallback
|
|
171
|
-
if (fs.existsSync(CRED_FILE)) {
|
|
172
|
-
fs.unlinkSync(CRED_FILE);
|
|
64
|
+
const secrets = await getAllSecrets();
|
|
65
|
+
if (secrets && secrets.length > 0) {
|
|
66
|
+
const deletionPromises = secrets.map(
|
|
67
|
+
async ({ account }) => await deleteSecret(account)
|
|
68
|
+
);
|
|
69
|
+
await Promise.all(deletionPromises);
|
|
173
70
|
}
|
|
174
71
|
} catch (error) {
|
|
175
72
|
console.error(`Error deleting all secrets:`, error.message);
|
package/helper/serverless.js
CHANGED
|
@@ -14,10 +14,10 @@ import ora from "ora";
|
|
|
14
14
|
// Supported languages and their versions
|
|
15
15
|
export const SUPPORTED_LANGUAGES = ["nodejs", "python", "golang", "java"];
|
|
16
16
|
export const LANGUAGE_VERSIONS = {
|
|
17
|
-
nodejs: "
|
|
17
|
+
nodejs: "24",
|
|
18
18
|
python: "3",
|
|
19
|
-
golang: "1.
|
|
20
|
-
java: "
|
|
19
|
+
golang: "1.24",
|
|
20
|
+
java: "21",
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
// Handler mapping per language
|
|
@@ -935,7 +935,7 @@ public class AutogenIndex {
|
|
|
935
935
|
function getGoModContent(appName) {
|
|
936
936
|
return `module ${appName}
|
|
937
937
|
|
|
938
|
-
go 1.
|
|
938
|
+
go 1.24
|
|
939
939
|
`;
|
|
940
940
|
}
|
|
941
941
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boltic/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.45",
|
|
4
4
|
"description": "Professional CLI for interacting with the Boltic platform — create, manage, and publish integrations, serverless functions, workflows, MCPs, and more with enterprise-grade features and a seamless developer experience",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"start": "node index.js",
|
|
42
42
|
"dev": "nodemon index.js",
|
|
43
|
+
"dev:debug": "nodemon --inspect index.js",
|
|
43
44
|
"test": "jest",
|
|
44
45
|
"test:watch": "jest --watch",
|
|
45
46
|
"test:coverage": "jest --coverage",
|