feat(queue): support REDIS_PASS for Redis AUTH
Wire optional REDIS_PASS into the BullMQ ioredis connection, redact credentials in startup logs, and document REDIS_PASS plus JFLOW_ROLE. Co-authored-by: Nasyarobby Putra <nasyarobby@gmail.com>
This commit is contained in:
@@ -65,9 +65,10 @@ Optional `script.meta.reads = "ctx"` documents expression hosts. `meta.input` /
|
||||
| `JFLOW_SECRETS_KEY` | `jflow-dev-secrets-key` (dev only) | Master key for named secrets. **Required in production**. Changing it makes existing secrets unreadable. 64 hex chars are used as a raw AES-256 key; any other string is derived with scrypt. |
|
||||
| `JFLOW_DB_PATH` | `packages/server/data/jerapah-flow.db` | SQLite file. |
|
||||
| `REDIS_URL` | `redis://127.0.0.1:6379` | Redis for BullMQ workflow queue. **Required** — the server will not start if Redis is unreachable. |
|
||||
| `REDIS_PASS` | — | Optional Redis AUTH password (sent via ioredis `password`). Prefer this over embedding credentials in `REDIS_URL` so logs stay clean. |
|
||||
| `JFLOW_QUEUE_NAME` | `jerapah-workflows` | BullMQ queue name. |
|
||||
| `JFLOW_WORKER_CONCURRENCY` | `5` | Max parallel workflow jobs per worker process. |
|
||||
| `JFLOW_ROLE` | `all` | `all` (API + cron producer + worker), `api` (HTTP/admin/cron enqueue only), or `worker` (consume queue only). |
|
||||
| `JFLOW_ROLE` | `all` | Which duties this process performs: `all` (HTTP/admin + cron producer + worker), `api` (HTTP/admin + cron enqueue only), or `worker` (consume queue only). Use separate processes in production when you want to scale workers independently. |
|
||||
| `JFLOW_LOG_LEVEL` | `debug` | Pino level |
|
||||
| `JFLOW_RETENTION_DAYS` | `30` | Run history prune |
|
||||
| `JFLOW_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev |
|
||||
@@ -81,8 +82,8 @@ Workflow runs are **queued** via BullMQ. HTTP and manual triggers return `202 {
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
# Redis must be reachable at REDIS_URL
|
||||
JFLOW_JWT_SECRET=... JFLOW_SECRETS_KEY=... REDIS_URL=redis://127.0.0.1:6379 NODE_ENV=production pnpm start
|
||||
# Redis must be reachable at REDIS_URL (set REDIS_PASS if Redis requires AUTH)
|
||||
JFLOW_JWT_SECRET=... JFLOW_SECRETS_KEY=... REDIS_URL=redis://127.0.0.1:6379 REDIS_PASS=... NODE_ENV=production pnpm start
|
||||
```
|
||||
|
||||
The server serves `packages/web/dist` when that folder exists.
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
closeRedis,
|
||||
createWorkflowQueue,
|
||||
createWorkflowWorker,
|
||||
getRedisUrl,
|
||||
getRedisUrlForLog,
|
||||
} from "./workflow-queue.js";
|
||||
|
||||
await migrate();
|
||||
@@ -52,13 +52,13 @@ const role = (process.env.JFLOW_ROLE || "all").toLowerCase();
|
||||
const runApi = role === "all" || role === "api";
|
||||
const runWorker = role === "all" || role === "worker";
|
||||
|
||||
log.info({ redis: getRedisUrl(), role }, "starting jerapah-flow");
|
||||
log.info({ redis: getRedisUrlForLog(), role }, "starting jerapah-flow");
|
||||
|
||||
const workflowQueue = createWorkflowQueue();
|
||||
try {
|
||||
await workflowQueue.waitUntilReady();
|
||||
} catch (err) {
|
||||
log.error({ err, redis: getRedisUrl() }, "failed to connect to Redis");
|
||||
log.error({ err, redis: getRedisUrlForLog() }, "failed to connect to Redis");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,28 @@ export function getRedisUrl() {
|
||||
return process.env.REDIS_URL || DEFAULT_REDIS_URL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional Redis AUTH password. Applied even when REDIS_URL has no embedded credentials.
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
export function getRedisPassword() {
|
||||
const pass = process.env.REDIS_PASS;
|
||||
if (typeof pass !== "string" || pass.length === 0) return undefined;
|
||||
return pass;
|
||||
}
|
||||
|
||||
/** Redact credentials for logs. */
|
||||
export function getRedisUrlForLog() {
|
||||
try {
|
||||
const url = new URL(getRedisUrl());
|
||||
if (url.password || getRedisPassword()) url.password = "***";
|
||||
if (url.username) url.username = url.username ? "***" : "";
|
||||
return url.toString();
|
||||
} catch {
|
||||
return getRedisUrl();
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueueName() {
|
||||
return process.env.JFLOW_QUEUE_NAME || DEFAULT_QUEUE_NAME;
|
||||
}
|
||||
@@ -29,10 +51,15 @@ export function getWorkerConcurrency() {
|
||||
*/
|
||||
export function getSharedConnection() {
|
||||
if (sharedConnection) return sharedConnection;
|
||||
sharedConnection = new IORedis(getRedisUrl(), {
|
||||
/** @type {import("ioredis").RedisOptions} */
|
||||
const options = {
|
||||
maxRetriesPerRequest: null,
|
||||
enableReadyCheck: true,
|
||||
});
|
||||
};
|
||||
const password = getRedisPassword();
|
||||
if (password) options.password = password;
|
||||
|
||||
sharedConnection = new IORedis(getRedisUrl(), options);
|
||||
sharedConnection.on("error", (err) => {
|
||||
log.error({ err }, "redis connection error");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user