smoonb 0.0.100 → 0.0.101

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.

Potentially problematic release.


This version of smoonb might be problematic. Click here for more details.

package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "0.0.100",
3
+ "version": "0.0.101",
4
4
  "description": "Complete Supabase backup and migration tool - EXPERIMENTAL VERSION - USE AT YOUR OWN RISK",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -10,9 +10,11 @@ const {
10
10
  maskSecret,
11
11
  safeStringifyHeaders,
12
12
  truncateBody,
13
+ sanitizeResponseParsedForBundle,
13
14
  classifyRequestError,
14
15
  buildSupportBundle,
15
- formatBundleForTerminal
16
+ formatBundleForTerminal,
17
+ BODY_TRUNCATE_BYTES_BUNDLE
16
18
  } = require('../../../utils/supportDiagnostics');
17
19
 
18
20
  const LICENSE_KEY_ENV = 'SMOONB_LICENSE_KEY';
@@ -119,7 +121,7 @@ function printLicenseValidationError(getT, appUrl, opts) {
119
121
  bundleParams.httpStatus = httpStatus;
120
122
  bundleParams.httpStatusText = httpStatusText || '';
121
123
  bundleParams.responseHeaders = responseHeaders;
122
- bundleParams.responseBodyTruncated = responseBodyTruncated;
124
+ bundleParams.responseBodyRaw = responseBodyTruncated || undefined;
123
125
  }
