smoonb 1.0.8 → 1.0.10

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 CHANGED
@@ -4,6 +4,37 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.0.10] - 2026-10-10
8
+
9
+ ### Fixed
10
+
11
+ - When a Realtime PATCH succeeds but capacity limits remain different, show the backup and destination values and offer an explicit choice to retain the destination limits. Record accepted differences instead of failing with field names only or claiming an identical restore.
12
+ - Retry readback without repeating PATCH, then re-read all selected Realtime settings after consent. Security switches, connection pools, invalid values, API failures and concurrent configuration changes remain failures.
13
+ - Record refused quota differences in the failure report; allow a retry selecting only Realtime without repeating Database, Auth or Storage. Non-interactive runs never silently accept differences.
14
+ - Preserve SMTP verification limits in combined reports and show completion with notes when any selected component has accepted differences or unverified secrets.
15
+ - Replace obsolete manual-configuration messages in the English and Portuguese Auth/Realtime summaries.
16
+
17
+ ### Tests
18
+
19
+ - Cover legacy and API snapshots, delayed readback, consent and rejection, non-interactive execution, strict security validation, concurrent changes, secret-safe errors, partial reports and Realtime-only retries.
20
+
21
+ ## [1.0.9] - 2026-10-10
22
+
23
+ ### Fixed
24
+
25
+ - Treat SMTP passwords as write-only credentials. Never replay the protected SMTP value returned by the Management API from a legacy backup as the provider password.
26
+ - Ask for the real SMTP password with hidden input before restoring any selected components. Fail before remote mutations if it is missing. Keep the entered password in memory only.
27
+ - Verify readable Auth settings independently from password submission. Continue after the API accepts the supplied password, but report `applied_with_unverified_secrets` and require an authentication email test instead of claiming password or email-delivery verification.
28
+ - Omit the protected SMTP value from new Auth snapshots and explicitly record that the provider password is required during restore. Existing backups remain usable.
29
+
30
+ ### Recovery from 1.0.8
31
+
32
+ - If restore stopped at `Configuration verification failed: smtp_pass`, inspect its `report.json`. When Database and Edge Functions are already verified, run restore with those two components disabled and select Auth, Storage, Database Settings and Realtime as needed. Supply the real provider SMTP password when prompted; no new backup or destination project is required for this specific failure.
33
+
34
+ ### Tests
35
+
36
+ - Cover legacy/new snapshots, hidden input, missing-password preflight, protected-value rejection, HTTP failures, readable-setting mismatches, partial verification reporting and continuation without reloading the database.
37
+
7
38
  ## [1.0.8] - 2026-10-10
8
39
 
9
40
  ### Fixed
package/README.md CHANGED
@@ -26,7 +26,7 @@ smoonb performs a complete backup of all components of your Supabase project:
26
26
  - ✅ **Edge Functions** (download automático do servidor)
27
27
  - ✅ **Auth Settings** (configurações de autenticação via Management API)
28
28
  - ✅ **Storage Buckets** (backup completo: metadados, configurações e todos os arquivos via Management API + Supabase Client, cria ZIP no padrão do Dashboard)
29
- - ✅ **Realtime Settings** (7 parâmetros capturados interativamente)
29
+ - ✅ **Realtime Settings** (current settings captured through the Management API)
30
30
  - ✅ **Supabase .temp** (arquivos temporários do Supabase CLI)
31
31
  - ✅ **Migrations** (todas as migrations do projeto via `supabase migration fetch`)
32
32
 
@@ -217,7 +217,7 @@ npx smoonb backup --skip-supabase-version-check
217
217
  - ⚡ Edge Functions (explanation about link reset and download)
218
218
  - 📦 Storage (explanation about full backup: file download + ZIP in Dashboard format)
219
219
  - 🔐 Auth Settings (explanation about configurations)
220
- - 🔄 Realtime Settings (explanation about interactive capture of 7 parameters)
220
+ - 🔄 Realtime Settings (current configuration captured through the API)
221
221
  - 🗑️ Cleanup options (functions, .temp, migrations after backup)
222
222
  8. **Configuration summary** - Shows everything that will be done
223
223
  9. **Final confirmation** - Confirms before starting
@@ -226,7 +226,7 @@ npx smoonb backup --skip-supabase-version-check
226
226
  - 📊 2/10 - Separate Database SQL (schema, data, roles)
227
227
  - 🔧 3/10 - Database Extensions and Settings Backup
228
228
  - 🔐 4/10 - Auth Settings Backup (if selected)
229
- - 🔄 5/10 - Realtime Settings Backup (if selected) - 7 parameters captured interactively
229
+ - 🔄 5/10 - Realtime Settings Backup (if selected) - fresh API snapshot
230
230
  - 📦 6/10 - Storage Backup (if selected) - Full file download + ZIP in Dashboard format
231
231
  - 👥 7/10 - Custom Roles Backup
232
232
  - ⚡ 8/10 - Edge Functions Backup (if selected)
@@ -313,7 +313,15 @@ Database loading uses one transaction, stops on errors, suspends triggers only o
313
313
 
314
314
  Optional URL remapping changes only the project's Supabase hostname, including URLs inside JSON and text files. It does not rename files or replace isolated project IDs. Historical URLs may also change; choose preservation when needed. Signed URLs, custom domains, external integrations and application API keys need separate validation. Destination platform keys are not replaced.
315
315
 
316
- Auth and Realtime are applied through the Management API and verified by reading them back. Legacy Realtime captures may be stale: restoration reproduces the recorded values and date, not the source project's current settings. New backups capture Realtime directly through the API. Legacy function backups without the JWT flag default to requiring JWT; new backups preserve that flag.
316
+ Readable Auth and Realtime settings are applied through the Management API and verified by reading them back. The **SMTP password cannot be exported**: Supabase returns a protected value that cannot be used as the password in another project. Starting with 1.0.9, smoonb requests the real provider password with hidden input **before restoring any components**, including when using legacy backups. It stays in memory only and is sent to the destination Supabase project. The report distinguishes API password acceptance from readback verification (`applied_with_unverified_secrets`). Test an authentication email on the destination; API acceptance does not validate the password with the email provider.
317
+
318
+ If a 1.0.8 restore stopped at `Configuration verification failed: smtp_pass` and its `report.json` confirms Database and Edge Functions, reuse the same backup and project: run `npx smoonb@1.0.9 restore`, answer **no** for Database and Edge Functions, and **yes** for the pending Auth, Storage, Database Settings and Realtime components. Enter the provider SMTP password when prompted. This specific failure does not require another backup or a database reload.
319
+
320
+ Legacy Realtime captures may be stale: restoration reproduces the recorded values and date, not the source project's current settings. New backups capture Realtime directly through the API. Legacy function backups without the JWT flag default to requiring JWT; new backups preserve that flag.
321
+
322
+ Since 1.0.10, if Supabase does not apply numeric Realtime capacity limits, smoonb shows **backup value vs current destination value** and asks whether to retain the destination values. The default is **no**. After acceptance, it re-reads the configuration and records `completed_with_accepted_differences` with exact values, without claiming an identical restore. Security switches, service activation and connection pools still require a match. The tool does not change your plan or spend cap.
323
+
324
+ If an earlier restore ended only with `Configuration verification failed: max_presence_events_per_second, max_payload_size_in_kb`, consult its report and run `npx smoonb@1.0.10 restore` selecting **only Realtime Settings**. Do not repeat completed components. If you agree with the displayed limits, answer **yes** to the new prompt. Refusal leaves the step pending and records the differences; changes already applied are not undone.
317
325
 
