smoonb 1.0.7 → 1.0.8

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.
@@ -0,0 +1,74 @@
1
+ async function management(
2
+ target,
3
+ endpoint,
4
+ { method = 'GET', body, fetchImpl = fetch } = {},
5
+ ) {
6
+ if (!/^[a-z0-9-]+$/.test(target.targetProjectId) || !target.targetAccessToken)
7
+ throw new Error('Management API credentials are required');
8
+ let response;
9
+ try {
10
+ response = await fetchImpl(
11
+ `https://api.supabase.com/v1/projects/${target.targetProjectId}/${endpoint}`,
12
+ {
13
+ method,
14
+ signal: AbortSignal.timeout(60000),
15
+ headers: {
16
+ Authorization: `Bearer ${target.targetAccessToken}`,
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
20
+ },
21
+ );
22
+ } catch {
23
+ throw new Error(`Management API ${endpoint}: network failure`);
24
+ }
25
+ // Error response bodies may contain secrets from the submitted configuration.
26
+ if (!response.ok)
27
+ throw new Error(`Management API ${endpoint}: HTTP ${response.status}`);
28
+ return response.status === 204 ? null : response.json();
29
+ }
30
+ async function applyConfig(
31
+ target,
32
+ endpoint,
33
+ desired,
34
+ allowed,
35
+ request = management,
36
+ ) {
37
+ const before = await request(target, endpoint);
38
+ const patch = {};
39
+ const unsupported = [];
40
+ for (const [key, value] of Object.entries(desired)) {
41
+ if (JSON.stringify(before[key]) === JSON.stringify(value)) continue;
42
+ if (!allowed.includes(key)) {
43
+ unsupported.push(key);
44
+ continue;
45
+ }
46
+ if (
47
+ typeof value === 'string' &&
48
+ /^(\*{3,}|[•●]{3,}|\[redacted\])$/i.test(value)
49
+ )
50
+ throw new Error(`Masked configuration cannot be restored: ${key}`);
51
+ patch[key] = value;
52
+ }
53
+ if (unsupported.length)
54
+ throw new Error(
55
+ `Configuration requires manual migration: ${unsupported.join(', ')}`,
56
+ );
57
+ if (Object.keys(patch).length)
58
+ await request(target, endpoint, { method: 'PATCH', body: patch });
59
+ const after = await request(target, endpoint);
60
+ const differences = Object.keys(desired).filter(
61
+ (k) => JSON.stringify(desired[k]) !== JSON.stringify(after[k]),
62
+ );
63
+ if (differences.length)
64
+ throw new Error(
65
+ `Configuration verification failed: ${differences.join(', ')}`,
66
+ );
67
+ return {
68
+ success: true,
69
+ verified: true,
70
+ fields_verified: Object.keys(desired).length,
71
+ fields_updated: Object.keys(patch),
72
+ };
73
+ }
74
+ module.exports = { management, applyConfig };
@@ -0,0 +1,83 @@
1
+ const { spawn } = require('child_process');
2
+
3
+ const ident = (value) => '"' + String(value).replace(/"/g, '""') + '"';
4
+ const literal = (value) => "'" + String(value).replace(/'/g, "''") + "'";
5
+
6
+ // SQL and credentials go through stdin/environment, never a shell command.
7
+ function runSql(databaseUrl, sql, { postgresMajor = 17 } = {}) {
8
+ const url = new URL(databaseUrl);
9
+ if (!['postgres:', 'postgresql:'].includes(url.protocol))
10
+ throw new Error('Invalid database URL');
11
+ if (![15, 16, 17].includes(Number(postgresMajor)))
12
+ throw new Error('Unsupported PostgreSQL major');
13
+ const env = {
14
+ ...process.env,
15
+ PGHOST: url.hostname,
16
+ PGPORT: url.port || '5432',
17
+ PGUSER: decodeURIComponent(url.username),
18
+ PGPASSWORD: decodeURIComponent(url.password),
19
+ PGDATABASE: decodeURIComponent(url.pathname.slice(1) || 'postgres'),
20
+ PGSSLMODE: url.searchParams.get('sslmode') || 'prefer',
21
+ PGCONNECT_TIMEOUT: '20',
22
+ };
23
+ const args = [
24
+ 'run',
25
+ '--rm',
26
+ '-i',
27
+ '--network',
28
+ 'host',
29
+ ...[
30
+ 'PGHOST',
31
+ 'PGPORT',
32
+ 'PGUSER',
33
+ 'PGPASSWORD',
34
+ 'PGDATABASE',
35
+ 'PGSSLMODE',
36
+ 'PGCONNECT_TIMEOUT',
37
+ ].flatMap((k) => ['-e', k]),
38
+ `postgres:${postgresMajor}`,
39
+ 'psql',
40
+ '-X',
41
+ '-q',
42
+ '-A',
43
+ '-t',
44
+ '-v',
45
+ 'ON_ERROR_STOP=1',
46
+ '-v',
47
+ 'VERBOSITY=sqlstate',
48
+ ];
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawn('docker', args, {
51
+ env,
52
+ windowsHide: true,
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ });
55
+ let output = '';
56
+ let errors = '';
57
+ child.stdout.on('data', (chunk) => {
58
+ output += chunk;
59
+ });
60
+ child.stderr.on('data', (chunk) => {
61
+ errors += chunk;
62
+ });
63
+ child.stdin.on('error', () => {});
64
+ child.on('error', () =>
65
+ reject(new Error('Could not start Docker PostgreSQL client')),
66
+ );
67
+ child.on('close', (code) => {
68
+ if (code !== 0) {
69
+ // COPY errors can contain customer records. Expose SQLSTATE only.
70
+ const states = [
71
+ ...new Set(errors.match(/\b[0-9A-Z]{5}\b/g) || []),
72
+ ].filter((x) => /\d/.test(x));
73
+ reject(
74
+ new Error(
75
+ `Database operation failed (exit ${code}; SQLSTATE ${states.join(', ') || 'unavailable'}). Transaction rolled back when applicable.`,
76
+ ),
77
+ );
78
+ } else resolve(output.trim());
79
+ });
80
+ child.stdin.end(sql);
81
+ });
82
+ }
83
+ module.exports = { runSql, ident, literal };
@@ -0,0 +1,117 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { t } = require('../../i18n');
4
+ const { buildRestore, readDump } = require('./dump');
5
+ const { storagePlan } = require('./steps/06-storage');
6
+ const steps = {
7
+ database: require('./steps/03-database'),
8
+ edgeFunctions: require('./steps/04-edge-functions'),
9
+ authSettings: require('./steps/05-auth-settings'),
10
+ storage: require('./steps/06-storage'),
11
+ databaseSettings: require('./steps/07-database-settings'),
12
+ realtimeSettings: require('./steps/08-realtime-settings'),
13
+ };
14
+ async function runRestore(
15
+ {
16
+ selectedBackup,
17
+ components,
18
+ targetProject,
19
+ processDir,
20
+ postgresMajor,
21
+ remapUrls = true,
22
+ },
23
+ implementations = steps,
24
+ ) {
25
+ if (selectedBackup.projectId === targetProject.targetProjectId)
26
+ throw new Error('Restore requires a different, empty destination project');
27
+ const context = {
28
+ backupPath: selectedBackup.path,
29
+ backupFilePath: path.join(selectedBackup.path, selectedBackup.backupFile),
30
+ targetProject,
31
+ targetDatabaseUrl: targetProject.targetDatabaseUrl,
32
+ sourceProjectId: selectedBackup.projectId,
33
+ targetProjectId: targetProject.targetProjectId,
34
+ postgresMajor,
35
+ remapUrls,
36
+ };
37
+ const report = {
38
+ process: 'restore',
39
+ smoonb_version: require('../../../package.json').version,
40
+ created_at: new Date().toISOString(),
41
+ source_project_id: selectedBackup.projectId,
42
+ target_project_id: targetProject.targetProjectId,
43
+ status: 'running',
44
+ components,
45
+ results: {},
46
+ remap_service_urls: remapUrls,
47
+ notes: [
48
+ 'Local source files and backup archives are preserved.',
49
+ 'Verification covers selected components; application login and workflows require acceptance testing.',
50
+ ],
51
+ };
52
+ const save = () =>
53
+ fs.writeFileSync(
54
+ path.join(processDir, 'report.json'),
55
+ JSON.stringify(report, null, 2),
56
+ );
57
+ save();
58
+ let current = 'preflight';
59
+ try {
60
+ if (targetProject.targetDatabaseUrl) {
61
+ const url = new URL(targetProject.targetDatabaseUrl);
62
+ const hosted =
63
+ url.hostname.endsWith('.supabase.co') ||
64
+ url.hostname.endsWith('.supabase.com');
65
+ if (
66
+ hosted &&
67
+ url.hostname !== `db.${targetProject.targetProjectId}.supabase.co` &&
68
+ !decodeURIComponent(url.username).endsWith(
69
+ `.${targetProject.targetProjectId}`,
70
+ )
71
+ )
72
+ throw new Error(
73
+ 'Database connection does not match the destination project',
74
+ );
75
+ }
76
+ if (
77
+ targetProject.targetUrl &&
78
+ new URL(targetProject.targetUrl).hostname !==
79
+ `${targetProject.targetProjectId}.supabase.co`
80
+ )
81
+ throw new Error('API URL does not match the destination project');
82
+ // Parse and validate inventories before the first mutation.
83
+ if (components.database)
84
+ buildRestore(readDump(context.backupFilePath), context);
85
+ if (components.storage)
86
+ storagePlan(context.backupPath, context.backupFilePath);
87
+ for (const [name, step] of Object.entries(implementations)) {
88
+ if (!components[name]) continue;
89
+ current = name;
90
+ report.results[name] = { success: false, status: 'running' };
91
+ save();
92
+ const labels = {
93
+ database: 'restoringDatabase', edgeFunctions: 'restoringEdgeFunctions',
94
+ authSettings: 'restoringAuthSettings', storage: 'restoringStorageBuckets',
95
+ databaseSettings: 'restoringDatabaseSettings', realtimeSettings: 'restoringRealtimeSettings',
96
+ };
97
+ console.log((global.smoonbI18n?.t || t)(`restore.index.${labels[name]}`));
98
+ const result = await step(context);
99
+ if (!result?.success || !result?.verified)
100
+ throw new Error(`Component ${name} did not pass verification`);
101
+ report.results[name] = { ...result, status: 'verified' };
102
+ save();
103
+ }
104
+ report.status = 'verified';
105
+ report.completed_at = new Date().toISOString();
106
+ save();
107
+ return report.results;
108
+ } catch (error) {
109
+ report.status = 'failed';
110
+ report.failed_component = current;
111
+ report.results[current] = { success: false, status: 'failed' };
112
+ report.completed_at = new Date().toISOString();
113
+ save();
114
+ throw error;
115
+ }
116
+ }
117
+ module.exports = { runRestore };
@@ -1,84 +1,18 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const { execSync } = require('child_process');
4
- const { t } = require('../../../i18n');
5
- const ui = require('../../../utils/cliUi');
6
-
7
- /**
8
- * Etapa 3: Restaurar Database via psql
9
- */
10
- module.exports = async ({ backupFilePath, targetDatabaseUrl }) => {
11
- try {
12
- const getT = global.smoonbI18n?.t || t;
13
- const backupDirAbs = path.resolve(path.dirname(backupFilePath));
14
- const fileName = path.basename(backupFilePath);
15
- let uncompressedFile = fileName;
16
-
17
- // Verificar se é arquivo .backup.gz (compactado) ou .backup (descompactado)
18
- if (fileName.endsWith('.backup.gz')) {
19
- console.log(chalk.white(` - ${getT('restore.steps.database.detectedGz')}`));
20
- console.log(chalk.white(` - ${getT('restore.steps.database.extractingGz')}`));
21
-
22
- const unzipCmd = [
23
- 'docker run --rm',
24
- `-v "${backupDirAbs}:/host"`,
25
- 'postgres:17 gunzip /host/' + fileName
26
- ].join(' ');
27
-
28
- execSync(unzipCmd, { stdio: 'pipe' });
29
- uncompressedFile = fileName.replace('.gz', '');
30
- console.log(chalk.white(` - ${getT('restore.steps.database.uncompressed', { file: uncompressedFile })}`));
31
- } else if (fileName.endsWith('.backup')) {
32
- console.log(chalk.white(` - ${getT('restore.steps.database.detectedBackup')}`));
33
- console.log(chalk.white(` - ${getT('restore.steps.database.proceeding')}`));
34
- } else {
35
- throw new Error(`Formato de arquivo inválido. Esperado .backup.gz ou .backup, recebido: ${fileName}`);
36
- }
37
-
38
- // Extrair credenciais da URL de conexão
39
- const urlMatch = targetDatabaseUrl.match(/postgresql:\/\/([^@:]+):([^@]+)@(.+)$/);
40
-
41
- if (!urlMatch) {
42
- const getT = global.smoonbI18n?.t || t;
43
- throw new Error(getT('error.databaseUrlInvalid'));
44
- }
45
-
46
- // Comando psql conforme documentação oficial Supabase
47
- const restoreCmd = [
48
- 'docker run --rm --network host',
49
- `-v "${backupDirAbs}:/host"`,
50
- `-e PGPASSWORD="${encodeURIComponent(urlMatch[2])}"`,
51
- 'postgres:17 psql',
52
- `-d "${targetDatabaseUrl}"`,
53
- `-f /host/${uncompressedFile}`
54
- ].join(' ');
55
-
56
- console.log(chalk.cyan(` - ${getT('restore.steps.database.executing')}`));
57
- console.log(chalk.cyan(` ℹ️ ${getT('restore.steps.database.followingDocs')}`));
58
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warning')}`));
59
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warningReason1')}`));
60
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.warningReason2')}`));
61
-
62
- // Executar comando de restauração
63
- execSync(restoreCmd, { stdio: 'inherit', encoding: 'utf8' });
64
-
65
- console.log(chalk.green(` ✅ ${getT('restore.steps.database.success')}`));
66
- ui.hint(` ℹ️ ${getT('restore.steps.database.normalErrors')}`);
67
-
68
- } catch (error) {
69
- // Erros esperados conforme documentação oficial Supabase
70
- const getT = global.smoonbI18n?.t || t;
71
- if (error.message.includes('already exists') ||
72
- error.message.includes('constraint') ||
73
- error.message.includes('duplicate') ||
74
- error.stdout?.includes('already exists')) {
75
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.database.expectedErrors')}`));
76
- console.log(chalk.green(` ✅ ${getT('restore.steps.database.success')}`));
77
- ui.hint(` ℹ️ ${getT('restore.steps.database.errorsIgnored')}`);
78
- } else {
79
- console.error(chalk.red(` ❌ ${getT('restore.steps.database.error', { message: error.message })}`));
80
- throw error;
81
- }
82
- }
83
- };
84
-
1
+ const { readDump, buildRestore } = require('../dump');
2
+ const { runSql } = require('../pg');
3
+ module.exports = async ({
4
+ backupFilePath,
5
+ targetDatabaseUrl,
6
+ sourceProjectId,
7
+ targetProjectId,
8
+ remapUrls = true,
9
+ postgresMajor = 17,
10
+ }) => {
11
+ const plan = buildRestore(readDump(backupFilePath), {
12
+ sourceProjectId,
13
+ targetProjectId,
14
+ remapUrls,
15
+ });
16
+ await runSql(targetDatabaseUrl, plan.sql, { postgresMajor });
17
+ return plan.summary;
18
+ };
@@ -1,122 +1,95 @@
1
- const chalk = require('chalk');
2
- const path = require('path');
3
- const fs = require('fs').promises;
4
- const { execSync } = require('child_process');
5
- const { copyDirectoryRecursive } = require('../utils');
6
- const { t } = require('../../../i18n');
7
-
8
- /**
9
- * Etapa 4: Restaurar Edge Functions via supabase functions deploy
10
- */
11
- module.exports = async ({ backupPath, targetProject }) => {
12
- try {
13
- const getT = global.smoonbI18n?.t || t;
14
- const edgeFunctionsDir = path.join(backupPath, 'edge-functions');
15
-
16
- if (!await fs.access(edgeFunctionsDir).then(() => true).catch(() => false)) {
17
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.edgeFunctions.notFound')}`));
18
- return { success: false, functions_count: 0, success_count: 0 };
19
- }
20
-
21
- const items = await fs.readdir(edgeFunctionsDir);
22
- const functions = [];
23
-
24
- for (const item of items) {
25
- const itemPath = path.join(edgeFunctionsDir, item);
26
- const stats = await fs.stat(itemPath);
27
- if (stats.isDirectory()) {
28
- functions.push(item);
29
- }
30
- }
31
-
32
- if (functions.length === 0) {
33
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.edgeFunctions.notFound')}`));
34
- return { success: false, functions_count: 0, success_count: 0 };
35
- }
36
-
37
- console.log(chalk.white(` - ${getT('restore.steps.edgeFunctions.found', { count: functions.length })}`));
38
-
39
- // COPIAR Edge Functions de backups/backup-XXX/edge-functions para supabase/functions
40
- const supabaseFunctionsDir = path.join(process.cwd(), 'supabase', 'functions');
41
-
42
- // Criar diretório supabase/functions se não existir
43
- await fs.mkdir(supabaseFunctionsDir, { recursive: true });
44
-
45
- // Limpar supabase/functions antes de copiar (necessário para garantir ambiente limpo)
46
- console.log(chalk.cyan(` - ${getT('restore.steps.edgeFunctions.cleaningBefore')}`));
47
- try {
48
- await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
49
- await fs.mkdir(supabaseFunctionsDir, { recursive: true });
50
- } catch {
51
- // Ignorar erro de limpeza se não existir
52
- }
53
-
54
- // Copiar cada Edge Function para supabase/functions
55
- for (const funcName of functions) {
56
- const backupFuncPath = path.join(edgeFunctionsDir, funcName);
57
- const targetFuncPath = path.join(supabaseFunctionsDir, funcName);
58
-
59
- console.log(chalk.white(` - ${getT('restore.steps.edgeFunctions.copying', { funcName })}`));
60
-
61
- // Copiar recursivamente
62
- await copyDirectoryRecursive(backupFuncPath, targetFuncPath);
63
- }
64
-
65
- console.log(chalk.white(` - ${getT('restore.steps.edgeFunctions.linking', { projectId: targetProject.targetProjectId })}`));
66
-
67
- // Linkar com o projeto destino
68
- try {
69
- execSync(`supabase link --project-ref ${targetProject.targetProjectId}`, {
70
- stdio: 'pipe',
71
- encoding: 'utf8',
72
- timeout: 10000,
73
- env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
74
- });
75
- } catch {
76
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.edgeFunctions.linkMayExist')}`));
77
- }
78
-
79
- // Deploy das Edge Functions
80
- let successCount = 0;
81
- for (const funcName of functions) {
82
- console.log(chalk.white(` - ${getT('restore.steps.edgeFunctions.deploying', { funcName })}`));
83
-
84
- try {
85
- execSync(`supabase functions deploy ${funcName}`, {
86
- cwd: process.cwd(),
87
- stdio: 'pipe',
88
- encoding: 'utf8',
89
- timeout: 120000,
90
- env: { ...process.env, SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken || '' }
91
- });
92
-
93
- console.log(chalk.green(` ✅ ${getT('restore.steps.edgeFunctions.deployed', { funcName })}`));
94
- successCount++;
95
- } catch (deployError) {
96
- console.log(chalk.yellow(` ⚠️ ${getT('restore.steps.edgeFunctions.deployFailed', { funcName, message: deployError.message })}`));
97
- }
98
- }
99
-
100
- // Limpar supabase/functions após deploy (arquivos temporários não são mais necessários)
101
- console.log(chalk.cyan(` - ${getT('restore.steps.edgeFunctions.cleaningAfter')}`));
102
- try {
103
- await fs.rm(supabaseFunctionsDir, { recursive: true, force: true });
104
- } catch {
105
- // Ignorar erro de limpeza
106
- }
107
-
108
- console.log(chalk.green(` ✅ ${getT('restore.steps.edgeFunctions.success')}`));
109
-
110
- return {
111
- success: true,
112
- functions_count: functions.length,
113
- success_count: successCount
114
- };
115
-
116
- } catch (error) {
117
- const getT = global.smoonbI18n?.t || t;
118
- console.error(chalk.red(` ❌ ${getT('restore.steps.edgeFunctions.error', { message: error.message })}`));
119
- throw error;
120
- }
121
- };
122
-
1
+ const fs = require('fs').promises;
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { runSupabase } = require('../../../utils/supabaseCli');
5
+ const { management } = require('../management');
6
+ const { copyDirectoryRecursive } = require('../utils');
7
+ module.exports = async (
8
+ { backupPath, targetProject },
9
+ { deploy = runSupabase, request = management } = {},
10
+ ) => {
11
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'smoonb-functions-'));
12
+ try {
13
+ const source = path.join(backupPath, 'edge-functions');
14
+ const names = (await fs.readdir(source, { withFileTypes: true }))
15
+ .filter((e) => e.isDirectory() && e.name !== '_shared')
16
+ .map((e) => e.name);
17
+ if (!names.length)
18
+ return {
19
+ success: true,
20
+ verified: true,
21
+ functions_count: 0,
22
+ success_count: 0,
23
+ };
24
+ if (names.some((n) => !/^[-a-zA-Z0-9_]+$/.test(n)))
25
+ throw new Error('Invalid function slug in backup');
26
+ await copyDirectoryRecursive(
27
+ source,
28
+ path.join(root, 'supabase', 'functions'),
29
+ );
30
+ const manifest = JSON.parse(
31
+ await fs.readFile(path.join(backupPath, 'backup-manifest.json'), 'utf8'),
32
+ );
33
+ const metadata = manifest.components?.edge_functions?.functions || [];
34
+ const settings = names.map((name) => ({
35
+ name,
36
+ verify_jwt:
37
+ metadata.find((f) => (f.slug || f.name) === name)?.verify_jwt ?? true,
38
+ }));
39
+ await fs.writeFile(
40
+ path.join(root, 'supabase', 'config.toml'),
41
+ `project_id = "smoonb-restore"\n` +
42
+ settings
43
+ .map((f) => `[functions.${f.name}]\nverify_jwt = ${f.verify_jwt}\n`)
44
+ .join('\n'),
45
+ );
46
+ for (const f of settings) {
47
+ const args = [
48
+ 'functions',
49
+ 'deploy',
50
+ f.name,
51
+ '--project-ref',
52
+ targetProject.targetProjectId,
53
+ '--workdir',
54
+ root,
55
+ '--use-api',
56
+ ];
57
+ if (!f.verify_jwt) args.push('--no-verify-jwt');
58
+ try {
59
+ deploy('supabase', args, {
60
+ cwd: root,
61
+ env: {
62
+ ...process.env,
63
+ SUPABASE_ACCESS_TOKEN: targetProject.targetAccessToken,
64
+ },
65
+ stdio: 'pipe',
66
+ timeout: 180000,
67
+ windowsHide: true,
68
+ });
69
+ } catch {
70
+ throw new Error(`Edge Function deployment failed: ${f.name}`);
71
+ }
72
+ }
73
+ const remote = await request(targetProject, 'functions');
74
+ for (const f of settings)
75
+ if (
76
+ !remote.some(
77
+ (r) =>
78
+ r.slug === f.name &&
79
+ r.status === 'ACTIVE' &&
80
+ r.verify_jwt === f.verify_jwt,
81
+ )
82
+ )
83
+ throw new Error(`Edge Function verification failed: ${f.name}`);
84
+ return {
85
+ success: true,
86
+ verified: true,
87
+ functions_count: names.length,
88
+ success_count: names.length,
89
+ local_sources_preserved: true,
90
+ };
91
+ } finally {
92
+ // Only the directory freshly allocated by mkdtemp is removed.
93
+ await fs.rm(root, { recursive: true, force: true });
94
+ }
95
+ };