From 91d066660a4cc2436975ea494698bc378a02dde0 Mon Sep 17 00:00:00 2001 From: Robert Markovski <5818108+Roshan931@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:09:55 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E5=9C=A8=E4=BD=BF=E7=94=A8docker?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E7=9A=84=E6=97=B6=E5=80=99=E8=BF=90=E8=A1=8C?= =?UTF-8?q?bun=20run=20setup:env=E6=8A=A5=E9=94=99=C3=AF...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/scripts/src/backend.ts | 126 +++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 25 deletions(-) diff --git a/packages/scripts/src/backend.ts b/packages/scripts/src/backend.ts index 6125f5f6bf..90527709e3 100644 --- a/packages/scripts/src/backend.ts +++ b/packages/scripts/src/backend.ts @@ -1,9 +1,9 @@ import chalk from 'chalk'; import { spawn } from 'node:child_process'; +import type { EventEmitter } from 'node:events'; import fs from 'node:fs'; import path from 'node:path'; -import ora, { type Ora } from 'ora'; -import { z } from 'zod'; +import ora, { type Ora } from 'ora';import { z } from 'zod'; import { writeEnvFile } from './helpers'; /** @@ -332,38 +332,114 @@ const createProcessHandlers = ( return { onData, onClose, onError }; }; +const BACKEND_START_INACTIVITY_TIMEOUT_MS = 300_000; +const OUTPUT_TAIL_LIMIT = 2_000; +const PROGRESS_LINE_LIMIT = 100; + +export interface MonitoredProcess { + stdout: EventEmitter | null; + stderr: EventEmitter | null; + on(event: 'close', listener: (code: number | null) => void): unknown; + on(event: 'error', listener: (err: Error) => void): unknown; + kill(): unknown; +} + +export interface BackendStartOptions { + inactivityTimeoutMs: number; + onOutput?: (chunk: string) => void; +} + +const lastNonEmptyLine = (chunk: string): string | undefined => + chunk + .split(/[\r\n]+/) + .map((line) => line.trim()) + .filter((line) => line.length > 0) + .pop(); + +export const waitForBackendStart = ( + proc: MonitoredProcess, + { inactivityTimeoutMs, onOutput }: BackendStartOptions, +): Promise => + new Promise((resolve, reject) => { + let settled = false; + let timeout: NodeJS.Timeout | undefined; + + const settle = (finish: () => void) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + finish(); + }; + + const armTimeout = () => { + timeout = setTimeout(() => { + settle(() => { + proc.kill(); + reject( + new Error( + `Supabase produced no output for ${Math.round(inactivityTimeoutMs / 1000)}s while starting.`, + ), + ); + }); + }, inactivityTimeoutMs); + }; + + const onData = (chunk: Buffer | string) => { + if (settled) return; + clearTimeout(timeout); + armTimeout(); + onOutput?.(chunk.toString()); + }; + + proc.stdout?.on('data', onData); + proc.stderr?.on('data', onData); + + proc.on('close', (code) => + settle(() => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`Supabase start failed with exit code ${code ?? 'unknown'}.`)); + } + }), + ); + + proc.on('error', (err) => settle(() => reject(err))); + + armTimeout(); + }); + const startBackendAndExtractKeys = async (): Promise => { console.log(chalk.yellow('🚀 Starting Supabase backend...')); const spinner = ora('Waiting for Supabase to initialize...').start(); - const startProc = spawn('bun run', ['backend:start'], { cwd: rootDir, shell: true }); - - await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - startProc.kill(); - spinner.fail('Timed out waiting for Supabase keys.'); - reject(new Error('Supabase start timeout')); - }, 120_000); + const startProc = spawn('bun', ['run', 'backend:start'], { + cwd: rootDir, + stdio: ['ignore', 'pipe', 'pipe'], + }); - startProc.on('close', (code) => { - clearTimeout(timeout); - if (code === 0) { - resolve(); - } else { - spinner.fail('Failed to start Supabase backend.'); - reject(new Error('Supabase start failed')); - } - }); + let outputTail = ''; - startProc.on('error', (err) => { - clearTimeout(timeout); - spinner.fail(`Backend error: ${err.message}`); - reject(err); + try { + await waitForBackendStart(startProc, { + inactivityTimeoutMs: BACKEND_START_INACTIVITY_TIMEOUT_MS, + onOutput: (chunk) => { + outputTail = (outputTail + chunk).slice(-OUTPUT_TAIL_LIMIT); + const progress = lastNonEmptyLine(chunk); + if (progress) { + spinner.text = `Waiting for Supabase to initialize... ${progress.slice(0, PROGRESS_LINE_LIMIT)}`; + } + }, }); - }); + } catch (error) { + spinner.fail((error as Error).message); + if (outputTail.trim()) { + console.error(chalk.gray(outputTail.trim())); + } + throw error; + } spinner.succeed('Supabase backend started.'); - // Now get all keys from status const keysSpinner = ora('Extracting Supabase keys...').start(); const backendDir = path.join(rootDir, 'apps', 'backend'); From f2f1510064a2ce9b2d5b86c528727b73d99c15d6 Mon Sep 17 00:00:00 2001 From: Roshan931 Date: Mon, 24 Aug 2026 12:44:42 +0200 Subject: [PATCH 2/4] fix: address review feedback --- packages/scripts/src/backend.ts | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/scripts/src/backend.ts b/packages/scripts/src/backend.ts index 90527709e3..bcf5167da1 100644 --- a/packages/scripts/src/backend.ts +++ b/packages/scripts/src/backend.ts @@ -3,7 +3,8 @@ import { spawn } from 'node:child_process'; import type { EventEmitter } from 'node:events'; import fs from 'node:fs'; import path from 'node:path'; -import ora, { type Ora } from 'ora';import { z } from 'zod'; +import ora, { type Ora } from 'ora'; +import { z } from 'zod'; import { writeEnvFile } from './helpers'; /** @@ -349,6 +350,28 @@ export interface BackendStartOptions { onOutput?: (chunk: string) => void; } +const REDACTED = '[redacted]'; + +const SENSITIVE_LABEL_PATTERN = + /\b((?:jwt|anon|service[_ ]role|publishable|secret|access|s3)[a-z_ ]*(?:key|secret|token|password)[a-z_ ]*)(\s*[:=]\s*)(\S+)/gi; + +const SENSITIVE_TOKEN_PATTERNS: RegExp[] = [ + /\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}/g, + /\bsb_[a-z]+_[A-Za-z0-9_-]{8,}/g, +]; + +const redactSensitiveOutput = (text: string): string => { + const withoutLabelledValues = text.replace( + SENSITIVE_LABEL_PATTERN, + (_match, label: string, separator: string) => `${label}${separator}${REDACTED}`, + ); + + return SENSITIVE_TOKEN_PATTERNS.reduce( + (redacted, pattern) => redacted.replace(pattern, REDACTED), + withoutLabelledValues, + ); +}; + const lastNonEmptyLine = (chunk: string): string | undefined => chunk .split(/[\r\n]+/) @@ -425,7 +448,7 @@ const startBackendAndExtractKeys = async (): Promise => { inactivityTimeoutMs: BACKEND_START_INACTIVITY_TIMEOUT_MS, onOutput: (chunk) => { outputTail = (outputTail + chunk).slice(-OUTPUT_TAIL_LIMIT); - const progress = lastNonEmptyLine(chunk); + const progress = lastNonEmptyLine(redactSensitiveOutput(chunk)); if (progress) { spinner.text = `Waiting for Supabase to initialize... ${progress.slice(0, PROGRESS_LINE_LIMIT)}`; } @@ -433,8 +456,9 @@ const startBackendAndExtractKeys = async (): Promise => { }); } catch (error) { spinner.fail((error as Error).message); - if (outputTail.trim()) { - console.error(chalk.gray(outputTail.trim())); + const redactedTail = redactSensitiveOutput(outputTail).trim(); + if (redactedTail) { + console.error(chalk.gray(redactedTail)); } throw error; } From c054534cf6322c551e2e066d755bcff6bfde1125 Mon Sep 17 00:00:00 2001 From: Roshan931 Date: Tue, 25 Aug 2026 09:58:40 +0200 Subject: [PATCH 3/4] fix: address review feedback --- packages/scripts/src/backend.ts | 29 ++++++++++++++++-- packages/scripts/test/comprehensive.test.ts | 33 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/packages/scripts/src/backend.ts b/packages/scripts/src/backend.ts index bcf5167da1..6cd0d0283f 100644 --- a/packages/scripts/src/backend.ts +++ b/packages/scripts/src/backend.ts @@ -336,6 +336,7 @@ const createProcessHandlers = ( const BACKEND_START_INACTIVITY_TIMEOUT_MS = 300_000; const OUTPUT_TAIL_LIMIT = 2_000; const PROGRESS_LINE_LIMIT = 100; +const PROGRESS_CARRY_LIMIT = 4_000; export interface MonitoredProcess { stdout: EventEmitter | null; @@ -379,6 +380,29 @@ const lastNonEmptyLine = (chunk: string): string | undefined => .filter((line) => line.length > 0) .pop(); +/** + * Builds a reporter that keeps incomplete lines in a carry buffer so redaction + * always runs on whole lines + * @returns Function returning the redacted progress line for a chunk, if any + */ +export const createProgressReporter = (): ((chunk: string) => string | undefined) => { + let carry = ''; + + return (chunk: string): string | undefined => { + const combined = carry + chunk; + const lastBreak = Math.max(combined.lastIndexOf('\n'), combined.lastIndexOf('\r')); + + if (lastBreak === -1) { + carry = combined.slice(0, PROGRESS_CARRY_LIMIT); + return undefined; + } + + carry = combined.slice(lastBreak + 1, lastBreak + 1 + PROGRESS_CARRY_LIMIT); + const progress = lastNonEmptyLine(redactSensitiveOutput(combined.slice(0, lastBreak))); + return progress?.slice(0, PROGRESS_LINE_LIMIT); + }; +}; + export const waitForBackendStart = ( proc: MonitoredProcess, { inactivityTimeoutMs, onOutput }: BackendStartOptions, @@ -442,15 +466,16 @@ const startBackendAndExtractKeys = async (): Promise => { }); let outputTail = ''; + const reportProgress = createProgressReporter(); try { await waitForBackendStart(startProc, { inactivityTimeoutMs: BACKEND_START_INACTIVITY_TIMEOUT_MS, onOutput: (chunk) => { outputTail = (outputTail + chunk).slice(-OUTPUT_TAIL_LIMIT); - const progress = lastNonEmptyLine(redactSensitiveOutput(chunk)); + const progress = reportProgress(chunk); if (progress) { - spinner.text = `Waiting for Supabase to initialize... ${progress.slice(0, PROGRESS_LINE_LIMIT)}`; + spinner.text = `Waiting for Supabase to initialize... ${progress}`; } }, }); diff --git a/packages/scripts/test/comprehensive.test.ts b/packages/scripts/test/comprehensive.test.ts index 4daeacfe9e..3871684576 100644 --- a/packages/scripts/test/comprehensive.test.ts +++ b/packages/scripts/test/comprehensive.test.ts @@ -3,7 +3,12 @@ import fs from 'node:fs'; import path from 'node:path'; // Import actual functions to test -import { getDbEnvContent, generateBackendEnvContent, CLIENT_BACKEND_KEYS } from '../src/backend'; +import { + getDbEnvContent, + generateBackendEnvContent, + CLIENT_BACKEND_KEYS, + createProgressReporter, +} from '../src/backend'; import { parseEnvContent, buildEnvFileContent, writeEnvFile } from '../src/helpers'; describe('comprehensive functionality tests', () => { @@ -397,6 +402,32 @@ Supabase local development setup completed. }); }); + describe('Startup progress redaction', () => { + it('should redact a labeled secret split across chunks', () => { + const reportProgress = createProgressReporter(); + + expect(reportProgress('SERVICE_ROLE_KEY=')).toBeUndefined(); + expect(reportProgress('super-secret-value\n')).toBe('SERVICE_ROLE_KEY=[redacted]'); + }); + + it('should redact a JWT split across chunks', () => { + const reportProgress = createProgressReporter(); + + expect(reportProgress('starting...\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJz')).toBe( + 'starting...', + ); + expect(reportProgress('dWIiOiIxMjM0NTY3ODkwIn0.signature\n')).toBe('[redacted]'); + }); + + it('should report plain progress lines unchanged', () => { + const reportProgress = createProgressReporter(); + + expect(reportProgress('Starting containers...\n')).toBe('Starting containers...'); + expect(reportProgress('Seeding data')).toBeUndefined(); + expect(reportProgress('...done\r')).toBe('Seeding data...done'); + }); + }); + describe('Real function tests with mixed environment variables', () => { it('should parse mixed environment variables correctly using actual parseEnvContent', () => { const complexEnvContent = `# App configuration From 3522f9681729035d123c02c84e6b80b31ff95a97 Mon Sep 17 00:00:00 2001 From: Roshan931 Date: Tue, 25 Aug 2026 10:11:22 +0200 Subject: [PATCH 4/4] fix: address review feedback --- packages/scripts/src/backend.ts | 30 +++++++++++++++------ packages/scripts/test/comprehensive.test.ts | 13 +++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/scripts/src/backend.ts b/packages/scripts/src/backend.ts index 6cd0d0283f..4413e048ef 100644 --- a/packages/scripts/src/backend.ts +++ b/packages/scripts/src/backend.ts @@ -346,9 +346,11 @@ export interface MonitoredProcess { kill(): unknown; } +export type OutputStream = 'stdout' | 'stderr'; + export interface BackendStartOptions { inactivityTimeoutMs: number; - onOutput?: (chunk: string) => void; + onOutput?: (chunk: string, stream: OutputStream) => void; } const REDACTED = '[redacted]'; @@ -403,6 +405,18 @@ export const createProgressReporter = (): ((chunk: string) => string | undefined }; }; +export const createStreamProgressReporter = (): (( + chunk: string, + stream: OutputStream, +) => string | undefined) => { + const reporters: Record string | undefined> = { + stdout: createProgressReporter(), + stderr: createProgressReporter(), + }; + + return (chunk: string, stream: OutputStream): string | undefined => reporters[stream](chunk); +}; + export const waitForBackendStart = ( proc: MonitoredProcess, { inactivityTimeoutMs, onOutput }: BackendStartOptions, @@ -431,15 +445,15 @@ export const waitForBackendStart = ( }, inactivityTimeoutMs); }; - const onData = (chunk: Buffer | string) => { + const onData = (stream: OutputStream) => (chunk: Buffer | string) => { if (settled) return; clearTimeout(timeout); armTimeout(); - onOutput?.(chunk.toString()); + onOutput?.(chunk.toString(), stream); }; - proc.stdout?.on('data', onData); - proc.stderr?.on('data', onData); + proc.stdout?.on('data', onData('stdout')); + proc.stderr?.on('data', onData('stderr')); proc.on('close', (code) => settle(() => { @@ -466,14 +480,14 @@ const startBackendAndExtractKeys = async (): Promise => { }); let outputTail = ''; - const reportProgress = createProgressReporter(); + const reportProgress = createStreamProgressReporter(); try { await waitForBackendStart(startProc, { inactivityTimeoutMs: BACKEND_START_INACTIVITY_TIMEOUT_MS, - onOutput: (chunk) => { + onOutput: (chunk, stream) => { outputTail = (outputTail + chunk).slice(-OUTPUT_TAIL_LIMIT); - const progress = reportProgress(chunk); + const progress = reportProgress(chunk, stream); if (progress) { spinner.text = `Waiting for Supabase to initialize... ${progress}`; } diff --git a/packages/scripts/test/comprehensive.test.ts b/packages/scripts/test/comprehensive.test.ts index 3871684576..4bbef05168 100644 --- a/packages/scripts/test/comprehensive.test.ts +++ b/packages/scripts/test/comprehensive.test.ts @@ -8,6 +8,7 @@ import { generateBackendEnvContent, CLIENT_BACKEND_KEYS, createProgressReporter, + createStreamProgressReporter, } from '../src/backend'; import { parseEnvContent, buildEnvFileContent, writeEnvFile } from '../src/helpers'; @@ -426,6 +427,18 @@ Supabase local development setup completed. expect(reportProgress('Seeding data')).toBeUndefined(); expect(reportProgress('...done\r')).toBe('Seeding data...done'); }); + + it('should keep stdout and stderr carry buffers separate', () => { + const reportProgress = createStreamProgressReporter(); + + expect(reportProgress('SERVICE_ROLE_KEY=', 'stdout')).toBeUndefined(); + expect(reportProgress('warning: docker is slow\n', 'stderr')).toBe( + 'warning: docker is slow', + ); + expect(reportProgress('super-secret-value\n', 'stdout')).toBe( + 'SERVICE_ROLE_KEY=[redacted]', + ); + }); }); describe('Real function tests with mixed environment variables', () => {