318
326
  Reports are saved after each stage, including failures. External services cannot share the database transaction: earlier stages remain completed if a later stage fails. Do not reload the database into a populated destination. Resolve the reported stage and select only pending components, or use another empty project.
319
327
 
@@ -468,7 +476,7 @@ restore/
468
476
  #### Auth, Realtime
469
477
  - **Management API**: Uses Personal Access Token
470
478
  - **JSON Export**: Configurations exported as JSON
471
- - **Realtime Settings**: Interactive capture of 7 parameters:
479
+ - **Realtime Settings**: Fresh API capture, including:
472
480
  1. Enable Realtime service
473
481
  2. Allow public access
474
482
  3. Database connection pool size
@@ -476,25 +484,25 @@ restore/
476
484
  5. Max events per second
477
485
  6. Max presence events per second
478
486
  7. Max payload size in KB
479
- - **Manual for some**: Some settings need to be applied manually for security
487
+ - **SMTP password**: Not exportable; requested with hidden input during restore.
480
488
 
481
489
  ### Restore Strategy
482
490
 
483
491
  #### Database
484
492
  - **Format Support**:
485
- - `.backup.gz` - Automatically decompresses via Docker
493
+ - `.backup.gz` - Reads compressed content without modifying the archive
486
494
  - `.backup` - Restores directly via `psql` (Docker)
487
- - **Clean Restore**: Can overwrite existing data (with confirmation)
495
+ - **Empty Destination**: Requires an empty destination for the database; skip this component when retrying later stages that failed.
488
496
 
489
497
  #### Edge Functions
490
- - **Clean Deploy**: Cleans `supabase/functions` before deploy
491
- - **Link Reset**: Ensures correct link with target project
498
+ - **Isolated Deploy**: Uses a temporary workspace, preserving local `supabase/functions` files
499
+ - **Explicit Destination**: Passes the target project reference to the CLI
492
500
  - **Automatic Deploy**: Uses `supabase functions deploy`
493
501
 
494
502
  #### Other Components
495
503
  - **Database Settings**: Restores via SQL
496
504
  - **Storage**: Restores buckets and files from ZIP (if available) or displays information for manual configuration
497
- - **Auth/Realtime**: Displays information for manual configuration in Dashboard
505
+ - **Auth/Realtime**: Applies and checks configurations through the API; reports SMTP verification limits and any explicitly accepted Realtime capacity differences.
498
506
 
499
507
  ### Cross-Platform
500
508
 
package/README.pt-BR.md CHANGED
@@ -26,7 +26,7 @@ O smoonb faz backup completo de todos os componentes do seu projeto Supabase:
26
26
  - ✅ **Edge Functions** (download automático do servidor)
27
27
  - ✅ **Auth Settings** (configurações de autenticação via Management API)
28
28
  - ✅ **Storage Buckets** (backup completo: metadados, configurações e todos os arquivos via Management API + Supabase Client, cria ZIP no padrão do Dashboard)
29
- - ✅ **Realtime Settings** (7 parâmetros capturados interativamente)
29
+ - ✅ **Realtime Settings** (configurações atuais capturadas pela Management API)
30
30
  - ✅ **Supabase .temp** (arquivos temporários do Supabase CLI)
31
31
  - ✅ **Migrations** (todas as migrations do projeto via `supabase migration fetch`)
32
32
 
@@ -313,7 +313,15 @@ A carga do banco usa uma transação com interrupção em erro, suspende trigger
313
313
 
314
314
  O remapeamento opcional troca apenas o domínio Supabase do projeto em URLs, inclusive em JSON e arquivos de texto. Não troca IDs isolados nem nomes de arquivos. URLs históricas também podem ser afetadas; escolha preservar quando isso for necessário. URLs assinadas, domínios personalizados, integrações externas e chaves de API da aplicação precisam de validação específica. As chaves de plataforma do destino não são substituídas.
315
315
 
316
- Auth e Realtime são aplicados pela Management API e conferidos por nova leitura. A captura antiga de Realtime pode conter valores desatualizados: a restauração reproduz os valores e a data registrados, não o estado atual do projeto original. Novos backups capturam Realtime diretamente pela API. Em backups antigos sem o sinalizador de JWT de Functions, o padrão seguro é exigir JWT; os novos backups guardam esse sinalizador.
316
+ As configurações legíveis de Auth e Realtime são aplicadas pela Management API e conferidas por nova leitura. A **senha SMTP não é exportável**: o Supabase devolve um valor protegido, que não pode ser usado como senha em outro projeto. A partir da versão 1.0.9, o smoonb solicita a senha real do provedor em campo oculto **antes de restaurar os componentes**, inclusive ao usar backups antigos. Ela permanece somente na memória e é enviada ao Supabase de destino. O relatório distingue senha aceita pela API de configurações verificadas por leitura (`applied_with_unverified_secrets`). Teste o envio de e-mail de autenticação no destino; aceitar uma senha pela API não comprova sua validade junto ao provedor.
317
+
318
+ Se uma restauração 1.0.8 parou em `Configuration verification failed: smtp_pass` e o `report.json` confirmou Database e Edge Functions, use o mesmo backup e projeto: execute `npx smoonb@1.0.9 restore`, responda **não** para Database e Edge Functions e **sim** para Auth, Storage, Database Settings e Realtime que ficaram pendentes. Informe a senha SMTP do provedor quando solicitada. Não é necessário refazer o backup nem recarregar o banco nesse caso.
319
+
320
+ A captura antiga de Realtime pode conter valores desatualizados: a restauração reproduz os valores e a data registrados, não o estado atual do projeto original. Novos backups capturam Realtime diretamente pela API. Em backups antigos sem o sinalizador de JWT de Functions, o padrão seguro é exigir JWT; os novos backups guardam esse sinalizador.
321
+
322
+ Desde a versão 1.0.10, se o Supabase não aplicar limites numéricos de Realtime, o smoonb mostra **valor do backup × valor atual do destino** e pergunta se você aceita manter os valores do destino. O padrão é **não**. Ao aceitar, ele relê as configurações e registra `completed_with_accepted_differences`, com os valores exatos, sem afirmar que o destino é idêntico ao backup. Configurações de segurança, ativação do serviço e pools de conexão continuam exigindo correspondência. A ferramenta não altera seu plano nem o limite de gastos.
323
+
324
+ Se uma restauração anterior terminou apenas com `Configuration verification failed: max_presence_events_per_second, max_payload_size_in_kb`, consulte o relatório anterior e execute `npx smoonb@1.0.10 restore` selecionando **somente Realtime Settings**. Não repita componentes já concluídos. Se concordar com os limites mostrados, responda **sim** à nova pergunta. Recusar mantém a etapa pendente e registra as diferenças; alterações já aplicadas não são desfeitas.
317
325
 
