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 +31 -0
- package/README.md +19 -11
- package/README.pt-BR.md +17 -9
- package/package.json +1 -1
- package/src/commands/backup/steps/05-auth-settings.js +6 -2
- package/src/commands/restore/auth.js +65 -0
- package/src/commands/restore/index.js +12 -3
- package/src/commands/restore/management.js +40 -10
- package/src/commands/restore/runner.js +59 -6
- package/src/commands/restore/steps/05-auth-settings.js +11 -22
- package/src/commands/restore/steps/08-realtime-settings.js +147 -14
- package/src/i18n/locales/en.json +30 -6
- package/src/i18n/locales/pt-BR.json +30 -6
- package/src/utils/auth-secrets.js +23 -0
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** (
|
|
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 (
|
|
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) -
|
|
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.
|
|
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**:
|
|
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
|
-
- **
|
|
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` -
|
|
493
|
+
- `.backup.gz` - Reads compressed content without modifying the archive
|
|
486
494
|
- `.backup` - Restores directly via `psql` (Docker)
|
|
487
|
-
- **
|
|
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
|
-
- **
|
|
491
|
-
- **
|
|
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**:
|
|
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** (
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
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` -
|
|
493
|
+
- `.backup.gz` - Lê o conteúdo compactado sem alterar o arquivo original
|
|
486
494
|
- `.backup` - Restaura diretamente via `psql` (Docker)
|
|
487
|
-
- **
|
|
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
|
|
491
|
-
- **
|
|
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**:
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
65
|
-
`Configuration verification failed: ${differences.join(', ')}`,
|
|
66
|
-
);
|
|
93
|
+
throw new ConfigurationVerificationError(differences, Object.keys(patch));
|
|
67
94
|
return {
|
|
68
95
|
success: true,
|
|
69
|
-
verified:
|
|
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',
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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] = {
|
|
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
|
-
|
|
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 {
|
|
2
|
+
const { prepareAuth } = require('../auth');
|
|
5
3
|
const allowed = require('../auth-fields.json');
|
|
6
|
-
module.exports = async ({
|
|
7
|
-
const
|
|
8
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
desired
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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;
|
package/src/i18n/locales/en.json
CHANGED
|
@@ -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
|
|
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": "
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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": "
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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 };
|