124
126
  if (err) {
125
127
  bundleParams.errorName = err.name;
@@ -165,6 +167,63 @@ function printLicenseValidationError(getT, appUrl, opts) {
165
167
  ui.hint(` ${getT('license.error.messageSuggestion')}\n`);
166
168
  }
167
169
 
170
+ /**
171
+ * Exibe falha quando servidor retorna allow === false (reason literal + CTA + bundle).
172
+ */
173
+ function printLicenseDeniedByServer(getT, appUrl, opts) {
174
+ const {
175
+ correlationId,
176
+ correlationIdEchoed,
177
+ reason,
178
+ fullUrl,
179
+ rawBody,
180
+ resBody,
181
+ licenseMasked,
182
+ version,
183
+ command
184
+ } = opts;
185
+ const displayId = correlationIdEchoed || correlationId;
186
+ const responseBodyRaw = rawBody ? truncateBody(rawBody, BODY_TRUNCATE_BYTES_BUNDLE) : '';
187
+ const responseParsed = sanitizeResponseParsedForBundle(resBody);
188
+
189
+ ui.errorBold(`\n❌ ${getT('license.denied.title')}\n`);
190
+ ui.info(` ${getT('license.denied.reason', { reason: reason || 'No reason provided' })}`);
191
+ ui.info(` ${getT('license.denied.correlationId', { id: displayId })}\n`);
192
+ ui.link(` ${getT('license.error.howToHelp')}`);
193
+ ui.info(` ${getT('license.error.supportStep1', { url: appUrl })}`);
194
+ ui.info(` ${getT('license.error.supportStep2')}`);
195
+ ui.info(` ${getT('license.error.supportStep3')}`);
196
+ ui.info(` ${getT('license.error.supportStep4')}`);
197
+ ui.info(` ${getT('license.error.supportStep5')}\n`);
198
+
199
+ const bundleParams = {
200
+ correlationId: displayId,
201
+ product: 'smoonb CLI',
202
+ version,
203
+ timestamp: new Date().toISOString(),
204
+ os: process.platform,
205
+ arch: process.arch,
206
+ nodeVersion: process.version,
207
+ command: `smoonb ${command || 'backup'}`,
208
+ licenseMasked: licenseMasked || '***',
209
+ apiBaseUrl: fullUrl ? fullUrl.replace(/\/[^/]*$/, '') : APP_CONFIG.apiBaseUrl,
210
+ endpoint: ENDPOINT_PATH,
211
+ timeoutMs: REQUEST_TIMEOUT_MS,
212
+ telemetryEnabled: (process.env.SMOONB_TELEMETRY_ENABLED !== 'false').toString(),
213
+ httpStatus: 200,
214
+ httpStatusText: 'OK',
215
+ responseBodyRaw: responseBodyRaw || undefined,
216
+ responseParsed: responseParsed !== '(none)' ? responseParsed : undefined,
217
+ correlationIdEchoed: correlationIdEchoed && correlationIdEchoed !== correlationId ? correlationIdEchoed : undefined
218
+ };
219
+ const bundleText = buildSupportBundle(bundleParams);
220
+ ui.block(formatBundleForTerminal(bundleText));
221
+ ui.info('');
222
+ ui.link(` ${getT('license.error.visit', { url: appUrl })}\n`);
223
+ ui.hint(` ${getT('license.error.subjectSuggestion', { id: displayId })}`);
224
+ ui.hint(` ${getT('license.error.messageSuggestion')}\n`);
225
+ }
226
+
168
227
  /**
169
228
  * Step 00: Validação de licença via backend.
170
229
  * Roda antes de todos os outros steps.
@@ -276,7 +335,7 @@ module.exports = async (options) => {
276
335
  if (statusCode !== 200) {
277
336
  const displayCorrelationId = resBody.correlationId || correlationId;
278
337
  const responseHeadersStr = safeStringifyHeaders(headers);
279
- const responseBodyTruncated = rawBody ? truncateBody(rawBody) : '';
338
+ const responseBodyRaw = rawBody ? truncateBody(rawBody, BODY_TRUNCATE_BYTES_BUNDLE) : '';
280
339
  printLicenseValidationError(getT, appUrl, {
281
340
  correlationId: displayCorrelationId,
282
341
  kind: 'http',
@@ -289,35 +348,47 @@ module.exports = async (options) => {
289
348
  version: cliVersion,
290
349
  command,
291
350
  responseHeaders: responseHeadersStr,
292
- responseBodyTruncated
351
+ responseBodyTruncated: responseBodyRaw
293
352
  });
294
353
  process.exit(1);
295
354
  }
296
355
 
297
- const valid = resBody.valid === true;
298
- const status = resBody.status || '';
356
+ // allow como boolean robusto: true ou "true" -> true; resto -> false
357
+ const allowRaw = resBody.allow;
358
+ const allow = allowRaw === true || (typeof allowRaw === 'string' && allowRaw.toLowerCase() === 'true');
299
359
 
300
- if (!valid || ['expired', 'canceled'].includes(status)) {
301
- ui.error(`❌ ${getT('license.invalidOrInactive')}`);
302
- ui.info(` ${getT('license.openDesktopApp')}`);
303
- ui.link(` ${appUrl}`);
304
- process.exit(1);
305
- }
306
-
307
- if (status === 'trial') {
308
- ui.warn(` ⚠️ ${getT('license.trialNotice')}`);
360
+ if (allow === true) {
361
+ // Nunca negar quando allow === true
362
+ const status = resBody.status || '';
363
+ if (status === 'trial') {
364
+ ui.warn(` ⚠️ ${getT('license.trialNotice')}`);
365
+ }
366
+ const license = {
367
+ status: resBody.status,
368
+ plan: resBody.plan,
369
+ expiresAt: resBody.expiresAt,
370
+ accountId: resBody.accountId,
371
+ workspaceId: resBody.workspaceId
372
+ };
373
+ const smoonbToken = resBody.token || '';
374
+ return { license, smoonbToken };
309
375
  }
310
376
 
311
- const license = {
312
- status: resBody.status,
313
- plan: resBody.plan,
314
- expiresAt: resBody.expiresAt,
315
- accountId: resBody.accountId,
316
- workspaceId: resBody.workspaceId
317
- };
318
- const smoonbToken = resBody.token || '';
319
-
320
- return { license, smoonbToken };
377
+ // allow === false: exibir reason do servidor literalmente + CTA + bundle
378
+ const serverReason = resBody.reason != null ? String(resBody.reason) : 'No reason provided';
379
+ const correlationIdEchoed = resBody.correlationId || null;
380
+ printLicenseDeniedByServer(getT, appUrl, {
381
+ correlationId,
382
+ correlationIdEchoed,
383
+ reason: serverReason,
384
+ fullUrl,
385
+ rawBody,
386
+ resBody,
387
+ licenseMasked: maskSecret(licenseKey),
388
+ version: cliVersion,
389
+ command
390
+ });
391
+ process.exit(1);
321
392
  };
322
393
 
323
394
  module.exports.maskLicense = maskLicense;
@@ -273,6 +273,9 @@
273
273
  "license.checkConnection": "Check your connection and try again.",
274
274
  "license.invalidOrInactive": "Your license is invalid or your subscription is not active.",
275
275
  "license.openDesktopApp": "Open the desktop app, check your subscription and generate a new license.",
276
+ "license.denied.title": "License validation denied by server.",
277
+ "license.denied.reason": "Reason: {reason}",
278
+ "license.denied.correlationId": "Correlation ID: {id}",
276
279
  "license.trialNotice": "You are using a trial license.",
277
280
  "license.error.title": "License validation failed.",
278
281
  "license.error.whatHappened": "What happened:",
@@ -273,6 +273,9 @@
273
273
  "license.checkConnection": "Verifique sua conexão e tente novamente.",
274
274
  "license.invalidOrInactive": "Sua licença é inválida ou sua assinatura não está ativa.",
275
275
  "license.openDesktopApp": "Abra o app desktop, verifique sua assinatura e gere uma nova licença.",
276
+ "license.denied.title": "Validação de licença negada pelo servidor.",
277
+ "license.denied.reason": "Motivo: {reason}",
278
+ "license.denied.correlationId": "Correlation ID: {id}",
276
279
  "license.trialNotice": "Você está usando uma licença trial.",
277
280
  "license.error.title": "Falha na validação da licença.",
278
281
  "license.error.whatHappened": "O que aconteceu:",
@@ -6,6 +6,7 @@ const crypto = require('crypto');
6
6
 
7
7
  const ALLOWLIST_HEADERS = ['content-type', 'server', 'cf-ray', 'x-request-id'];
8
8
  const BODY_TRUNCATE_BYTES = 2048;
9
+ const BODY_TRUNCATE_BYTES_BUNDLE = 4096;
9
10
 
10
11
  function createCorrelationId() {
11
12
  if (crypto.randomUUID) {
@@ -65,6 +66,30 @@ function truncateBody(input, maxBytes = BODY_TRUNCATE_BYTES) {
65
66
  return buf.slice(0, maxBytes).toString('utf8') + '\n...[truncated]';
66
67
  }
67
68
 
69
+ /** Campos permitidos no ResponseParsed do bundle (sem tokens/sensíveis). */
70
+ const RESPONSE_PARSED_KEYS = ['allow', 'reason', 'periodEnd', 'trialEndsAt', 'remainingThisMonth', 'correlationId'];
71
+
72
+ /**
73
+ * Extrai objeto sanitizado da resposta para o bundle (allow, reason, periodEnd, trialEndsAt, remainingThisMonth, correlationId).
74
+ * @param {object} parsed - resBody
75
+ * @returns {string} JSON string ou "(none)"
76
+ */
77
+ function sanitizeResponseParsedForBundle(parsed) {
78
+ if (!parsed || typeof parsed !== 'object') return '(none)';
79
+ const out = {};
80
+ for (const key of RESPONSE_PARSED_KEYS) {
81
+ if (Object.prototype.hasOwnProperty.call(parsed, key)) {
82
+ out[key] = parsed[key];
83
+ }
84
+ }
85
+ if (Object.keys(out).length === 0) return '(none)';
86
+ try {
87
+ return JSON.stringify(out);
88
+ } catch {
89
+ return '(none)';
90
+ }
91
+ }
92
+
68
93
  /**
69
94
  * Classifica erro de request (rede/TLS/timeout).
70
95
  * @param {Error} err
@@ -169,14 +194,22 @@ function buildSupportBundle(params) {
169
194
  ];
170
195
 
171
196
  if (p.httpStatus != null) {
172
- lines.push(`HTTP: ${p.httpStatus} ${p.httpStatusText || ''}`.trim());
197
+ lines.push(`HttpStatus: ${p.httpStatus} ${(p.httpStatusText || '').trim()}`);
198
+ }
199
+ if (p.correlationIdEchoed != null && p.correlationIdEchoed !== p.correlationId) {
200
+ lines.push(`CorrelationIdEchoed: ${p.correlationIdEchoed}`);
173
201
  }
174
202
  if (p.responseHeaders) {
175
203
  lines.push('ResponseHeaders:', p.responseHeaders);
176
204
  }
177
- if (p.responseBodyTruncated) {
205
+ if (p.responseBodyRaw != null) {
206
+ lines.push('ResponseBodyRaw:', p.responseBodyRaw);
207
+ } else if (p.responseBodyTruncated) {
178
208
  lines.push('ResponseBody(Truncated):', p.responseBodyTruncated);
179
209
  }
210
+ if (p.responseParsed != null) {
211
+ lines.push('ResponseParsed:', p.responseParsed);
212
+ }
180
213
  if (p.errorName || p.errorMessage) {
181
214
  lines.push(`Error: name: ${p.errorName || ''} message: ${p.errorMessage || ''}`);
182
215
  }
@@ -209,8 +242,10 @@ module.exports = {
209
242
  maskSecret,
210
243
  safeStringifyHeaders,
211
244
  truncateBody,
245
+ sanitizeResponseParsedForBundle,
212
246
  classifyRequestError,
213
247
  buildSupportBundle,
214
248
  formatBundleForTerminal,
215
- BODY_TRUNCATE_BYTES
249
+ BODY_TRUNCATE_BYTES,
250
+ BODY_TRUNCATE_BYTES_BUNDLE
216
251
  };