318
326
  O relatório é salvo durante cada etapa, inclusive em falhas. Serviços externos não participam da transação do banco: se uma etapa posterior falhar, as anteriores permanecem registradas como concluídas. Não repita a carga do banco em um destino já preenchido. Resolva a etapa indicada e selecione apenas os componentes pendentes, ou use outro projeto vazio.
319
327
 
@@ -468,7 +476,7 @@ restore/
468
476
  #### Auth, Realtime
469
477
  - **Management API**: Usa Personal Access Token
470
478
  - **JSON Export**: Configurações exportadas como JSON
471
- - **Realtime Settings**: Captura interativa de 7 parâmetros:
479
+ - **Realtime Settings**: Captura atual pela API, incluindo:
472
480
  1. Habilitar serviço Realtime
473
481
  2. Permitir acesso público
474
482
  3. Tamanho do pool de conexões do database
@@ -476,25 +484,25 @@ restore/
476
484
  5. Máximo de eventos por segundo
477
485
  6. Máximo de eventos de presença por segundo
478
486
  7. Tamanho máximo do payload em KB
479
- - **Manual para alguns**: Alguns settings precisam ser aplicados manualmente por segurança
487
+ - **Senha SMTP**: Não exportável; solicitada em campo oculto durante a restauração.
480
488
 
481
489
  ### Estratégia de Restauração
482
490
 
483
491
  #### Database
484
492
  - **Suporte a Formatos**:
485
- - `.backup.gz` - Descompacta automaticamente via Docker
493
+ - `.backup.gz` - Lê o conteúdo compactado sem alterar o arquivo original
486
494
  - `.backup` - Restaura diretamente via `psql` (Docker)
487
- - **Restauração Limpa**: Pode sobrescrever dados existentes (com confirmação)
495
+ - **Destino Vazio**: Exige destino vazio para restaurar o banco; desmarque esse componente ao repetir etapas posteriores que falharam.
488
496
 
489
497
  #### Edge Functions
490
- - **Deploy Limpo**: Limpa `supabase/functions` antes do deploy
491
- - **Reset de Link**: Garante link correto com projeto destino
498
+ - **Deploy Isolado**: Usa uma pasta temporária, preservando os arquivos locais de `supabase/functions`
499
+ - **Destino Explícito**: Passa a referência do projeto de destino ao CLI
492
500
  - **Deploy Automático**: Usa `supabase functions deploy`
493
501
 
494
502
  #### Outros Componentes
495
503
  - **Database Settings**: Restaura via SQL
496
504
  - **Storage**: Restaura buckets e arquivos do ZIP (se disponível) ou exibe informações para configuração manual
497
- - **Auth/Realtime**: Exibe informações para configuração manual no Dashboard
505
+ - **Auth/Realtime**: Aplica e confere as configurações pela API; registra os limites da verificação SMTP e diferenças de capacidade do Realtime aceitas explicitamente.
498
506
 
499
507
  ### Multiplataforma
500
508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoonb",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "Complete Supabase backup and migration tool. https://www.smoonb.com/#price",
5
5
  "preferGlobal": false,
6
6
  "preventGlobalInstall": true,
@@ -2,6 +2,7 @@ const chalk = require('chalk');
2
2
  const path = require('path');
3
3
  const { writeJson } = require('../../../utils/fsx');
4
4
  const { t } = require('../../../i18n');
5
+ const { authSnapshot } = require('../../../utils/auth-secrets');
5
6
 
