837f0fa63f
Close deployment IDOR and gate stub payment endpoints, add production secret validation, health probes, Redis-backed build progress, GitHub Actions CI, expanded tests, billing/k8s refactors, and ops runbooks. Co-authored-by: Cursor <cursoragent@cursor.com>
59 lines
1.6 KiB
TypeScript
59 lines
1.6 KiB
TypeScript
import { Injectable, OnModuleDestroy } from '@nestjs/common';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import Redis from 'ioredis';
|
|
import type { BuildProgress } from './build.service';
|
|
|
|
const KEY_PREFIX = 'build:progress:';
|
|
const TTL_SECONDS = 3600;
|
|
|
|
@Injectable()
|
|
export class BuildProgressStore implements OnModuleDestroy {
|
|
private readonly redis: Redis;
|
|
|
|
constructor(private readonly configService: ConfigService) {
|
|
this.redis = new Redis({
|
|
host: this.configService.get<string>('redis.host'),
|
|
port: this.configService.get<number>('redis.port'),
|
|
lazyConnect: true,
|
|
maxRetriesPerRequest: 1,
|
|
});
|
|
this.redis.connect().catch(() => {
|
|
// Redis may be unavailable in local unit tests — in-memory fallback remains in BuildService.
|
|
});
|
|
}
|
|
|
|
async get(deploymentId: string): Promise<BuildProgress | null> {
|
|
try {
|
|
const raw = await this.redis.get(`${KEY_PREFIX}${deploymentId}`);
|
|
return raw ? (JSON.parse(raw) as BuildProgress) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async set(deploymentId: string, progress: BuildProgress): Promise<void> {
|
|
try {
|
|
await this.redis.set(
|
|
`${KEY_PREFIX}${deploymentId}`,
|
|
JSON.stringify(progress),
|
|
'EX',
|
|
TTL_SECONDS,
|
|
);
|
|
} catch {
|
|
// Best-effort — local map still holds progress for this replica.
|
|
}
|
|
}
|
|
|
|
async clear(deploymentId: string): Promise<void> {
|
|
try {
|
|
await this.redis.del(`${KEY_PREFIX}${deploymentId}`);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
onModuleDestroy(): void {
|
|
this.redis.disconnect();
|
|
}
|
|
}
|