smoonb 1.0.11 → 1.0.13
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/CHANGELOG.md +167 -150
- package/README.md +695 -687
- package/README.pt-BR.md +697 -687
- package/npm-shrinkwrap.json +2030 -0
- package/package.json +71 -68
- package/src/commands/backup/index.js +39 -21
- package/src/commands/backup/outcome.js +16 -0
- package/src/commands/backup/steps/00-license.js +427 -427
- package/src/commands/backup/steps/02-database.js +6 -6
- package/src/commands/backup/steps/03-database-separated.js +129 -161
- package/src/commands/backup/steps/04-database-settings.js +24 -40
- package/src/commands/backup/steps/07-storage.js +11 -10
- package/src/commands/backup/steps/08-custom-roles.js +5 -55
- package/src/commands/backup/steps/10-supabase-temp.js +0 -9
- package/src/commands/backup/steps/11-migrations.js +30 -74
- package/src/i18n/locales/en.json +825 -824
- package/src/i18n/locales/pt-BR.json +824 -823
- package/src/interactive/envMapper.js +6 -12
- package/src/utils/docker.js +6 -53
- package/src/utils/pg-dump-diagnostics.js +68 -64
- package/src/utils/supabaseCli.js +26 -32
|
@@ -1,427 +1,427 @@
|
|
|
1
|
-
const https = require('https');
|
|
2
|
-
const inquirer = require('inquirer');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { readEnvFile
|
|
5
|
-
const { APP_CONFIG } = require('../../../config/appConfig');
|
|
6
|
-
const { t } = require('../../../i18n');
|
|
7
|
-
const ui = require('../../../utils/cliUi');
|
|
8
|
-
const { getOrCreateInstallationId, maskInstallationId } = require('../../../utils/installationId');
|
|
9
|
-
const {
|
|
10
|
-
createCorrelationId,
|
|
11
|
-
maskSecret,
|
|
12
|
-
safeStringifyHeaders,
|
|
13
|
-
truncateBody,
|
|
14
|
-
sanitizeResponseParsedForBundle,
|
|
15
|
-
classifyRequestError,
|
|
16
|
-
buildSupportBundle,
|
|
17
|
-
formatBundleForTerminal,
|
|
18
|
-
BODY_TRUNCATE_BYTES_BUNDLE
|
|
19
|
-
} = require('../../../utils/supportDiagnostics');
|
|
20
|
-
|
|
21
|
-
const LICENSE_KEY_ENV = 'SMOONB_LICENSE_KEY';
|
|
22
|
-
const REQUEST_TIMEOUT_MS = 15000;
|
|
23
|
-
const ENDPOINT_PATH = '/v1/license/validate';
|
|
24
|
-
|
|
25
|
-
function maskLicense(key) {
|
|
26
|
-
if (!key || typeof key !== 'string') return '***';
|
|
27
|
-
const s = key.trim();
|
|
28
|
-
if (s.length <= 10) return '***';
|
|
29
|
-
return `${s.slice(0, 6)}...${s.slice(-4)}`;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* POST com https; retorna statusCode, headers, body (parsed), rawBody (string).
|
|
34
|
-
*/
|
|
35
|
-
function httpsPost(url, body, headers, timeoutMs) {
|
|
36
|
-
const u = new URL(url);
|
|
37
|
-
const bodyStr = JSON.stringify(body);
|
|
38
|
-
const opts = {
|
|
39
|
-
hostname: u.hostname,
|
|
40
|
-
port: u.port || 443,
|
|
41
|
-
path: u.pathname + u.search,
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
'Content-Length': Buffer.byteLength(bodyStr),
|
|
46
|
-
...headers
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const req = https.request(opts, (res) => {
|
|
51
|
-
const chunks = [];
|
|
52
|
-
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
53
|
-
res.on('end', () => {
|
|
54
|
-
const rawBody = Buffer.concat(chunks).toString('utf8');
|
|
55
|
-
let bodyParsed = {};
|
|
56
|
-
try {
|
|
57
|
-
bodyParsed = rawBody ? JSON.parse(rawBody) : {};
|
|
58
|
-
} catch {
|
|
59
|
-
bodyParsed = {};
|
|
60
|
-
}
|
|
61
|
-
resolve({
|
|
62
|
-
statusCode: res.statusCode,
|
|
63
|
-
statusMessage: res.statusMessage,
|
|
64
|
-
headers: res.headers,
|
|
65
|
-
body: bodyParsed,
|
|
66
|
-
rawBody
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
req.on('error', reject);
|
|
71
|
-
req.setTimeout(timeoutMs, () => {
|
|
72
|
-
req.destroy();
|
|
73
|
-
const e = new Error('Request timeout');
|
|
74
|
-
e.code = 'ETIMEDOUT';
|
|
75
|
-
reject(e);
|
|
76
|
-
});
|
|
77
|
-
req.write(bodyStr);
|
|
78
|
-
req.end();
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Exibe erro estruturado + instruções de suporte + bundle (pronto para colar).
|
|
84
|
-
* Usa ui (sem gray/dim) para legibilidade em Windows e copy/paste.
|
|
85
|
-
* @param {Function} getT
|
|
86
|
-
* @param {string} appUrl - APP_CONFIG.appUrl
|
|
87
|
-
* @param {object} opts
|
|
88
|
-
*/
|
|
89
|
-
function printLicenseValidationError(getT, appUrl, opts) {
|
|
90
|
-
const {
|
|
91
|
-
correlationId,
|
|
92
|
-
kind,
|
|
93
|
-
httpStatus,
|
|
94
|
-
httpStatusText,
|
|
95
|
-
endpoint,
|
|
96
|
-
fullUrl,
|
|
97
|
-
timeoutMs,
|
|
98
|
-
licenseMasked,
|
|
99
|
-
version,
|
|
100
|
-
responseHeaders,
|
|
101
|
-
responseBodyTruncated,
|
|
102
|
-
err
|
|
103
|
-
} = opts;
|
|
104
|
-
|
|
105
|
-
const bundleParams = {
|
|
106
|
-
correlationId,
|
|
107
|
-
product: 'smoonb CLI',
|
|
108
|
-
version,
|
|
109
|
-
timestamp: new Date().toISOString(),
|
|
110
|
-
os: process.platform,
|
|
111
|
-
arch: process.arch,
|
|
112
|
-
nodeVersion: process.version,
|
|
113
|
-
command: `smoonb ${opts.command || 'backup'}`,
|
|
114
|
-
licenseMasked: licenseMasked || '***',
|
|
115
|
-
apiBaseUrl: fullUrl ? fullUrl.replace(/\/[^/]*$/, '') : APP_CONFIG.apiBaseUrl,
|
|
116
|
-
endpoint: endpoint || ENDPOINT_PATH,
|
|
117
|
-
timeoutMs,
|
|
118
|
-
telemetryEnabled: (process.env.SMOONB_TELEMETRY_ENABLED !== 'false').toString(),
|
|
119
|
-
installationIdMasked: opts.installationIdMasked
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
if (httpStatus != null) {
|
|
123
|
-
bundleParams.httpStatus = httpStatus;
|
|
124
|
-
bundleParams.httpStatusText = httpStatusText || '';
|
|
125
|
-
bundleParams.responseHeaders = responseHeaders;
|
|
126
|
-
bundleParams.responseBodyRaw = responseBodyTruncated || undefined;
|
|
127
|
-
}
|
|
128
|
-
if (err) {
|
|
129
|
-
bundleParams.errorName = err.name;
|
|
130
|
-
bundleParams.errorMessage = (err.message || '').replace(/\r?\n/g, ' ');
|
|
131
|
-
bundleParams.errorCode = err.code;
|
|
132
|
-
bundleParams.errno = err.errno;
|
|
133
|
-
if (process.env.SMOONB_DEBUG === 'true') {
|
|
134
|
-
bundleParams.stack = err.stack;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const bundleText = buildSupportBundle(bundleParams);
|
|
139
|
-
const typeKey = kind === 'http' ? 'license.error.typeHttp'
|
|
140
|
-
: kind === 'tls' ? 'license.error.typeTls'
|
|
141
|
-
: kind === 'parse' ? 'license.error.typeParse'
|
|
142
|
-
: 'license.error.typeNetwork';
|
|
143
|
-
|
|
144
|
-
ui.errorBold(`\n❌ ${getT('license.error.title')}\n`);
|
|
145
|
-
ui.info(` ${getT('license.error.whatHappened')}`);
|
|
146
|
-
ui.info(` ${getT(typeKey)}`);
|
|
147
|
-
if (kind === 'network' || kind === 'tls') {
|
|
148
|
-
ui.hint(` ${getT('license.error.testConnectivity', { url: APP_CONFIG.healthCheckUrl || appUrl + '/api/health' })}`);
|
|
149
|
-
}
|
|
150
|
-
if (httpStatus != null) {
|
|
151
|
-
ui.info(` ${getT('license.error.status', { status: `${httpStatus} ${(httpStatusText || '').trim()}` })}`);
|
|
152
|
-
}
|
|
153
|
-
if (endpoint || fullUrl) {
|
|
154
|
-
ui.info(` ${getT('license.error.endpoint', { endpoint: fullUrl || endpoint })}`);
|
|
155
|
-
}
|
|
156
|
-
ui.info(` ${getT('license.error.correlationId', { id: correlationId })}\n`);
|
|
157
|
-
|
|
158
|
-
ui.link(` ${getT('license.error.howToHelp')}`);
|
|
159
|
-
ui.info(` ${getT('license.error.supportStep1', { url: appUrl })}`);
|
|
160
|
-
ui.info(` ${getT('license.error.supportStep2')}`);
|
|
161
|
-
ui.info(` ${getT('license.error.supportStep3')}`);
|
|
162
|
-
ui.info(` ${getT('license.error.supportStep4')}`);
|
|
163
|
-
ui.info(` ${getT('license.error.supportStep5')}\n`);
|
|
164
|
-
|
|
165
|
-
ui.block(formatBundleForTerminal(bundleText));
|
|
166
|
-
ui.info('');
|
|
167
|
-
ui.link(` ${getT('license.error.visit', { url: appUrl })}\n`);
|
|
168
|
-
ui.hint(` ${getT('license.error.subjectSuggestion', { id: correlationId })}`);
|
|
169
|
-
ui.hint(` ${getT('license.error.messageSuggestion')}\n`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Exibe falha quando servidor retorna allow === false (reason literal + CTA + bundle).
|
|
174
|
-
* Se reason/reasonCode === LICENSE_ALREADY_BOUND, exibe mensagem específica de revogar e gerar nova.
|
|
175
|
-
*/
|
|
176
|
-
function printLicenseDeniedByServer(getT, appUrl, opts) {
|
|
177
|
-
const {
|
|
178
|
-
correlationId,
|
|
179
|
-
correlationIdEchoed,
|
|
180
|
-
reason,
|
|
181
|
-
reasonCode,
|
|
182
|
-
fullUrl,
|
|
183
|
-
rawBody,
|
|
184
|
-
resBody,
|
|
185
|
-
licenseMasked,
|
|
186
|
-
version,
|
|
187
|
-
command,
|
|
188
|
-
installationIdMasked
|
|
189
|
-
} = opts;
|
|
190
|
-
const displayId = correlationIdEchoed || correlationId;
|
|
191
|
-
const responseBodyRaw = rawBody ? truncateBody(rawBody, BODY_TRUNCATE_BYTES_BUNDLE) : '';
|
|
192
|
-
const responseParsed = sanitizeResponseParsedForBundle(resBody);
|
|
193
|
-
const isAlreadyBound = (reasonCode && String(reasonCode).toUpperCase() === 'LICENSE_ALREADY_BOUND') ||
|
|
194
|
-
(reason && String(reason).toUpperCase() === 'LICENSE_ALREADY_BOUND');
|
|
195
|
-
|
|
196
|
-
ui.errorBold(`\n❌ ${getT('license.denied.title')}\n`);
|
|
197
|
-
if (isAlreadyBound) {
|
|
198
|
-
ui.info(` ${getT('license.denied.alreadyBound')}`);
|
|
199
|
-
ui.info(` ${getT('license.denied.alreadyBoundAction', { url: appUrl })}`);
|
|
200
|
-
} else {
|
|
201
|
-
ui.info(` ${getT('license.denied.reason', { reason: reason || 'No reason provided' })}`);
|
|
202
|
-
}
|
|
203
|
-
ui.info(` ${getT('license.denied.correlationId', { id: displayId })}\n`);
|
|
204
|
-
ui.link(` ${getT('license.error.howToHelp')}`);
|
|
205
|
-
ui.info(` ${getT('license.error.supportStep1', { url: appUrl })}`);
|
|
206
|
-
ui.info(` ${getT('license.error.supportStep2')}`);
|
|
207
|
-
ui.info(` ${getT('license.error.supportStep3')}`);
|
|
208
|
-
ui.info(` ${getT('license.error.supportStep4')}`);
|
|
209
|
-
ui.info(` ${getT('license.error.supportStep5')}\n`);
|
|
210
|
-
|
|
211
|
-
const bundleParams = {
|
|
212
|
-
correlationId: displayId,
|
|
213
|
-
product: 'smoonb CLI',
|
|
214
|
-
version,
|
|
215
|
-
timestamp: new Date().toISOString(),
|
|
216
|
-
os: process.platform,
|
|
217
|
-
arch: process.arch,
|
|
218
|
-
nodeVersion: process.version,
|
|
219
|
-
command: `smoonb ${command || 'backup'}`,
|
|
220
|
-
licenseMasked: licenseMasked || '***',
|
|
221
|
-
apiBaseUrl: fullUrl ? fullUrl.replace(/\/[^/]*$/, '') : APP_CONFIG.apiBaseUrl,
|
|
222
|
-
endpoint: ENDPOINT_PATH,
|
|
223
|
-
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
224
|
-
telemetryEnabled: (process.env.SMOONB_TELEMETRY_ENABLED !== 'false').toString(),
|
|
225
|
-
httpStatus: 200,
|
|
226
|
-
httpStatusText: 'OK',
|
|
227
|
-
responseBodyRaw: responseBodyRaw || undefined,
|
|
228
|
-
responseParsed: responseParsed !== '(none)' ? responseParsed : undefined,
|
|
229
|
-
correlationIdEchoed: correlationIdEchoed && correlationIdEchoed !== correlationId ? correlationIdEchoed : undefined,
|
|
230
|
-
installationIdMasked: installationIdMasked || undefined,
|
|
231
|
-
reason: reason != null ? String(reason) : (reasonCode != null ? String(reasonCode) : undefined)
|
|
232
|
-
};
|
|
233
|
-
const bundleText = buildSupportBundle(bundleParams);
|
|
234
|
-
ui.block(formatBundleForTerminal(bundleText));
|
|
235
|
-
ui.info('');
|
|
236
|
-
ui.link(` ${getT('license.error.visit', { url: appUrl })}\n`);
|
|
237
|
-
ui.hint(` ${getT('license.error.subjectSuggestion', { id: displayId })}`);
|
|
238
|
-
ui.hint(` ${getT('license.error.messageSuggestion')}\n`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Step 00: Validação de licença via backend.
|
|
243
|
-
* Roda antes de todos os outros steps.
|
|
244
|
-
* Resolve SMOONB_LICENSE_KEY: env > .env.local > prompt. Persiste se prompt.
|
|
245
|
-
* Sem cache local; falha de rede/servidor = abortar.
|
|
246
|
-
* @param {{ envPath: string, command: string }} options
|
|
247
|
-
* @returns {Promise<{ license: object, smoonbToken: string }>}
|
|
248
|
-
*/
|
|
249
|
-
module.exports = async (options) => {
|
|
250
|
-
const getT = global.smoonbI18n?.t || t;
|
|
251
|
-
const envPath = options.envPath || path.join(process.cwd(), '.env.local');
|
|
252
|
-
const command = options.command || 'backup';
|
|
253
|
-
const appUrl = APP_CONFIG.appUrl;
|
|
254
|
-
const fullUrl = `${APP_CONFIG.apiBaseUrl}${ENDPOINT_PATH}`;
|
|
255
|
-
|
|
256
|
-
let licenseKey = (process.env[LICENSE_KEY_ENV] || '').toString().trim();
|
|
257
|
-
if (!licenseKey) {
|
|
258
|
-
const currentEnv = await readEnvFile(envPath);
|
|
259
|
-
licenseKey = (currentEnv[LICENSE_KEY_ENV] || '').toString().trim();
|
|
260
|
-
}
|
|
261
|
-
if (!licenseKey) {
|
|
262
|
-
const { licenseKey: prompted } = await inquirer.prompt([{
|
|
263
|
-
type: 'input',
|
|
264
|
-
name: 'licenseKey',
|
|
265
|
-
message: getT('license.promptPaste'),
|
|
266
|
-
prefix: ''
|
|
267
|
-
}]);
|
|
268
|
-
licenseKey = (prompted || '').toString().trim();
|
|
269
|
-
if (!licenseKey) {
|
|
270
|
-
ui.error(`❌ ${getT('license.required')}`);
|
|
271
|
-
ui.link(` ${appUrl}`);
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
let cliVersion = '0.0.0';
|
|
279
|
-
try {
|
|
280
|
-
cliVersion = require('../../../../package.json').version;
|
|
281
|
-
} catch {
|
|
282
|
-
// ignore
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
let installationId;
|
|
286
|
-
try {
|
|
287
|
-
installationId = await getOrCreateInstallationId();
|
|
288
|
-
} catch (err) {
|
|
289
|
-
ui.error(`❌ ${getT('license.error.title')}`);
|
|
290
|
-
ui.info(` ${getT('license.error.whatHappened')}`);
|
|
291
|
-
ui.info(` Could not create or read installation ID: ${(err && err.message) || err}`);
|
|
292
|
-
ui.link(` ${getT('license.error.visit', { url: appUrl })}`);
|
|
293
|
-
process.exit(1);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const correlationId = createCorrelationId();
|
|
297
|
-
const body = {
|
|
298
|
-
licenseKey,
|
|
299
|
-
installationId,
|
|
300
|
-
correlationId,
|
|
301
|
-
cliVersion,
|
|
302
|
-
command,
|
|
303
|
-
platform: process.platform,
|
|
304
|
-
arch: process.arch,
|
|
305
|
-
nodeVersion: process.version
|
|
306
|
-
};
|
|
307
|
-
const requestHeaders = {
|
|
308
|
-
'User-Agent': `smoonb-cli/${cliVersion}`,
|
|
309
|
-
'x-correlation-id': correlationId
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
let response;
|
|
313
|
-
try {
|
|
314
|
-
response = await httpsPost(
|
|
315
|
-
fullUrl,
|
|
316
|
-
body,
|
|
317
|
-
requestHeaders,
|
|
318
|
-
REQUEST_TIMEOUT_MS
|
|
319
|
-
);
|
|
320
|
-
} catch (err) {
|
|
321
|
-
const classification = classifyRequestError(err);
|
|
322
|
-
if (classification.hintUser) {
|
|
323
|
-
ui.warn(` ${classification.hintUser}`);
|
|
324
|
-
}
|
|
325
|
-
printLicenseValidationError(getT, appUrl, {
|
|
326
|
-
correlationId,
|
|
327
|
-
kind: classification.kind,
|
|
328
|
-
endpoint: ENDPOINT_PATH,
|
|
329
|
-
fullUrl,
|
|
330
|
-
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
331
|
-
licenseMasked: maskSecret(licenseKey),
|
|
332
|
-
version: cliVersion,
|
|
333
|
-
command,
|
|
334
|
-
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined,
|
|
335
|
-
err
|
|
336
|
-
});
|
|
337
|
-
process.exit(1);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const { statusCode, statusMessage, headers, body: resBody, rawBody } = response;
|
|
341
|
-
|
|
342
|
-
// Resposta 200 mas corpo inesperado (ex.: HTML em vez de JSON)
|
|
343
|
-
const looksLikeJson = rawBody && rawBody.trim().startsWith('{');
|
|
344
|
-
if (statusCode === 200 && !looksLikeJson && rawBody) {
|
|
345
|
-
const displayCorrelationId = resBody.correlationId || correlationId;
|
|
346
|
-
const responseHeadersStr = safeStringifyHeaders(headers);
|
|
347
|
-
const responseBodyTruncated = truncateBody(rawBody);
|
|
348
|
-
printLicenseValidationError(getT, appUrl, {
|
|
349
|
-
correlationId: displayCorrelationId,
|
|
350
|
-
kind: 'parse',
|
|
351
|
-
endpoint: ENDPOINT_PATH,
|
|
352
|
-
fullUrl,
|
|
353
|
-
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
354
|
-
licenseMasked: maskSecret(licenseKey),
|
|
355
|
-
version: cliVersion,
|
|
356
|
-
command,
|
|
357
|
-
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined,
|
|
358
|
-
responseHeaders: responseHeadersStr,
|
|
359
|
-
responseBodyTruncated
|
|
360
|
-
});
|
|
361
|
-
process.exit(1);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (statusCode !== 200) {
|
|
365
|
-
const displayCorrelationId = resBody.correlationId || correlationId;
|
|
366
|
-
const responseHeadersStr = safeStringifyHeaders(headers);
|
|
367
|
-
const responseBodyRaw = rawBody ? truncateBody(rawBody, BODY_TRUNCATE_BYTES_BUNDLE) : '';
|
|
368
|
-
printLicenseValidationError(getT, appUrl, {
|
|
369
|
-
correlationId: displayCorrelationId,
|
|
370
|
-
kind: 'http',
|
|
371
|
-
httpStatus: statusCode,
|
|
372
|
-
httpStatusText: statusMessage,
|
|
373
|
-
endpoint: ENDPOINT_PATH,
|
|
374
|
-
fullUrl,
|
|
375
|
-
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
376
|
-
licenseMasked: maskSecret(licenseKey),
|
|
377
|
-
version: cliVersion,
|
|
378
|
-
command,
|
|
379
|
-
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined,
|
|
380
|
-
responseHeaders: responseHeadersStr,
|
|
381
|
-
responseBodyTruncated: responseBodyRaw
|
|
382
|
-
});
|
|
383
|
-
process.exit(1);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// allow como boolean robusto: true ou "true" -> true; resto -> false
|
|
387
|
-
const allowRaw = resBody.allow;
|
|
388
|
-
const allow = allowRaw === true || (typeof allowRaw === 'string' && allowRaw.toLowerCase() === 'true');
|
|
389
|
-
|
|
390
|
-
if (allow === true) {
|
|
391
|
-
// Nunca negar quando allow === true
|
|
392
|
-
const status = resBody.status || '';
|
|
393
|
-
if (status === 'trial') {
|
|
394
|
-
ui.warn(` ⚠️ ${getT('license.trialNotice')}`);
|
|
395
|
-
}
|
|
396
|
-
const license = {
|
|
397
|
-
status: resBody.status,
|
|
398
|
-
plan: resBody.plan,
|
|
399
|
-
expiresAt: resBody.expiresAt,
|
|
400
|
-
accountId: resBody.accountId,
|
|
401
|
-
workspaceId: resBody.workspaceId
|
|
402
|
-
};
|
|
403
|
-
const smoonbToken = resBody.token || '';
|
|
404
|
-
return { license, smoonbToken };
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// allow === false: exibir reason do servidor literalmente + CTA + bundle
|
|
408
|
-
const serverReason = resBody.reason != null ? String(resBody.reason) : 'No reason provided';
|
|
409
|
-
const serverReasonCode = resBody.reasonCode != null ? String(resBody.reasonCode) : null;
|
|
410
|
-
const correlationIdEchoed = resBody.correlationId || null;
|
|
411
|
-
printLicenseDeniedByServer(getT, appUrl, {
|
|
412
|
-
correlationId,
|
|
413
|
-
correlationIdEchoed,
|
|
414
|
-
reason: serverReason,
|
|
415
|
-
reasonCode: serverReasonCode,
|
|
416
|
-
fullUrl,
|
|
417
|
-
rawBody,
|
|
418
|
-
resBody,
|
|
419
|
-
licenseMasked: maskSecret(licenseKey),
|
|
420
|
-
version: cliVersion,
|
|
421
|
-
command,
|
|
422
|
-
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined
|
|
423
|
-
});
|
|
424
|
-
process.exit(1);
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
module.exports.maskLicense = maskLicense;
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { readEnvFile } = require('../../../utils/env');
|
|
5
|
+
const { APP_CONFIG } = require('../../../config/appConfig');
|
|
6
|
+
const { t } = require('../../../i18n');
|
|
7
|
+
const ui = require('../../../utils/cliUi');
|
|
8
|
+
const { getOrCreateInstallationId, maskInstallationId } = require('../../../utils/installationId');
|
|
9
|
+
const {
|
|
10
|
+
createCorrelationId,
|
|
11
|
+
maskSecret,
|
|
12
|
+
safeStringifyHeaders,
|
|
13
|
+
truncateBody,
|
|
14
|
+
sanitizeResponseParsedForBundle,
|
|
15
|
+
classifyRequestError,
|
|
16
|
+
buildSupportBundle,
|
|
17
|
+
formatBundleForTerminal,
|
|
18
|
+
BODY_TRUNCATE_BYTES_BUNDLE
|
|
19
|
+
} = require('../../../utils/supportDiagnostics');
|
|
20
|
+
|
|
21
|
+
const LICENSE_KEY_ENV = 'SMOONB_LICENSE_KEY';
|
|
22
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
23
|
+
const ENDPOINT_PATH = '/v1/license/validate';
|
|
24
|
+
|
|
25
|
+
function maskLicense(key) {
|
|
26
|
+
if (!key || typeof key !== 'string') return '***';
|
|
27
|
+
const s = key.trim();
|
|
28
|
+
if (s.length <= 10) return '***';
|
|
29
|
+
return `${s.slice(0, 6)}...${s.slice(-4)}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* POST com https; retorna statusCode, headers, body (parsed), rawBody (string).
|
|
34
|
+
*/
|
|
35
|
+
function httpsPost(url, body, headers, timeoutMs) {
|
|
36
|
+
const u = new URL(url);
|
|
37
|
+
const bodyStr = JSON.stringify(body);
|
|
38
|
+
const opts = {
|
|
39
|
+
hostname: u.hostname,
|
|
40
|
+
port: u.port || 443,
|
|
41
|
+
path: u.pathname + u.search,
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'Content-Length': Buffer.byteLength(bodyStr),
|
|
46
|
+
...headers
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const req = https.request(opts, (res) => {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
res.on('data', (chunk) => { chunks.push(chunk); });
|
|
53
|
+
res.on('end', () => {
|
|
54
|
+
const rawBody = Buffer.concat(chunks).toString('utf8');
|
|
55
|
+
let bodyParsed = {};
|
|
56
|
+
try {
|
|
57
|
+
bodyParsed = rawBody ? JSON.parse(rawBody) : {};
|
|
58
|
+
} catch {
|
|
59
|
+
bodyParsed = {};
|
|
60
|
+
}
|
|
61
|
+
resolve({
|
|
62
|
+
statusCode: res.statusCode,
|
|
63
|
+
statusMessage: res.statusMessage,
|
|
64
|
+
headers: res.headers,
|
|
65
|
+
body: bodyParsed,
|
|
66
|
+
rawBody
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
req.on('error', reject);
|
|
71
|
+
req.setTimeout(timeoutMs, () => {
|
|
72
|
+
req.destroy();
|
|
73
|
+
const e = new Error('Request timeout');
|
|
74
|
+
e.code = 'ETIMEDOUT';
|
|
75
|
+
reject(e);
|
|
76
|
+
});
|
|
77
|
+
req.write(bodyStr);
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Exibe erro estruturado + instruções de suporte + bundle (pronto para colar).
|
|
84
|
+
* Usa ui (sem gray/dim) para legibilidade em Windows e copy/paste.
|
|
85
|
+
* @param {Function} getT
|
|
86
|
+
* @param {string} appUrl - APP_CONFIG.appUrl
|
|
87
|
+
* @param {object} opts
|
|
88
|
+
*/
|
|
89
|
+
function printLicenseValidationError(getT, appUrl, opts) {
|
|
90
|
+
const {
|
|
91
|
+
correlationId,
|
|
92
|
+
kind,
|
|
93
|
+
httpStatus,
|
|
94
|
+
httpStatusText,
|
|
95
|
+
endpoint,
|
|
96
|
+
fullUrl,
|
|
97
|
+
timeoutMs,
|
|
98
|
+
licenseMasked,
|
|
99
|
+
version,
|
|
100
|
+
responseHeaders,
|
|
101
|
+
responseBodyTruncated,
|
|
102
|
+
err
|
|
103
|
+
} = opts;
|
|
104
|
+
|
|
105
|
+
const bundleParams = {
|
|
106
|
+
correlationId,
|
|
107
|
+
product: 'smoonb CLI',
|
|
108
|
+
version,
|
|
109
|
+
timestamp: new Date().toISOString(),
|
|
110
|
+
os: process.platform,
|
|
111
|
+
arch: process.arch,
|
|
112
|
+
nodeVersion: process.version,
|
|
113
|
+
command: `smoonb ${opts.command || 'backup'}`,
|
|
114
|
+
licenseMasked: licenseMasked || '***',
|
|
115
|
+
apiBaseUrl: fullUrl ? fullUrl.replace(/\/[^/]*$/, '') : APP_CONFIG.apiBaseUrl,
|
|
116
|
+
endpoint: endpoint || ENDPOINT_PATH,
|
|
117
|
+
timeoutMs,
|
|
118
|
+
telemetryEnabled: (process.env.SMOONB_TELEMETRY_ENABLED !== 'false').toString(),
|
|
119
|
+
installationIdMasked: opts.installationIdMasked
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (httpStatus != null) {
|
|
123
|
+
bundleParams.httpStatus = httpStatus;
|
|
124
|
+
bundleParams.httpStatusText = httpStatusText || '';
|
|
125
|
+
bundleParams.responseHeaders = responseHeaders;
|
|
126
|
+
bundleParams.responseBodyRaw = responseBodyTruncated || undefined;
|
|
127
|
+
}
|
|
128
|
+
if (err) {
|
|
129
|
+
bundleParams.errorName = err.name;
|
|
130
|
+
bundleParams.errorMessage = (err.message || '').replace(/\r?\n/g, ' ');
|
|
131
|
+
bundleParams.errorCode = err.code;
|
|
132
|
+
bundleParams.errno = err.errno;
|
|
133
|
+
if (process.env.SMOONB_DEBUG === 'true') {
|
|
134
|
+
bundleParams.stack = err.stack;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const bundleText = buildSupportBundle(bundleParams);
|
|
139
|
+
const typeKey = kind === 'http' ? 'license.error.typeHttp'
|
|
140
|
+
: kind === 'tls' ? 'license.error.typeTls'
|
|
141
|
+
: kind === 'parse' ? 'license.error.typeParse'
|
|
142
|
+
: 'license.error.typeNetwork';
|
|
143
|
+
|
|
144
|
+
ui.errorBold(`\n❌ ${getT('license.error.title')}\n`);
|
|
145
|
+
ui.info(` ${getT('license.error.whatHappened')}`);
|
|
146
|
+
ui.info(` ${getT(typeKey)}`);
|
|
147
|
+
if (kind === 'network' || kind === 'tls') {
|
|
148
|
+
ui.hint(` ${getT('license.error.testConnectivity', { url: APP_CONFIG.healthCheckUrl || appUrl + '/api/health' })}`);
|
|
149
|
+
}
|
|
150
|
+
if (httpStatus != null) {
|
|
151
|
+
ui.info(` ${getT('license.error.status', { status: `${httpStatus} ${(httpStatusText || '').trim()}` })}`);
|
|
152
|
+
}
|
|
153
|
+
if (endpoint || fullUrl) {
|
|
154
|
+
ui.info(` ${getT('license.error.endpoint', { endpoint: fullUrl || endpoint })}`);
|
|
155
|
+
}
|
|
156
|
+
ui.info(` ${getT('license.error.correlationId', { id: correlationId })}\n`);
|
|
157
|
+
|
|
158
|
+
ui.link(` ${getT('license.error.howToHelp')}`);
|
|
159
|
+
ui.info(` ${getT('license.error.supportStep1', { url: appUrl })}`);
|
|
160
|
+
ui.info(` ${getT('license.error.supportStep2')}`);
|
|
161
|
+
ui.info(` ${getT('license.error.supportStep3')}`);
|
|
162
|
+
ui.info(` ${getT('license.error.supportStep4')}`);
|
|
163
|
+
ui.info(` ${getT('license.error.supportStep5')}\n`);
|
|
164
|
+
|
|
165
|
+
ui.block(formatBundleForTerminal(bundleText));
|
|
166
|
+
ui.info('');
|
|
167
|
+
ui.link(` ${getT('license.error.visit', { url: appUrl })}\n`);
|
|
168
|
+
ui.hint(` ${getT('license.error.subjectSuggestion', { id: correlationId })}`);
|
|
169
|
+
ui.hint(` ${getT('license.error.messageSuggestion')}\n`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Exibe falha quando servidor retorna allow === false (reason literal + CTA + bundle).
|
|
174
|
+
* Se reason/reasonCode === LICENSE_ALREADY_BOUND, exibe mensagem específica de revogar e gerar nova.
|
|
175
|
+
*/
|
|
176
|
+
function printLicenseDeniedByServer(getT, appUrl, opts) {
|
|
177
|
+
const {
|
|
178
|
+
correlationId,
|
|
179
|
+
correlationIdEchoed,
|
|
180
|
+
reason,
|
|
181
|
+
reasonCode,
|
|
182
|
+
fullUrl,
|
|
183
|
+
rawBody,
|
|
184
|
+
resBody,
|
|
185
|
+
licenseMasked,
|
|
186
|
+
version,
|
|
187
|
+
command,
|
|
188
|
+
installationIdMasked
|
|
189
|
+
} = opts;
|
|
190
|
+
const displayId = correlationIdEchoed || correlationId;
|
|
191
|
+
const responseBodyRaw = rawBody ? truncateBody(rawBody, BODY_TRUNCATE_BYTES_BUNDLE) : '';
|
|
192
|
+
const responseParsed = sanitizeResponseParsedForBundle(resBody);
|
|
193
|
+
const isAlreadyBound = (reasonCode && String(reasonCode).toUpperCase() === 'LICENSE_ALREADY_BOUND') ||
|
|
194
|
+
(reason && String(reason).toUpperCase() === 'LICENSE_ALREADY_BOUND');
|
|
195
|
+
|
|
196
|
+
ui.errorBold(`\n❌ ${getT('license.denied.title')}\n`);
|
|
197
|
+
if (isAlreadyBound) {
|
|
198
|
+
ui.info(` ${getT('license.denied.alreadyBound')}`);
|
|
199
|
+
ui.info(` ${getT('license.denied.alreadyBoundAction', { url: appUrl })}`);
|
|
200
|
+
} else {
|
|
201
|
+
ui.info(` ${getT('license.denied.reason', { reason: reason || 'No reason provided' })}`);
|
|
202
|
+
}
|
|
203
|
+
ui.info(` ${getT('license.denied.correlationId', { id: displayId })}\n`);
|
|
204
|
+
ui.link(` ${getT('license.error.howToHelp')}`);
|
|
205
|
+
ui.info(` ${getT('license.error.supportStep1', { url: appUrl })}`);
|
|
206
|
+
ui.info(` ${getT('license.error.supportStep2')}`);
|
|
207
|
+
ui.info(` ${getT('license.error.supportStep3')}`);
|
|
208
|
+
ui.info(` ${getT('license.error.supportStep4')}`);
|
|
209
|
+
ui.info(` ${getT('license.error.supportStep5')}\n`);
|
|
210
|
+
|
|
211
|
+
const bundleParams = {
|
|
212
|
+
correlationId: displayId,
|
|
213
|
+
product: 'smoonb CLI',
|
|
214
|
+
version,
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
os: process.platform,
|
|
217
|
+
arch: process.arch,
|
|
218
|
+
nodeVersion: process.version,
|
|
219
|
+
command: `smoonb ${command || 'backup'}`,
|
|
220
|
+
licenseMasked: licenseMasked || '***',
|
|
221
|
+
apiBaseUrl: fullUrl ? fullUrl.replace(/\/[^/]*$/, '') : APP_CONFIG.apiBaseUrl,
|
|
222
|
+
endpoint: ENDPOINT_PATH,
|
|
223
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
224
|
+
telemetryEnabled: (process.env.SMOONB_TELEMETRY_ENABLED !== 'false').toString(),
|
|
225
|
+
httpStatus: 200,
|
|
226
|
+
httpStatusText: 'OK',
|
|
227
|
+
responseBodyRaw: responseBodyRaw || undefined,
|
|
228
|
+
responseParsed: responseParsed !== '(none)' ? responseParsed : undefined,
|
|
229
|
+
correlationIdEchoed: correlationIdEchoed && correlationIdEchoed !== correlationId ? correlationIdEchoed : undefined,
|
|
230
|
+
installationIdMasked: installationIdMasked || undefined,
|
|
231
|
+
reason: reason != null ? String(reason) : (reasonCode != null ? String(reasonCode) : undefined)
|
|
232
|
+
};
|
|
233
|
+
const bundleText = buildSupportBundle(bundleParams);
|
|
234
|
+
ui.block(formatBundleForTerminal(bundleText));
|
|
235
|
+
ui.info('');
|
|
236
|
+
ui.link(` ${getT('license.error.visit', { url: appUrl })}\n`);
|
|
237
|
+
ui.hint(` ${getT('license.error.subjectSuggestion', { id: displayId })}`);
|
|
238
|
+
ui.hint(` ${getT('license.error.messageSuggestion')}\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Step 00: Validação de licença via backend.
|
|
243
|
+
* Roda antes de todos os outros steps.
|
|
244
|
+
* Resolve SMOONB_LICENSE_KEY: env > .env.local > prompt. Persiste se prompt.
|
|
245
|
+
* Sem cache local; falha de rede/servidor = abortar.
|
|
246
|
+
* @param {{ envPath: string, command: string }} options
|
|
247
|
+
* @returns {Promise<{ license: object, smoonbToken: string }>}
|
|
248
|
+
*/
|
|
249
|
+
module.exports = async (options) => {
|
|
250
|
+
const getT = global.smoonbI18n?.t || t;
|
|
251
|
+
const envPath = options.envPath || path.join(process.cwd(), '.env.local');
|
|
252
|
+
const command = options.command || 'backup';
|
|
253
|
+
const appUrl = APP_CONFIG.appUrl;
|
|
254
|
+
const fullUrl = `${APP_CONFIG.apiBaseUrl}${ENDPOINT_PATH}`;
|
|
255
|
+
|
|
256
|
+
let licenseKey = (process.env[LICENSE_KEY_ENV] || '').toString().trim();
|
|
257
|
+
if (!licenseKey) {
|
|
258
|
+
const currentEnv = await readEnvFile(envPath);
|
|
259
|
+
licenseKey = (currentEnv[LICENSE_KEY_ENV] || '').toString().trim();
|
|
260
|
+
}
|
|
261
|
+
if (!licenseKey) {
|
|
262
|
+
const { licenseKey: prompted } = await inquirer.prompt([{
|
|
263
|
+
type: 'input',
|
|
264
|
+
name: 'licenseKey',
|
|
265
|
+
message: getT('license.promptPaste'),
|
|
266
|
+
prefix: ''
|
|
267
|
+
}]);
|
|
268
|
+
licenseKey = (prompted || '').toString().trim();
|
|
269
|
+
if (!licenseKey) {
|
|
270
|
+
ui.error(`❌ ${getT('license.required')}`);
|
|
271
|
+
ui.link(` ${appUrl}`);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
// Keep interactive credentials in memory; preserve the project's environment file.
|
|
275
|
+
process.env[LICENSE_KEY_ENV] = licenseKey;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let cliVersion = '0.0.0';
|
|
279
|
+
try {
|
|
280
|
+
cliVersion = require('../../../../package.json').version;
|
|
281
|
+
} catch {
|
|
282
|
+
// ignore
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let installationId;
|
|
286
|
+
try {
|
|
287
|
+
installationId = await getOrCreateInstallationId();
|
|
288
|
+
} catch (err) {
|
|
289
|
+
ui.error(`❌ ${getT('license.error.title')}`);
|
|
290
|
+
ui.info(` ${getT('license.error.whatHappened')}`);
|
|
291
|
+
ui.info(` Could not create or read installation ID: ${(err && err.message) || err}`);
|
|
292
|
+
ui.link(` ${getT('license.error.visit', { url: appUrl })}`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const correlationId = createCorrelationId();
|
|
297
|
+
const body = {
|
|
298
|
+
licenseKey,
|
|
299
|
+
installationId,
|
|
300
|
+
correlationId,
|
|
301
|
+
cliVersion,
|
|
302
|
+
command,
|
|
303
|
+
platform: process.platform,
|
|
304
|
+
arch: process.arch,
|
|
305
|
+
nodeVersion: process.version
|
|
306
|
+
};
|
|
307
|
+
const requestHeaders = {
|
|
308
|
+
'User-Agent': `smoonb-cli/${cliVersion}`,
|
|
309
|
+
'x-correlation-id': correlationId
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let response;
|
|
313
|
+
try {
|
|
314
|
+
response = await httpsPost(
|
|
315
|
+
fullUrl,
|
|
316
|
+
body,
|
|
317
|
+
requestHeaders,
|
|
318
|
+
REQUEST_TIMEOUT_MS
|
|
319
|
+
);
|
|
320
|
+
} catch (err) {
|
|
321
|
+
const classification = classifyRequestError(err);
|
|
322
|
+
if (classification.hintUser) {
|
|
323
|
+
ui.warn(` ${classification.hintUser}`);
|
|
324
|
+
}
|
|
325
|
+
printLicenseValidationError(getT, appUrl, {
|
|
326
|
+
correlationId,
|
|
327
|
+
kind: classification.kind,
|
|
328
|
+
endpoint: ENDPOINT_PATH,
|
|
329
|
+
fullUrl,
|
|
330
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
331
|
+
licenseMasked: maskSecret(licenseKey),
|
|
332
|
+
version: cliVersion,
|
|
333
|
+
command,
|
|
334
|
+
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined,
|
|
335
|
+
err
|
|
336
|
+
});
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { statusCode, statusMessage, headers, body: resBody, rawBody } = response;
|
|
341
|
+
|
|
342
|
+
// Resposta 200 mas corpo inesperado (ex.: HTML em vez de JSON)
|
|
343
|
+
const looksLikeJson = rawBody && rawBody.trim().startsWith('{');
|
|
344
|
+
if (statusCode === 200 && !looksLikeJson && rawBody) {
|
|
345
|
+
const displayCorrelationId = resBody.correlationId || correlationId;
|
|
346
|
+
const responseHeadersStr = safeStringifyHeaders(headers);
|
|
347
|
+
const responseBodyTruncated = truncateBody(rawBody);
|
|
348
|
+
printLicenseValidationError(getT, appUrl, {
|
|
349
|
+
correlationId: displayCorrelationId,
|
|
350
|
+
kind: 'parse',
|
|
351
|
+
endpoint: ENDPOINT_PATH,
|
|
352
|
+
fullUrl,
|
|
353
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
354
|
+
licenseMasked: maskSecret(licenseKey),
|
|
355
|
+
version: cliVersion,
|
|
356
|
+
command,
|
|
357
|
+
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined,
|
|
358
|
+
responseHeaders: responseHeadersStr,
|
|
359
|
+
responseBodyTruncated
|
|
360
|
+
});
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (statusCode !== 200) {
|
|
365
|
+
const displayCorrelationId = resBody.correlationId || correlationId;
|
|
366
|
+
const responseHeadersStr = safeStringifyHeaders(headers);
|
|
367
|
+
const responseBodyRaw = rawBody ? truncateBody(rawBody, BODY_TRUNCATE_BYTES_BUNDLE) : '';
|
|
368
|
+
printLicenseValidationError(getT, appUrl, {
|
|
369
|
+
correlationId: displayCorrelationId,
|
|
370
|
+
kind: 'http',
|
|
371
|
+
httpStatus: statusCode,
|
|
372
|
+
httpStatusText: statusMessage,
|
|
373
|
+
endpoint: ENDPOINT_PATH,
|
|
374
|
+
fullUrl,
|
|
375
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
376
|
+
licenseMasked: maskSecret(licenseKey),
|
|
377
|
+
version: cliVersion,
|
|
378
|
+
command,
|
|
379
|
+
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined,
|
|
380
|
+
responseHeaders: responseHeadersStr,
|
|
381
|
+
responseBodyTruncated: responseBodyRaw
|
|
382
|
+
});
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// allow como boolean robusto: true ou "true" -> true; resto -> false
|
|
387
|
+
const allowRaw = resBody.allow;
|
|
388
|
+
const allow = allowRaw === true || (typeof allowRaw === 'string' && allowRaw.toLowerCase() === 'true');
|
|
389
|
+
|
|
390
|
+
if (allow === true) {
|
|
391
|
+
// Nunca negar quando allow === true
|
|
392
|
+
const status = resBody.status || '';
|
|
393
|
+
if (status === 'trial') {
|
|
394
|
+
ui.warn(` ⚠️ ${getT('license.trialNotice')}`);
|
|
395
|
+
}
|
|
396
|
+
const license = {
|
|
397
|
+
status: resBody.status,
|
|
398
|
+
plan: resBody.plan,
|
|
399
|
+
expiresAt: resBody.expiresAt,
|
|
400
|
+
accountId: resBody.accountId,
|
|
401
|
+
workspaceId: resBody.workspaceId
|
|
402
|
+
};
|
|
403
|
+
const smoonbToken = resBody.token || '';
|
|
404
|
+
return { license, smoonbToken };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// allow === false: exibir reason do servidor literalmente + CTA + bundle
|
|
408
|
+
const serverReason = resBody.reason != null ? String(resBody.reason) : 'No reason provided';
|
|
409
|
+
const serverReasonCode = resBody.reasonCode != null ? String(resBody.reasonCode) : null;
|
|
410
|
+
const correlationIdEchoed = resBody.correlationId || null;
|
|
411
|
+
printLicenseDeniedByServer(getT, appUrl, {
|
|
412
|
+
correlationId,
|
|
413
|
+
correlationIdEchoed,
|
|
414
|
+
reason: serverReason,
|
|
415
|
+
reasonCode: serverReasonCode,
|
|
416
|
+
fullUrl,
|
|
417
|
+
rawBody,
|
|
418
|
+
resBody,
|
|
419
|
+
licenseMasked: maskSecret(licenseKey),
|
|
420
|
+
version: cliVersion,
|
|
421
|
+
command,
|
|
422
|
+
installationIdMasked: installationId ? maskInstallationId(installationId) : undefined
|
|
423
|
+
});
|
|
424
|
+
process.exit(1);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
module.exports.maskLicense = maskLicense;
|