6
7
  /**
7
8
  * Etapa 4: Backup Auth Settings via Management API
@@ -26,15 +27,18 @@ module.exports = async ({ projectId, accessToken, backupDir }) => {
26
27
 
27
28
  const authSettings = await authResponse.json();
28
29
 
29
- // Salvar configurações de Auth
30
+ const snapshot = authSnapshot(authSettings);
31
+ // Never store the opaque SMTP readback as a recoverable password.
30
32
  const authSettingsPath = path.join(backupDir, 'auth-settings.json');
31
33
  await writeJson(authSettingsPath, {
32
34
  project_id: projectId,
33
35
  timestamp: new Date().toISOString(),
34
- settings: authSettings
36
+ ...snapshot
35
37
  });
36
38
 
37
39
  console.log(chalk.green(`✅ Auth Settings exportadas: ${path.basename(authSettingsPath)}`));
40
+ if (snapshot.secrets.smtp_pass.requires_input)
41
+ console.log(chalk.yellow(getT('backup.smtp.passwordNotExported')));
38
42
  return { success: true };
39
43
 
40
44
  } catch (error) {
@@ -0,0 +1,65 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const inquirer = require('inquirer');
4
+ const { t } = require('../../i18n');
5
+ const { remapServiceUrls } = require('./dump');
6
+ const { isMasked } = require('../../utils/auth-secrets');
7
+
8
+ async function prepareAuth(
9
+ { backupPath, targetProject, remapUrls = true },
10
+ {
11
+ prompt = (questions) => inquirer.prompt(questions),
12
+ interactive = Boolean(process.stdin.isTTY),
13
+ log = console.log,
14
+ } = {},
15
+ ) {
16
+ const getT = global.smoonbI18n?.t || t;
17
+ const backup = JSON.parse(
18
+ fs.readFileSync(path.join(backupPath, 'auth-settings.json'), 'utf8'),
19
+ );
20
+ const settings =
21
+ backup.settings?.auth_url_config ||
22
+ backup.settings ||
23
+ backup.auth_url_config;
24
+ if (!settings || !Object.keys(settings).length)
25
+ throw new Error('Auth configuration missing from backup');
26
+ // Never replay an opaque value from a legacy backup as a password.
27
+ const { smtp_pass: stored, ...readable } = settings;
28
+ const desired = remapUrls
29
+ ? JSON.parse(
30
+ remapServiceUrls(
31
+ JSON.stringify(readable),
32
+ backup.project_id,
33
+ targetProject.targetProjectId,
34
+ ),
35
+ )
36
+ : readable;
37
+ const needsPassword = Boolean(
38
+ stored || settings.smtp_host || backup.secrets?.smtp_pass?.requires_input,
39
+ );
40
+ const writeOnly = {};
41
+ if (needsPassword) {
42
+ log(getT('restore.smtp.explanation'));
43
+ if (!interactive) throw new Error(getT('restore.smtp.interactiveRequired'));
44
+ const validate = (value) =>
45
+ typeof value === 'string' &&
46
+ value.trim().length > 0 &&
47
+ !isMasked(value) &&
48
+ value !== stored;
49
+ const answer = await prompt([
50
+ {
51
+ type: 'password',
52
+ name: 'smtpPassword',
53
+ mask: '*',
54
+ message: getT('restore.smtp.password'),
55
+ validate: (value) => validate(value) || getT('restore.smtp.required'),
56
+ },
57
+ ]);
58
+ if (!validate(answer.smtpPassword))
59
+ throw new Error(getT('restore.smtp.required'));
60
+ writeOnly.smtp_pass = answer.smtpPassword;
61
+ }
62
+ return { desired, writeOnly };
63
+ }
64
+
65
+ module.exports = { prepareAuth };
@@ -174,7 +174,10 @@ module.exports = async (options = {}) => {
174
174
  const restoreResults = await runRestore({ selectedBackup, components, targetProject, processDir, postgresMajor:parsedMajor, remapUrls });
175
175
 
176
176
  // Exibir resumo final
177
- ui.success(`\n🎉 ${getT('restore.index.restoreComplete')}`);
177
+ if (Object.values(restoreResults).some(result => !result.verified))
178
+ ui.warn(`\n${getT('restore.index.restoreCompleteWithNotes')}`);
179
+ else
180
+ ui.success(`\n🎉 ${getT('restore.index.restoreComplete')}`);
178
181
  ui.link(`🎯 ${getT('restore.index.targetProjectFinal', { projectId: targetProject.targetProjectId || getT('restore.index.notConfigured') })}`);
179
182
 
180
183
  if (restoreResults.database) {
@@ -188,7 +191,10 @@ module.exports = async (options = {}) => {
188
191
  }
189
192
 
190
193
  if (restoreResults.authSettings) {
191
- ui.success(`🔐 ${getT('restore.index.authSettingsRestored')}`);
194
+ if (restoreResults.authSettings.verified)
195
+ ui.success(`🔐 ${getT('restore.index.authSettingsRestored')}`);
196
+ else
197
+ ui.warn(getT('restore.smtp.accepted'));
192
198
  }
193
199
 
194
200
  if (restoreResults.storage) {
@@ -208,7 +214,10 @@ module.exports = async (options = {}) => {
208
214
  }
209
215
 
210
216
  if (restoreResults.realtimeSettings) {
211
- ui.success(`🔄 ${getT('restore.index.realtimeSettingsRestored')}`);
217
+ if (restoreResults.realtimeSettings.verified)
218
+ ui.success(`🔄 ${getT('restore.index.realtimeSettingsRestored')}`);
219
+ else
220
+ ui.warn(getT('restore.realtime.accepted'));
212
221
  }
213
222
 
214
223
  sendTelemetry({
@@ -1,3 +1,15 @@
1
+ const { isMasked } = require('../../utils/auth-secrets');
2
+
3
+ class ConfigurationVerificationError extends Error {
4
+ constructor(fields, updatedFields) {
5
+ super(`Configuration verification failed: ${fields.join(', ')}`);
6
+ this.name = 'ConfigurationVerificationError';
7
+ // Field names only: Auth readback may contain credentials.
8
+ this.fields = [...fields];
9
+ this.updatedFields = [...updatedFields];
10
+ }
11
+ }
12
+
1
13
  async function management(
2
14
  target,
3
15
  endpoint,
@@ -33,9 +45,29 @@ async function applyConfig(
33
45
  desired,
34
46
  allowed,
35
47
  request = management,
48
+ { writeOnly = {} } = {},
36
49
  ) {
50
+ const secretFields = Object.keys(writeOnly);
51
+ for (const key of secretFields) {
52
+ if (
53
+ endpoint !== 'config/auth' ||
54
+ key !== 'smtp_pass' ||
55
+ !allowed.includes(key)
56
+ )
57
+ throw new Error(`Unsupported write-only configuration: ${key}`);
58
+ if (
59
+ typeof writeOnly[key] !== 'string' ||
60
+ !writeOnly[key].trim() ||
61
+ isMasked(writeOnly[key])
62
+ )
63
+ throw new Error(`A real credential is required: ${key}`);
64
+ }
65
+ if (endpoint === 'config/auth' && Object.hasOwn(desired, 'smtp_pass'))
66
+ throw new Error(
67
+ 'SMTP password must be supplied separately, never copied from a configuration response',
68
+ );
37
69
  const before = await request(target, endpoint);
38
- const patch = {};
70
+ const patch = { ...writeOnly };
39
71
  const unsupported = [];
40
72
  for (const [key, value] of Object.entries(desired)) {
41
73
  if (JSON.stringify(before[key]) === JSON.stringify(value)) continue;
@@ -43,10 +75,7 @@ async function applyConfig(
43
75
  unsupported.push(key);
44
76
  continue;
45
77
  }
46
- if (
47
- typeof value === 'string' &&
48
- /^(\*{3,}|[•●]{3,}|\[redacted\])$/i.test(value)
49
- )
78
+ if (isMasked(value))
50
79
  throw new Error(`Masked configuration cannot be restored: ${key}`);
51
80
  patch[key] = value;
52
81
  }
@@ -61,14 +90,15 @@ async function applyConfig(
61
90
  (k) => JSON.stringify(desired[k]) !== JSON.stringify(after[k]),
62
91
  );
63
92
  if (differences.length)
64
- throw new Error(
65
- `Configuration verification failed: ${differences.join(', ')}`,
66
- );
93
+ throw new ConfigurationVerificationError(differences, Object.keys(patch));
67
94
  return {
68
95
  success: true,
69
- verified: true,
96
+ verified: secretFields.length === 0,
97
+ configuration_readback_verified: true,
98
+ write_only_fields_accepted: secretFields,
99
+ ...(secretFields.length ? { email_delivery_verified: false } : {}),
70
100
  fields_verified: Object.keys(desired).length,
71
101
  fields_updated: Object.keys(patch),
72
102
  };
73
103
  }
74
- module.exports = { management, applyConfig };
104
+ module.exports = { management, applyConfig, ConfigurationVerificationError };
@@ -3,6 +3,11 @@ const path = require('path');
3
3
  const { t } = require('../../i18n');
4
4
  const { buildRestore, readDump } = require('./dump');
5
5
  const { storagePlan } = require('./steps/06-storage');
6
+ const { prepareAuth } = require('./auth');
7
+ const {
8
+ RealtimeRestoreError,
9
+ capacityLimits,
10
+ } = require('./steps/08-realtime-settings');
6
11
  const steps = {
7
12
  database: require('./steps/03-database'),
8
13
  edgeFunctions: require('./steps/04-edge-functions'),
@@ -21,6 +26,7 @@ async function runRestore(
21
26
  remapUrls = true,
22
27
  },
23
28
  implementations = steps,
29
+ { prepareAuthConfig = prepareAuth } = {},
24
30
  ) {
25
31
  if (selectedBackup.projectId === targetProject.targetProjectId)
26
32
  throw new Error('Restore requires a different, empty destination project');
@@ -84,24 +90,69 @@ async function runRestore(
84
90
  buildRestore(readDump(context.backupFilePath), context);
85
91
  if (components.storage)
86
92
  storagePlan(context.backupPath, context.backupFilePath);
93
+ // Resolve write-only credentials before restoring the database or any service.
94
+ if (components.authSettings)
95
+ context.authConfig = await prepareAuthConfig(context);
87
96
  for (const [name, step] of Object.entries(implementations)) {
88
97
  if (!components[name]) continue;
89
98
  current = name;
90
99
  report.results[name] = { success: false, status: 'running' };
91
100
  save();
92
101
  const labels = {
93
- database: 'restoringDatabase', edgeFunctions: 'restoringEdgeFunctions',
94
- authSettings: 'restoringAuthSettings', storage: 'restoringStorageBuckets',
95
- databaseSettings: 'restoringDatabaseSettings', realtimeSettings: 'restoringRealtimeSettings',
102
+ database: 'restoringDatabase',
103
+ edgeFunctions: 'restoringEdgeFunctions',
104
+ authSettings: 'restoringAuthSettings',
105
+ storage: 'restoringStorageBuckets',
106
+ databaseSettings: 'restoringDatabaseSettings',
107
+ realtimeSettings: 'restoringRealtimeSettings',
96
108
  };
97
109
  console.log((global.smoonbI18n?.t || t)(`restore.index.${labels[name]}`));
98
110
  const result = await step(context);
99
- if (!result?.success || !result?.verified)
111
+ const acceptedSmtp =
112
+ name === 'authSettings' &&
113
+ result?.configuration_readback_verified === true &&
114
+ result?.email_delivery_verified === false &&
115
+ result?.write_only_fields_accepted?.length === 1 &&
116
+ result.write_only_fields_accepted[0] === 'smtp_pass';
117
+ const acceptedLimits =
118
+ name === 'realtimeSettings' &&
119
+ result?.effective_configuration_verified === true &&
120
+ result?.destination_limits_accepted === true &&
121
+ Array.isArray(result?.accepted_differences) &&
122
+ result.accepted_differences.length > 0 &&
123
+ result.accepted_differences.every(
124
+ (d) =>
125
+ capacityLimits.has(d.field) &&
126
+ Number.isInteger(d.backup_value) &&
127
+ d.backup_value > 0 &&
128
+ Number.isInteger(d.destination_value) &&
129
+ d.destination_value > 0,
130
+ );
131
+ if (
132
+ !result?.success ||
133
+ (!result?.verified && !acceptedSmtp && !acceptedLimits)
134
+ )
100
135
  throw new Error(`Component ${name} did not pass verification`);
101
- report.results[name] = { ...result, status: 'verified' };
136
+ report.results[name] = {
137
+ ...result,
138
+ status: result.verified
139
+ ? 'verified'
140
+ : acceptedLimits
141
+ ? 'completed_with_accepted_differences'
142
+ : 'applied_with_unverified_secrets',
143
+ };
102
144
  save();
145
+ if (acceptedSmtp)
146
+ console.log((global.smoonbI18n?.t || t)('restore.smtp.accepted'));
103
147
  }
104
- report.status = 'verified';
148
+ const results = Object.values(report.results);
149
+ report.status = results.some(
150
+ (r) => r.status === 'completed_with_accepted_differences',
151
+ )
152
+ ? 'completed_with_accepted_differences'
153
+ : results.every((r) => r.verified)
154
+ ? 'verified'
155
+ : 'applied_with_unverified_secrets';
105
156
  report.completed_at = new Date().toISOString();
106
157
  save();
107
158
  return report.results;
@@ -109,6 +160,8 @@ async function runRestore(
109
160
  report.status = 'failed';
110
161
  report.failed_component = current;
111
162
  report.results[current] = { success: false, status: 'failed' };
163
+ if (current === 'realtimeSettings' && error instanceof RealtimeRestoreError)
164
+ report.results[current].unapplied_limits = error.differences;
112
165
  report.completed_at = new Date().toISOString();
113
166
  save();
114
167
  throw error;
@@ -1,26 +1,15 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
1
  const { applyConfig } = require('../management');
4
- const { remapServiceUrls } = require('../dump');
2
+ const { prepareAuth } = require('../auth');
5
3
  const allowed = require('../auth-fields.json');
6
- module.exports = async ({ backupPath, targetProject, remapUrls = true }) => {
7
- const backup = JSON.parse(
8
- fs.readFileSync(path.join(backupPath, 'auth-settings.json'), 'utf8'),
4
+ module.exports = async (context, { request } = {}) => {
5
+ const { desired, writeOnly } =
6
+ context.authConfig || (await prepareAuth(context));
7
+ return applyConfig(
8
+ context.targetProject,
9
+ 'config/auth',
10
+ desired,
11
+ allowed,
12
+ request,
13
+ { writeOnly },
9
14
  );
10
- const settings =
11
- backup.settings?.auth_url_config ||
12
- backup.settings ||
13
- backup.auth_url_config;
14
- if (!settings || !Object.keys(settings).length)
15
- throw new Error('Auth configuration missing from backup');
16
- const desired = remapUrls
17
- ? JSON.parse(
18
- remapServiceUrls(
19
- JSON.stringify(settings),
20
- backup.project_id,
21
- targetProject.targetProjectId,
22
- ),
23
- )
24
- : settings;
25
- return applyConfig(targetProject, 'config/auth', desired, allowed);
26
15
  };
@@ -1,6 +1,12 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
- const { applyConfig } = require('../management');
3
+ const {
4
+ applyConfig,
5
+ management,
6
+ ConfigurationVerificationError,
7
+ } = require('../management');
8
+ const { confirm } = require('../../../utils/prompt');
9
+ const { t } = require('../../../i18n');
4
10
  const allowed = [
5
11
  'private_only',
6
12
  'connection_pool',
@@ -15,7 +21,35 @@ const allowed = [
15
21
  'suspend',
16
22
  'presence_enabled',
17
23
  ];
18
- module.exports = async ({ backupPath, targetProject }) => {
24
+ // Only capacity limits can be explicitly retained. Security, service switches
25
+ // and database connection pools must still match the backup exactly.
26
+ const capacityLimits = new Set([
27
+ 'max_concurrent_users',
28
+ 'max_events_per_second',
29
+ 'max_bytes_per_second',
30
+ 'max_channels_per_client',
31
+ 'max_joins_per_second',
32
+ 'max_presence_events_per_second',
33
+ 'max_payload_size_in_kb',
34
+ ]);
35
+ class RealtimeRestoreError extends Error {
36
+ constructor(message, differences = []) {
37
+ super(message);
38
+ this.name = 'RealtimeRestoreError';
39
+ this.differences = differences;
40
+ }
41
+ }
42
+ module.exports = async (
43
+ { backupPath, targetProject },
44
+ {
45
+ request = management,
46
+ confirmLimits = confirm,
47
+ interactive = Boolean(process.stdin.isTTY),
48
+ wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
49
+ log = console.log,
50
+ } = {},
51
+ ) => {
52
+ const getT = global.smoonbI18n?.t || t;
19
53
  const backup = JSON.parse(
20
54
  fs.readFileSync(path.join(backupPath, 'realtime-settings.json'), 'utf8'),
21
55
  );
@@ -44,18 +78,117 @@ module.exports = async ({ backupPath, targetProject }) => {
44
78
  ];
45
79
  }),
46
80
  );
47
- console.log(
48
- `Realtime: restoring the recorded snapshot (${backup.realtime_settings.captured_at || 'date unavailable'}).`,
81
+ log(
82
+ getT('restore.realtime.snapshot', {
83
+ date:
84
+ backup.realtime_settings.captured_at ||
85
+ getT('restore.realtime.unknownDate'),
86
+ }),
49
87
  );
50
88
  }
51
- const result = await applyConfig(
52
- targetProject,
53
- 'config/realtime',
54
- desired,
55
- allowed,
56
- );
57
- return {
58
- ...result,
59
- captured_at: backup.captured_at || backup.realtime_settings?.captured_at,
60
- };
89
+ const capturedAt =
90
+ backup.captured_at || backup.realtime_settings?.captured_at;
91
+ const mismatches = (state) =>
92
+ Object.keys(desired).filter(
93
+ (key) => JSON.stringify(desired[key]) !== JSON.stringify(state[key]),
94
+ );
95
+ try {
96
+ const result = await applyConfig(
97
+ targetProject,
98
+ 'config/realtime',
99
+ desired,
100
+ allowed,
101
+ request,
102
+ );
103
+ return { ...result, captured_at: capturedAt };
104
+ } catch (error) {
105
+ if (!(error instanceof ConfigurationVerificationError)) throw error;
106
+ // A successful PATCH may take time to become visible. Never repeat PATCH
107
+ // just to poll: changing Realtime settings can disconnect clients.
108
+ let actual;
109
+ let fields;
110
+ for (const delay of [1000, 2000]) {
111
+ await wait(delay);
112
+ actual = await request(targetProject, 'config/realtime');
113
+ fields = mismatches(actual);
114
+ if (!fields.length)
115
+ return {
116
+ success: true,
117
+ verified: true,
118
+ configuration_readback_verified: true,
119
+ fields_verified: Object.keys(desired).length,
120
+ fields_updated: error.updatedFields,
121
+ captured_at: capturedAt,
122
+ };
123
+ }
124
+ const canRetain = fields.every(
125
+ (key) =>
126
+ capacityLimits.has(key) &&
127
+ Number.isInteger(desired[key]) &&
128
+ desired[key] > 0 &&
129
+ Number.isInteger(actual[key]) &&
130
+ actual[key] > 0,
131
+ );
132
+ if (!canRetain)
133
+ throw new RealtimeRestoreError(
134
+ getT('restore.realtime.strictMismatch', { fields: fields.join(', ') }),
135
+ );
136
+ const differences = fields.map((field) => ({
137
+ field,
138
+ backup_value: desired[field],
139
+ destination_value: actual[field],
140
+ }));
141
+ log(getT('restore.realtime.limitsNotApplied'));
142
+ for (const difference of differences)
143
+ log(
144
+ getT('restore.realtime.difference', {
145
+ field: getT(`restore.realtime.fields.${difference.field}`),
146
+ backup: difference.backup_value,
147
+ destination: difference.destination_value,
148
+ }),
149
+ );
150
+ log(getT('restore.realtime.limitsExplanation'));
151
+ if (
152
+ !interactive ||
153
+ (await confirmLimits(getT('restore.realtime.acceptLimits'), false)) !==
154
+ true
155
+ )
156
+ throw new RealtimeRestoreError(
157
+ getT('restore.realtime.notAccepted'),
158
+ differences,
159
+ );
160
+ // The user approved these exact numeric differences. A fresh read must
161
+ // still match both the approved limits and every other backed-up field.
162
+ const accepted = {
163
+ ...desired,
164
+ ...Object.fromEntries(
165
+ differences.map((d) => [d.field, d.destination_value]),
166
+ ),
167
+ };
168
+ const final = await request(targetProject, 'config/realtime');
169
+ if (
170
+ Object.keys(accepted).some(
171
+ (key) => JSON.stringify(accepted[key]) !== JSON.stringify(final[key]),
172
+ )
173
+ )
174
+ throw new RealtimeRestoreError(
175
+ getT('restore.realtime.changed'),
176
+ differences,
177
+ );
178
+ log(getT('restore.realtime.accepted'));
179
+ return {
180
+ success: true,
181
+ verified: false,
182
+ effective_configuration_verified: true,
183
+ destination_limits_accepted: true,
184
+ accepted_differences: differences,
185
+ fields_verified: Object.keys(desired).length - differences.length,
186
+ fields_updated: error.updatedFields.filter(
187
+ (key) => !fields.includes(key),
188
+ ),
189
+ captured_at: capturedAt,
190
+ };
191
+ }
61
192
  };
193
+ module.exports.RealtimeRestoreError = RealtimeRestoreError;
194
+ module.exports.capacityLimits = capacityLimits;
@@ -18,6 +18,30 @@
18
18
  "prompt.confirmYes": "(Y/n)",
19
19
  "prompt.confirmNo": "(y/N)",
20
20
  "prompt.cancelled": "Operation cancelled.",
21
+ "backup.smtp.passwordNotExported": "⚠️ Supabase does not export the SMTP password. Keep it in your password manager: restoring Auth will require it. You do not need to create another backup.",
22
+ "restore.smtp.explanation": "The backup contains SMTP settings, but Supabase does not let you retrieve the password. Enter your email provider password (or the key used as the SMTP password). It will be sent only to the destination Supabase project and will not be saved by smoonb.",
23
+ "restore.smtp.password": "Email provider SMTP password:",
24
+ "restore.smtp.required": "Enter the real provider password, not the protected value in auth-settings.json. Use Ctrl+C to cancel before restoring any components.",
25
+ "restore.smtp.interactiveRequired": "Restoring Auth requires the SMTP password. Run in an interactive terminal to enter it securely before any components are restored.",
26
+ "restore.smtp.accepted": "⚠️ Auth: settings checked; SMTP password accepted by the API. Supabase does not allow password readback verification. Test an authentication email before using the project.",
27
+ "restore.realtime.snapshot": "Realtime: using the snapshot recorded at {date}.",
28
+ "restore.realtime.unknownDate": "unknown date",
29
+ "restore.realtime.limitsNotApplied": "⚠️ Supabase did not apply these Realtime limits from the backup:",
30
+ "restore.realtime.difference": " - {field}: backup = {backup}; destination = {destination}.",
31
+ "restore.realtime.limitsExplanation": "Limits may depend on the plan and organization billing restrictions; this response does not identify the cause. You can keep the current destination values. This choice will be recorded as accepted differences, without claiming an identical copy of the backup. No plan or spend cap will be changed.",
32
+ "restore.realtime.acceptLimits": "Keep these destination limits and complete Realtime with the differences above recorded?",
33
+ "restore.realtime.notAccepted": "Realtime remains pending: the backup limits were not applied and the destination values were not accepted. Changes already applied were not undone. Consult report.json and select only Realtime Settings in an interactive terminal to retry this step.",
34
+ "restore.realtime.changed": "Realtime configuration changed during confirmation. The step remains pending; consult report.json and select only Realtime Settings to retry.",
35
+ "restore.realtime.strictMismatch": "Realtime did not confirm these settings: {fields}. This difference cannot be accepted as a capacity adjustment. The step remains pending; consult report.json and select only Realtime Settings to retry.",
36
+ "restore.realtime.accepted": "⚠️ Realtime completed with destination limits accepted by you. Differences from the backup are recorded in report.json; the other Realtime settings from the backup were checked.",
37
+ "restore.realtime.fields.max_concurrent_users": "Concurrent clients",
38
+ "restore.realtime.fields.max_events_per_second": "Events per second",
39
+ "restore.realtime.fields.max_bytes_per_second": "Bytes per second",
40
+ "restore.realtime.fields.max_channels_per_client": "Channels per client",
41
+ "restore.realtime.fields.max_joins_per_second": "Channel joins per second",
42
+ "restore.realtime.fields.max_presence_events_per_second": "Presence events per second",
43
+ "restore.realtime.fields.max_payload_size_in_kb": "Maximum message size (KB)",
44
+ "restore.index.restoreCompleteWithNotes": "Selected components restored with notes. Review the accepted differences and pending verification below.",
21
45
 
22
46
  "error.generic": "Something went wrong.",
23
47
  "error.pathNotFound": "Path not found: {path}",
@@ -570,7 +594,7 @@
570
594
 
571
595
  "restore.steps.components.auth.title": "Auth Settings:",
572
596
  "restore.steps.components.auth.description1": "Auth settings will be applied through the API and verified on the destination.",
573
- "restore.steps.components.auth.description2": "Includes SMTP and redirects recorded in the backup.",
597
+ "restore.steps.components.auth.description2": "Includes redirects and SMTP settings; the real provider password will be requested when needed.",
574
598
  "restore.steps.components.auth.include": "Do you want to restore Auth",
575
599
 
576
600
  "restore.steps.components.storage.title": "Storage:",
@@ -589,7 +613,7 @@
589
613
 
590
614
  "restore.steps.components.realtime.title": "Realtime Settings:",
591
615
  "restore.steps.components.realtime.description1": "Realtime settings from the backup will be applied and verified through the API.",
592
- "restore.steps.components.realtime.description2": "Legacy captures reproduce the values recorded on their capture date.",
616
+ "restore.steps.components.realtime.description2": "Uses the recorded snapshot; limits that are not applied will be shown for your decision.",
593
617
  "restore.steps.components.realtime.include": "Do you want to restore Realtime",
594
618
  "restore.steps.components.database.include": "Do you want to restore Database",
595
619
 
@@ -746,21 +770,21 @@
746
770
  "restore.index.targetProjectFinal": "Target project: {projectId}",
747
771
  "restore.index.databaseRestored": "Database: Restored successfully via Docker",
748
772
  "restore.index.edgeFunctionsRestored": "Edge Functions: {success}/{total} functions restored",
749
- "restore.index.authSettingsRestored": "Auth Settings: Settings displayed for manual configuration",
773
+ "restore.index.authSettingsRestored": "Auth Settings: Settings applied and checked through the API",
750
774
  "restore.index.storageRestored": "Storage: {buckets} bucket(s) restored, {files} file(s) uploaded",
751
775
  "restore.index.storageMetadataOnly": "Storage: {buckets} bucket(s) found - metadata only (.storage.zip file not found)",
752
776
  "restore.index.databaseSettingsRestored": "Database Settings: Extensions and settings restored via SQL",
753
- "restore.index.realtimeSettingsRestored": "Realtime Settings: Settings displayed for manual configuration",
777
+ "restore.index.realtimeSettingsRestored": "Realtime Settings: Settings applied and checked through the API",
754
778
  "restore.index.error": "Error during restoration: {message}",
755
779
 
756
780
  "restore.utils.componentsTitle": "Components that will be restored:",
757
781
  "restore.utils.database": "Database (psql -f via Docker)",
758
782
  "restore.utils.edgeFunctions": "Edge Functions: {count} function(s)",
759
- "restore.utils.authSettings": "Auth Settings: Display URL and values for manual configuration",
783
+ "restore.utils.authSettings": "Auth Settings: Apply and check through the API; SMTP password requested when needed",
760
784
  "restore.utils.storageBuckets": "Storage Buckets: Automatic restoration of buckets and files via API",
761
785
  "restore.utils.storageMetadataOnly": "Storage Buckets: Display information (metadata only - .storage.zip file not found)",
762
786
  "restore.utils.databaseSettings": "Database Extensions and Settings: Restore via SQL",
763
- "restore.utils.realtimeSettings": "Realtime Settings: Display URL and values for manual configuration",
787
+ "restore.utils.realtimeSettings": "Realtime Settings: Apply and check through the API; limit differences require your choice",
764
788
  "restore.utils.summaryTitle": "Restoration Summary:",
765
789
  "restore.utils.summarySeparator": "═══════════════════════════════════════════════════════════════════════════════",
766
790
  "restore.utils.backup": "Backup: {name}",
@@ -18,6 +18,30 @@
18
18
  "prompt.confirmYes": "(S/n)",
19
19
  "prompt.confirmNo": "(s/N)",
20
20
  "prompt.cancelled": "Operação cancelada.",
21
+ "backup.smtp.passwordNotExported": "⚠️ O Supabase não exporta a senha SMTP. Guarde-a no seu gerenciador de senhas: ela será solicitada ao restaurar Auth. Não é necessário refazer o backup.",
22
+ "restore.smtp.explanation": "O backup contém as configurações SMTP, mas o Supabase não permite recuperar a senha. Informe a senha do provedor de e-mail (ou a chave usada como senha SMTP). Ela será enviada somente ao Supabase de destino e não será gravada pelo smoonb.",
23
+ "restore.smtp.password": "Senha SMTP do provedor de e-mail:",
24
+ "restore.smtp.required": "Informe a senha real do provedor, não o valor protegido de auth-settings.json. Use Ctrl+C para cancelar antes de restaurar os componentes.",
25
+ "restore.smtp.interactiveRequired": "A restauração de Auth precisa da senha SMTP. Execute em um terminal interativo para informá-la de forma oculta antes de restaurar os componentes.",
26
+ "restore.smtp.accepted": "⚠️ Auth: configurações conferidas; senha SMTP aceita pela API. O Supabase não permite conferir a senha por leitura. Teste o envio de e-mail de autenticação antes de usar o projeto.",
27
+ "restore.realtime.snapshot": "Realtime: usando a captura registrada em {date}.",
28
+ "restore.realtime.unknownDate": "data não informada",
29
+ "restore.realtime.limitsNotApplied": "⚠️ O Supabase não aplicou estes limites de Realtime do backup:",
30
+ "restore.realtime.difference": " - {field}: backup = {backup}; destino = {destination}.",
31
+ "restore.realtime.limitsExplanation": "Limites podem depender do plano e das restrições de cobrança da organização; esta resposta não informa a causa. Você pode manter os valores atuais do destino. Essa escolha será registrada no relatório como diferença aceita, sem declarar uma cópia idêntica do backup. Nenhum plano ou limite de gastos será alterado.",
32
+ "restore.realtime.acceptLimits": "Manter esses limites do destino e concluir Realtime com as diferenças acima registradas?",
33
+ "restore.realtime.notAccepted": "Realtime permanece pendente: os limites do backup não foram aplicados e os valores do destino não foram aceitos. Alterações já aplicadas não foram desfeitas. Consulte report.json e, para tentar esta etapa novamente, selecione somente Realtime Settings em um terminal interativo.",
34
+ "restore.realtime.changed": "A configuração Realtime mudou durante a confirmação. A etapa permanece pendente; consulte report.json e tente novamente selecionando somente Realtime Settings.",
35
+ "restore.realtime.strictMismatch": "Realtime não confirmou estas configurações: {fields}. Essa diferença não pode ser aceita como ajuste de limites. A etapa permanece pendente; consulte report.json e tente novamente selecionando somente Realtime Settings.",
36
+ "restore.realtime.accepted": "⚠️ Realtime concluído com os limites do destino aceitos por você. As diferenças do backup estão registradas em report.json; as demais configurações de Realtime do backup foram conferidas.",
37
+ "restore.realtime.fields.max_concurrent_users": "Clientes simultâneos",
38
+ "restore.realtime.fields.max_events_per_second": "Eventos por segundo",
39
+ "restore.realtime.fields.max_bytes_per_second": "Bytes por segundo",
40
+ "restore.realtime.fields.max_channels_per_client": "Canais por cliente",
41
+ "restore.realtime.fields.max_joins_per_second": "Entradas em canais por segundo",
42
+ "restore.realtime.fields.max_presence_events_per_second": "Eventos de presença por segundo",
43
+ "restore.realtime.fields.max_payload_size_in_kb": "Tamanho máximo da mensagem (KB)",
44
+ "restore.index.restoreCompleteWithNotes": "Restauração dos componentes selecionados concluída com observações. Confira abaixo as diferenças aceitas e verificações pendentes.",
21
45
 
22
46
  "error.generic": "Algo inesperado aconteceu.",
23
47
  "error.pathNotFound": "Caminho não encontrado: {path}",
@@ -569,7 +593,7 @@
569
593
 
570
594
  "restore.steps.components.auth.title": "Auth Settings:",
571
595
  "restore.steps.components.auth.description1": "As configurações de Auth serão aplicadas pela API e verificadas no destino.",
572
- "restore.steps.components.auth.description2": "Inclui SMTP e redirecionamentos registrados no backup.",
596
+ "restore.steps.components.auth.description2": "Inclui redirecionamentos e configurações SMTP; a senha real do provedor será solicitada quando necessária.",
573
597
  "restore.steps.components.auth.include": "Deseja restaurar Auth",
574
598
 
575
599
  "restore.steps.components.storage.title": "Storage:",
@@ -588,7 +612,7 @@
588
612
 
589
613
  "restore.steps.components.realtime.title": "Realtime Settings:",
590
614
  "restore.steps.components.realtime.description1": "As configurações de Realtime do backup serão aplicadas e verificadas pela API.",
591
- "restore.steps.components.realtime.description2": "Capturas antigas reproduzem os valores registrados naquela data.",
615
+ "restore.steps.components.realtime.description2": "Usa os valores registrados na captura; limites não aplicados serão mostrados para sua decisão.",
592
616
  "restore.steps.components.realtime.include": "Deseja restaurar Realtime",
593
617
  "restore.steps.components.database.include": "Deseja restaurar Database",
594
618
 
@@ -745,21 +769,21 @@
745
769
  "restore.index.targetProjectFinal": "Projeto destino: {projectId}",
746
770
  "restore.index.databaseRestored": "Database: Restaurada com sucesso via Docker",
747
771
  "restore.index.edgeFunctionsRestored": "Edge Functions: {success}/{total} functions restauradas",
748
- "restore.index.authSettingsRestored": "Auth Settings: Configurações exibidas para configuração manual",
772
+ "restore.index.authSettingsRestored": "Auth Settings: Configurações aplicadas e conferidas pela API",
749
773
  "restore.index.storageRestored": "Storage: {buckets} bucket(s) restaurado(s), {files} arquivo(s) enviado(s)",
750
774
  "restore.index.storageMetadataOnly": "Storage: {buckets} bucket(s) encontrado(s) - apenas metadados (arquivo .storage.zip não encontrado)",
751
775
  "restore.index.databaseSettingsRestored": "Database Settings: Extensões e configurações restauradas via SQL",
752
- "restore.index.realtimeSettingsRestored": "Realtime Settings: Configurações exibidas para configuração manual",
776
+ "restore.index.realtimeSettingsRestored": "Realtime Settings: Configurações aplicadas e conferidas pela API",
753
777
  "restore.index.error": "Erro na restauração: {message}",
754
778
 
755
779
  "restore.utils.componentsTitle": "Componentes que serão restaurados:",
756
780
  "restore.utils.database": "Database (psql -f via Docker)",
757
781
  "restore.utils.edgeFunctions": "Edge Functions: {count} function(s)",
758
- "restore.utils.authSettings": "Auth Settings: Exibir URL e valores para configuração manual",
782
+ "restore.utils.authSettings": "Auth Settings: Aplicar pela API e conferir as configurações; senha SMTP solicitada quando necessária",
759
783
  "restore.utils.storageBuckets": "Storage Buckets: Restauração automática de buckets e arquivos via API",
760
784
  "restore.utils.storageMetadataOnly": "Storage Buckets: Exibir informações (apenas metadados - arquivo .storage.zip não encontrado)",
761
785
  "restore.utils.databaseSettings": "Database Extensions and Settings: Restaurar via SQL",
762
- "restore.utils.realtimeSettings": "Realtime Settings: Exibir URL e valores para configuração manual",
786
+ "restore.utils.realtimeSettings": "Realtime Settings: Aplicar pela API e conferir; diferenças nos limites exigem sua escolha",
763
787
  "restore.utils.summaryTitle": "Resumo da Restauração:",
764
788
  "restore.utils.summarySeparator": "═══════════════════════════════════════════════════════════════════════════════",
765
789
  "restore.utils.backup": "Backup: {name}",
@@ -0,0 +1,23 @@
1
+ // Supabase SMTP passwords are write-only. The GET response is not a
2
+ // transferable credential, even when it looks like an ordinary string.
3
+ // https://github.com/supabase/supabase/blob/master/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
4
+ function authSnapshot(settings) {
5
+ const { smtp_pass: smtpPass, ...readable } = settings;
6
+ return {
7
+ settings: readable,
8
+ secrets: {
9
+ smtp_pass: {
10
+ exportable: false,
11
+ requires_input: Boolean(smtpPass || settings.smtp_host),
12
+ },
13
+ },
14
+ };
15
+ }
16
+
17
+ function isMasked(value) {
18
+ return (
19
+ typeof value === 'string' && /^(\*{3,}|[•●]{3,}|\[redacted\])$/i.test(value)
20
+ );
21
+ }
22
+
23
+ module.exports = { authSnapshot, isMasked };