From 9e7a6e8bf73c7b010087ec3d170cce6752f5be0e Mon Sep 17 00:00:00 2001 From: t0g3pii aka Nico Date: Fri, 8 May 2026 15:57:07 +0200 Subject: [PATCH 1/5] fix: restore wa-automate SessionManager + windows rimraf globs Re-add the wa-automate SessionManager source (previously ignored via a broad .gitignore rule) and make node-red clean scripts work on Windows by enabling rimraf globbing. Co-authored-by: Cursor --- .gitignore | 5 +- integrations/node-red/package.json | 4 +- .../wa-automate/src/session/SessionManager.ts | 136 ++++++++++++++++++ 3 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 packages/wa-automate/src/session/SessionManager.ts diff --git a/.gitignore b/.gitignore index 5f7ff4805..6212fc37b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ node_modules dist -session -newsession +/session/ +/newsession/ **buttons.js **req.ts **qr_code_session.png @@ -69,6 +69,7 @@ packages/*/generated/ **/generated/*.js **/generated/*.d.ts **/generated/*.map +*.mdx # Compiled JavaScript files in source directories # packages/*/src/**/*.js diff --git a/integrations/node-red/package.json b/integrations/node-red/package.json index 59c7a265c..0a5a7292f 100644 --- a/integrations/node-red/package.json +++ b/integrations/node-red/package.json @@ -22,8 +22,8 @@ "build:runtime": "pnpm run clean:runtime && tsc -p tsconfig.runtime.json", "build:runtime:watch": "tsc -p tsconfig.runtime.watch.json --watch --preserveWatchOutput", "build": "rimraf dist && pnpm run copy && pnpm run build:editor && pnpm run build:runtime", - "clean:editor": "rimraf dist/*.html && rimraf dist/*.js", - "clean:runtime": "rimraf dist/*.js", + "clean:editor": "rimraf --glob \"dist/*.html\" \"dist/*.js\"", + "clean:runtime": "rimraf --glob \"dist/*.js\"", "test": "vitest run --passWithNoTests", "test:watch": "vitest --passWithNoTests", "dev": "rimraf dist && pnpm run copy && concurrently --names 'COPY,EDITOR,RUNTIME,TEST' --prefix '({name})' --prefix-colors 'yellow.bold,cyan.bold,greenBright.bold,magenta.bold' 'onchange -v \"src/**/*.png\" \"src/**/*.svg\" -- pnpm run copy' 'pnpm run build:editor:watch' 'pnpm run build:runtime:watch' 'sleep 10; pnpm run test:watch'", diff --git a/packages/wa-automate/src/session/SessionManager.ts b/packages/wa-automate/src/session/SessionManager.ts new file mode 100644 index 000000000..8a3732a73 --- /dev/null +++ b/packages/wa-automate/src/session/SessionManager.ts @@ -0,0 +1,136 @@ +import { createLogger } from '@open-wa/logger'; +import { LocalSessionCompression, S3SyncManager } from '@open-wa/session-sync'; +import type { S3Config } from '@open-wa/session-sync'; +import type { Config } from '@open-wa/config'; + +type SessionManagerConfig = { + sessionId: string; + dataDir: string; + s3Config?: S3Config; + syncInterval?: number; + compressionOptions?: string; + enableLocalCompression?: boolean; + enableS3Backup?: boolean; +}; + +export class SessionManager { + private config: SessionManagerConfig; + private logger = createLogger({ component: 'SessionManager' }); + private localCompression: LocalSessionCompression | null = null; + private s3Sync: S3SyncManager | null = null; + + constructor(config: SessionManagerConfig) { + this.config = config; + this.logger.info('SessionManager initialized', { sessionId: config.sessionId }); + } + + async start(): Promise { + if (this.config.enableLocalCompression !== false) { + this.localCompression = new LocalSessionCompression({ + sessionPath: this.config.dataDir, + sessionId: this.config.sessionId, + intervalMs: this.config.syncInterval || 600_000, + }); + await this.localCompression.start(); + this.logger.info('Local session compression started'); + } + + if (this.config.enableS3Backup && this.config.s3Config) { + this.s3Sync = new S3SyncManager(this.config.s3Config); + this.startPeriodicSync(); + this.logger.info('S3 session sync started'); + } + } + + async stop(): Promise { + if (this.localCompression) { + await this.localCompression.stop(); + this.localCompression = null; + } + + if (this.s3Sync) { + this.s3Sync = null; + } + + this.logger.info('SessionManager stopped'); + } + + async backupSession(): Promise { + if (!this.s3Sync) { + this.logger.warn('S3 sync not configured'); + return null; + } + + const sessionZstPath = `${this.config.dataDir}/${this.config.sessionId}.data.zst`; + try { + const filename = await this.s3Sync.backupSession(sessionZstPath); + this.logger.info('Session backed up to S3', { filename }); + return filename; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error('Failed to backup session to S3', { error: message }); + throw error; + } + } + + async restoreSession(filename: string): Promise { + if (!this.s3Sync) { + this.logger.warn('S3 sync not configured'); + throw new Error('S3 sync not configured for session restore'); + } + + try { + await this.s3Sync.restoreSession(filename, this.config.dataDir); + this.logger.info('Session restored from S3', { filename }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error('Failed to restore session from S3', { error: message }); + throw error; + } + } + + async getSessionBackupUrl(filename: string, expiresIn = 3600): Promise { + if (!this.s3Sync) { + this.logger.warn('S3 sync not configured'); + return null; + } + + try { + const url = await this.s3Sync.getDownloadUrl(filename, expiresIn); + this.logger.info('Generated session backup URL', { filename, expiresIn }); + return url; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error('Failed to generate backup URL', { error: message }); + throw error; + } + } + + private startPeriodicSync(): void { + if (!this.s3Sync) return; + + const syncInterval = this.config.syncInterval || 600_000; + setInterval(async () => { + try { + await this.backupSession(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error('Periodic sync failed', { error: message }); + } + }, syncInterval); + } + + static createFromConfig(clientConfig: Config): SessionManager { + const s3Config = clientConfig.s3Sync as unknown as S3Config | undefined; + return new SessionManager({ + sessionId: clientConfig.sessionId || 'session', + dataDir: clientConfig.sessionDataPath || './.wwebjs', + s3Config, + syncInterval: clientConfig.s3Sync?.syncInterval, + compressionOptions: '-1 -T0', + enableLocalCompression: clientConfig.s3Sync?.enableLocalCompression !== false, + enableS3Backup: !!s3Config, + }); + } +} + From 61fa3b372c309ac5997d551b0b08adad3f09bf13 Mon Sep 17 00:00:00 2001 From: t0g3pii aka Nico Date: Fri, 8 May 2026 16:11:25 +0200 Subject: [PATCH 2/5] fix: validate s3Sync config in wa-automate SessionManager Parse and validate s3Sync into an S3Config only when required credentials are present; enableS3Backup is now gated on validated config. Co-authored-by: Cursor --- .../wa-automate/src/session/SessionManager.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/wa-automate/src/session/SessionManager.ts b/packages/wa-automate/src/session/SessionManager.ts index 8a3732a73..463036fa6 100644 --- a/packages/wa-automate/src/session/SessionManager.ts +++ b/packages/wa-automate/src/session/SessionManager.ts @@ -120,16 +120,44 @@ export class SessionManager { }, syncInterval); } + private static parseS3Config(input: unknown): S3Config | null { + if (!input || typeof input !== 'object') return null; + + const obj = input as Record; + const bucket = obj.bucket; + const region = obj.region; + const accessKeyId = obj.accessKeyId; + const secretAccessKey = obj.secretAccessKey; + + const hasRequired = + typeof bucket === 'string' && + bucket.length > 0 && + typeof region === 'string' && + region.length > 0 && + typeof accessKeyId === 'string' && + accessKeyId.length > 0 && + typeof secretAccessKey === 'string' && + secretAccessKey.length > 0; + + if (!hasRequired) return null; + + const endpoint = typeof obj.endpoint === 'string' ? obj.endpoint : undefined; + const host = typeof obj.host === 'string' ? obj.host : undefined; + const url = typeof obj.url === 'string' ? obj.url : undefined; + + return { bucket, region, accessKeyId, secretAccessKey, endpoint, host, url }; + } + static createFromConfig(clientConfig: Config): SessionManager { - const s3Config = clientConfig.s3Sync as unknown as S3Config | undefined; + const validatedS3Config = SessionManager.parseS3Config(clientConfig.s3Sync); return new SessionManager({ sessionId: clientConfig.sessionId || 'session', dataDir: clientConfig.sessionDataPath || './.wwebjs', - s3Config, + s3Config: validatedS3Config ?? undefined, syncInterval: clientConfig.s3Sync?.syncInterval, compressionOptions: '-1 -T0', enableLocalCompression: clientConfig.s3Sync?.enableLocalCompression !== false, - enableS3Backup: !!s3Config, + enableS3Backup: validatedS3Config !== null, }); } } From a3172df581d7cc58e2ac70d003501453ab672e09 Mon Sep 17 00:00:00 2001 From: t0g3pii aka Nico Date: Fri, 8 May 2026 16:13:12 +0200 Subject: [PATCH 3/5] fix: prevent S3 sync timer leaks and overlap Store the periodic S3 sync timer handle so stop() can clear it, and avoid overlapping backupSession calls by scheduling via setTimeout with an in-flight guard. Also drop unused compressionOptions config field. Co-authored-by: Cursor --- .../wa-automate/src/session/SessionManager.ts | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/wa-automate/src/session/SessionManager.ts b/packages/wa-automate/src/session/SessionManager.ts index 463036fa6..1205fb596 100644 --- a/packages/wa-automate/src/session/SessionManager.ts +++ b/packages/wa-automate/src/session/SessionManager.ts @@ -8,7 +8,6 @@ type SessionManagerConfig = { dataDir: string; s3Config?: S3Config; syncInterval?: number; - compressionOptions?: string; enableLocalCompression?: boolean; enableS3Backup?: boolean; }; @@ -18,6 +17,8 @@ export class SessionManager { private logger = createLogger({ component: 'SessionManager' }); private localCompression: LocalSessionCompression | null = null; private s3Sync: S3SyncManager | null = null; + private syncTimer: NodeJS.Timeout | null = null; + private syncInFlight = false; constructor(config: SessionManagerConfig) { this.config = config; @@ -48,6 +49,12 @@ export class SessionManager { this.localCompression = null; } + if (this.syncTimer) { + clearTimeout(this.syncTimer); + this.syncTimer = null; + } + this.syncInFlight = false; + if (this.s3Sync) { this.s3Sync = null; } @@ -110,14 +117,33 @@ export class SessionManager { if (!this.s3Sync) return; const syncInterval = this.config.syncInterval || 600_000; - setInterval(async () => { - try { - await this.backupSession(); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - this.logger.error('Periodic sync failed', { error: message }); + + // Defensive: stop any previous schedule before starting a new one + if (this.syncTimer) { + clearTimeout(this.syncTimer); + this.syncTimer = null; + } + + const tick = async () => { + if (!this.s3Sync) return; + + if (!this.syncInFlight) { + this.syncInFlight = true; + try { + await this.backupSession(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error('Periodic sync failed', { error: message }); + } finally { + this.syncInFlight = false; + } } - }, syncInterval); + + if (!this.s3Sync) return; + this.syncTimer = setTimeout(() => void tick(), syncInterval); + }; + + this.syncTimer = setTimeout(() => void tick(), syncInterval); } private static parseS3Config(input: unknown): S3Config | null { @@ -155,7 +181,6 @@ export class SessionManager { dataDir: clientConfig.sessionDataPath || './.wwebjs', s3Config: validatedS3Config ?? undefined, syncInterval: clientConfig.s3Sync?.syncInterval, - compressionOptions: '-1 -T0', enableLocalCompression: clientConfig.s3Sync?.enableLocalCompression !== false, enableS3Backup: validatedS3Config !== null, }); From 553988a2ec87be2a1007eeb0b0bd7a287daa8dc5 Mon Sep 17 00:00:00 2001 From: t0g3pii aka Nico Date: Fri, 8 May 2026 16:16:45 +0200 Subject: [PATCH 4/5] fix: make turbo filter scripts cross-platform Replace single-quoted turbo filters (breaks on Windows) and remove filters for non-existent legacy packages; keep docs excluded via filter on the actual docs workspace package. Co-authored-by: Cursor --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ed8d9335c..43dd55321 100644 --- a/package.json +++ b/package.json @@ -16,18 +16,18 @@ "sdks/*" ], "scripts": { - "build": "turbo build --filter='!@open-wa/legacy' --filter='!@open-wa/legacy-documented' --filter='!docs'", - "dev": "turbo dev --concurrency 100 --filter='!@open-wa/legacy' --filter='!@open-wa/legacy-documented'", - "test": "turbo test --filter='!@open-wa/legacy' --filter='!@open-wa/legacy-documented'", + "build": "turbo build --filter=\"!docs\"", + "dev": "turbo dev --concurrency 100 --filter=\"!docs\"", + "test": "turbo test --filter=\"!docs\"", "lint": "oxlint .", "lint:fix": "oxlint . --fix", "clean": "turbo clean && rimraf node_modules", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", - "typecheck": "turbo typecheck --filter='!@open-wa/legacy' --filter='!@open-wa/legacy-documented'", + "typecheck": "turbo typecheck --filter=\"!docs\"", "changeset": "changeset", "version-packages": "changeset version", "repomix": "npx repomix@latest", - "publish-packages": "turbo run build --filter='!@open-wa/legacy' --filter='!@open-wa/legacy-documented' && changeset publish", + "publish-packages": "turbo run build --filter=\"!docs\" && changeset publish", "release-image": "node tools/release-image.js" }, "devDependencies": { From 861e128db4f30e034e1511dd275685b61b2bc572 Mon Sep 17 00:00:00 2001 From: t0g3pii aka Nico Date: Fri, 8 May 2026 16:19:11 +0200 Subject: [PATCH 5/5] fix: harden SessionManager stop and S3 startup guard Make stop() best-effort so one failure doesn't prevent timer/S3 cleanup, and only enable S3 periodic uploads when local compression is enabled (since backups upload the .data.zst artifact). Co-authored-by: Cursor --- packages/wa-automate/src/session/SessionManager.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/wa-automate/src/session/SessionManager.ts b/packages/wa-automate/src/session/SessionManager.ts index 1205fb596..fd83a47be 100644 --- a/packages/wa-automate/src/session/SessionManager.ts +++ b/packages/wa-automate/src/session/SessionManager.ts @@ -36,7 +36,9 @@ export class SessionManager { this.logger.info('Local session compression started'); } - if (this.config.enableS3Backup && this.config.s3Config) { + // S3 backups upload the `.data.zst` artifact produced by LocalSessionCompression. + // If local compression is disabled, periodic S3 sync would target a non-existent file. + if (this.config.enableLocalCompression !== false && this.config.enableS3Backup && this.config.s3Config) { this.s3Sync = new S3SyncManager(this.config.s3Config); this.startPeriodicSync(); this.logger.info('S3 session sync started'); @@ -45,7 +47,12 @@ export class SessionManager { async stop(): Promise { if (this.localCompression) { - await this.localCompression.stop(); + try { + await this.localCompression.stop(); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.warn('Local session compression stop failed', { error: message }); + } this.localCompression = null; }