refactor: rename the project to JerapahFlow (jerapah-flow)

Update package names, env vars, cookie, and default ntfy topics while keeping SCRUNNER_* fallbacks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-15 21:21:24 +07:00
co-authored by Cursor
parent 97bb8d7429
commit 4a78f66e2d
20 changed files with 53 additions and 42 deletions
+12 -10
View File
@@ -1,11 +1,13 @@
# scrunner # JerapahFlow
![JerapahFlow](packages/web/src/theme/brand/wordmark.png)
Workflow runner with a sandboxed script engine, SQLite run history, and an admin UI. Workflow runner with a sandboxed script engine, SQLite run history, and an admin UI.
## Packages ## Packages
- `packages/server` — Fastify runner, HTTP/cron triggers, admin REST API - `@jerapah-flow/server` (`packages/server`) — Fastify runner, HTTP/cron triggers, admin REST API
- `packages/web` — React admin UI (Vite, DaisyUI, React Query) - `@jerapah-flow/web` (`packages/web`) — React admin UI (Vite, DaisyUI, React Query)
## Setup ## Setup
@@ -34,12 +36,12 @@ The first account created becomes **admin**. Later accounts are created from Use
| Variable | Default | Notes | | Variable | Default | Notes |
|---|---|---| |---|---|---|
| `SCRUNNER_JWT_SECRET` | `scrunner-dev-secret` (dev only) | **Required in production** | | `JERAPAH_FLOW_JWT_SECRET` | `scrunner-dev-secret` (dev only) | **Required in production**. `SCRUNNER_JWT_SECRET` is still accepted. |
| `SCRUNNER_SECRETS_KEY` | `scrunner-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. | | `JERAPAH_FLOW_SECRETS_KEY` | `scrunner-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. `SCRUNNER_SECRETS_KEY` is still accepted. |
| `SCRUNNER_DB_PATH` | `packages/server/data/scrunner.db` | SQLite file | | `JERAPAH_FLOW_DB_PATH` | `packages/server/data/jerapah-flow.db` (or existing `scrunner.db`) | SQLite file. `SCRUNNER_DB_PATH` is still accepted. |
| `SCRUNNER_LOG_LEVEL` | `debug` | Pino level | | `JERAPAH_FLOW_LOG_LEVEL` | `debug` | Pino level |
| `SCRUNNER_RETENTION_DAYS` | `30` | Run history prune | | `JERAPAH_FLOW_RETENTION_DAYS` | `30` | Run history prune |
| `SCRUNNER_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev | | `JERAPAH_FLOW_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev |
| `PORT` | `9000` | HTTP port | | `PORT` | `9000` | HTTP port |
| `NODE_ENV` | — | Set `production` for secure cookies | | `NODE_ENV` | — | Set `production` for secure cookies |
@@ -48,7 +50,7 @@ The first account created becomes **admin**. Later accounts are created from Use
```bash ```bash
pnpm install pnpm install
pnpm build pnpm build
SCRUNNER_JWT_SECRET=... SCRUNNER_SECRETS_KEY=... NODE_ENV=production pnpm start JERAPAH_FLOW_JWT_SECRET=... JERAPAH_FLOW_SECRETS_KEY=... NODE_ENV=production pnpm start
``` ```
The server serves `packages/web/dist` when that folder exists. The server serves `packages/web/dist` when that folder exists.
+7 -7
View File
@@ -1,16 +1,16 @@
{ {
"name": "scrunner", "name": "jerapah-flow",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"description": "Script/workflow runner with admin UI", "description": "Script/workflow runner with admin UI",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "pnpm --filter @scrunner/server --filter @scrunner/web --parallel dev", "dev": "pnpm --filter @jerapah-flow/server --filter @jerapah-flow/web --parallel dev",
"dev:server": "pnpm --filter @scrunner/server dev", "dev:server": "pnpm --filter @jerapah-flow/server dev",
"dev:web": "pnpm --filter @scrunner/web dev", "dev:web": "pnpm --filter @jerapah-flow/web dev",
"build": "pnpm --filter @scrunner/web build", "build": "pnpm --filter @jerapah-flow/web build",
"start": "pnpm --filter @scrunner/server start", "start": "pnpm --filter @jerapah-flow/server start",
"migrate": "pnpm --filter @scrunner/server migrate" "migrate": "pnpm --filter @jerapah-flow/server migrate"
}, },
"packageManager": "pnpm@10.25.0", "packageManager": "pnpm@10.25.0",
"pnpm": { "pnpm": {
+6 -1
View File
@@ -2,7 +2,12 @@ import fs from "fs";
import path from "path"; import path from "path";
import { DATA_DIR, SERVER_ROOT } from "./paths.js"; import { DATA_DIR, SERVER_ROOT } from "./paths.js";
const dbPath = process.env.SCRUNNER_DB_PATH ?? path.join(DATA_DIR, "scrunner.db"); const defaultNew = path.join(DATA_DIR, "jerapah-flow.db");
const defaultLegacy = path.join(DATA_DIR, "scrunner.db");
const dbPath =
process.env.JERAPAH_FLOW_DB_PATH ??
process.env.SCRUNNER_DB_PATH ??
(fs.existsSync(defaultLegacy) && !fs.existsSync(defaultNew) ? defaultLegacy : defaultNew);
fs.mkdirSync(path.dirname(dbPath), { recursive: true }); fs.mkdirSync(path.dirname(dbPath), { recursive: true });
/** @type {import("knex").Knex.Config} */ /** @type {import("knex").Knex.Config} */
+2 -2
View File
@@ -105,7 +105,7 @@ fs.mkdirSync(LOGS_DIR, { recursive: true });
const rollingFile = pino.transport({ const rollingFile = pino.transport({
target: "pino-roll", target: "pino-roll",
options: { options: {
file: path.join(LOGS_DIR, "scrunner.log"), file: path.join(LOGS_DIR, "jerapah-flow.log"),
size: "10m", size: "10m",
mkdir: true, mkdir: true,
limit: { count: 5 }, limit: { count: 5 },
@@ -113,7 +113,7 @@ const rollingFile = pino.transport({
}); });
export const log = pino( export const log = pino(
{ level: process.env.SCRUNNER_LOG_LEVEL ?? "debug" }, { level: process.env.JERAPAH_FLOW_LOG_LEVEL ?? process.env.SCRUNNER_LOG_LEVEL ?? "debug" },
pino.multistream([ pino.multistream([
{ level: "debug", stream: redactStream(process.stdout) }, { level: "debug", stream: redactStream(process.stdout) },
{ level: "debug", stream: redactStream(rollingFile) }, { level: "debug", stream: redactStream(rollingFile) },
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "@scrunner/server", "name": "@jerapah-flow/server",
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
+1 -1
View File
@@ -312,7 +312,7 @@ export function createRegistry(server) {
pruneTask.destroy(); pruneTask.destroy();
pruneTask = null; pruneTask = null;
} }
const days = Number(process.env.SCRUNNER_RETENTION_DAYS ?? 30); const days = Number(process.env.JERAPAH_FLOW_RETENTION_DAYS ?? process.env.SCRUNNER_RETENTION_DAYS ?? 30);
pruneTask = cron.schedule( pruneTask = cron.schedule(
"0 0 * * *", "0 0 * * *",
async () => { async () => {
+3 -2
View File
@@ -26,11 +26,12 @@ await migrate();
enableLogPersistence(); enableLogPersistence();
const jwtSecret = const jwtSecret =
process.env.JERAPAH_FLOW_JWT_SECRET ??
process.env.SCRUNNER_JWT_SECRET ?? process.env.SCRUNNER_JWT_SECRET ??
(process.env.NODE_ENV === "production" ? "" : "scrunner-dev-secret"); (process.env.NODE_ENV === "production" ? "" : "scrunner-dev-secret");
if (!jwtSecret) { if (!jwtSecret) {
log.error("SCRUNNER_JWT_SECRET is required in production"); log.error("JERAPAH_FLOW_JWT_SECRET is required in production");
process.exit(1); process.exit(1);
} }
@@ -52,7 +53,7 @@ await server.register(jwt, {
}, },
}); });
await server.register(cors, { await server.register(cors, {
origin: process.env.SCRUNNER_CORS_ORIGIN ?? "http://localhost:5173", origin: process.env.JERAPAH_FLOW_CORS_ORIGIN ?? process.env.SCRUNNER_CORS_ORIGIN ?? "http://localhost:5173",
credentials: true, credentials: true,
}); });
+1 -1
View File
@@ -395,7 +395,7 @@ function createScriptSandbox({
}; };
vm.createContext(sandbox, { vm.createContext(sandbox, {
name: `scrunner:${workflowName}:${script}`, name: `jerapah-flow:${workflowName}:${script}`,
codeGeneration: { strings: false, wasm: false }, codeGeneration: { strings: false, wasm: false },
}); });
+6 -5
View File
@@ -83,7 +83,7 @@ async function ntfy(ctx) {
} }
const headers = ntfyHeaders(ctx); const headers = ntfyHeaders(ctx);
const ntfyUrl = ctx.config?.url || "https://ntfy.sh/scrunner"; const ntfyUrl = ctx.config?.url || "https://ntfy.sh/jerapah-flow";
if (hasFile) { if (hasFile) {
const filename = ctx.data?.filename || "attachment"; const filename = ctx.data?.filename || "attachment";
@@ -114,7 +114,7 @@ async function ntfy(ctx) {
log.info("ntfy sending message to %s", ntfyUrl); log.info("ntfy sending message to %s", ntfyUrl);
log.info("ntfy messsage: %s", truncatedMessage); log.info("ntfy messsage: %s", truncatedMessage);
await $axios.post(ntfyUrl, ctx.data?.message || "Hello from scrunner", { await $axios.post(ntfyUrl, ctx.data?.message || "Hello from jerapah-flow", {
headers, headers,
}); });
} }
@@ -132,10 +132,11 @@ async function ntfy(ctx) {
ntfy.meta = { ntfy.meta = {
description: "Send a message or file to an ntfy topic", description: "Send a message or file to an ntfy topic",
previewConfigKey: "url", previewConfigKey: "url",
tags: ["channel"],
config: { config: {
url: { url: {
type: "string", type: "string",
default: "https://ntfy.sh/scrunner", default: "https://ntfy.sh/jerapah-flow",
description: "ntfy topic URL", description: "ntfy topic URL",
}, },
fingerprint: { fingerprint: {
@@ -166,8 +167,8 @@ ntfy.meta = {
sent: { type: "string", description: "Replaces the workflow context with { sent: \"true\" }" }, sent: { type: "string", description: "Replaces the workflow context with { sent: \"true\" }" },
}, },
example: { example: {
data: { title: "Hello", message: "Hello from scrunner" }, data: { title: "Hello", message: "Hello from jerapah-flow" },
config: { url: "https://ntfy.sh/scrunner", fingerprint: true }, config: { url: "https://ntfy.sh/jerapah-flow", fingerprint: true },
}, },
}; };
+2 -1
View File
@@ -231,6 +231,7 @@ async function sendEmail(ctx) {
sendEmail.meta = { sendEmail.meta = {
description: "Send an email via SMTP (nodemailer); plain text, HTML, or both", description: "Send an email via SMTP (nodemailer); plain text, HTML, or both",
previewConfigKey: "from", previewConfigKey: "from",
tags: ["channel"],
config: { config: {
service: { service: {
type: "string", type: "string",
@@ -427,7 +428,7 @@ sendEmail.meta = {
to: ["recipient@example.com", "other@example.com"], to: ["recipient@example.com", "other@example.com"],
cc: "manager@example.com", cc: "manager@example.com",
bcc: "audit@example.com", bcc: "audit@example.com",
subject: "Hello from scrunner", subject: "Hello from JerapahFlow",
text: "This is a plain-text test message.", text: "This is a plain-text test message.",
}, },
config: { config: {
+2 -1
View File
@@ -11,10 +11,11 @@ const AUTH_TAG_LEN = 16;
*/ */
export function resolveSecretsKeyMaterial() { export function resolveSecretsKeyMaterial() {
const raw = const raw =
process.env.JERAPAH_FLOW_SECRETS_KEY ??
process.env.SCRUNNER_SECRETS_KEY ?? process.env.SCRUNNER_SECRETS_KEY ??
(process.env.NODE_ENV === "production" ? "" : DEV_DEFAULT); (process.env.NODE_ENV === "production" ? "" : DEV_DEFAULT);
if (!raw) { if (!raw) {
throw new Error("SCRUNNER_SECRETS_KEY is required in production"); throw new Error("JERAPAH_FLOW_SECRETS_KEY is required in production");
} }
return raw; return raw;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import * as store from "../../store.js"; import * as store from "../../store.js";
export const COOKIE = "scrunner_token"; export const COOKIE = "jerapah_flow_token";
export const OPEN_API_ROUTES = new Set([ export const OPEN_API_ROUTES = new Set([
"GET /auth/bootstrap", "GET /auth/bootstrap",
"POST /auth/register", "POST /auth/register",
@@ -14,7 +14,7 @@ scripts:
- fetch-binary.js - fetch-binary.js
- script: ntfy.js - script: ntfy.js
config: config:
url: https://ntfy.sh/scrunner url: https://ntfy.sh/jerapah-flow
triggers: triggers:
- type: HTTP - type: HTTP
@@ -4,7 +4,7 @@ description: |
scripts: scripts:
- script: ntfy.js - script: ntfy.js
config: config:
url: https://ntfy.sh/scrunner url: https://ntfy.sh/jerapah-flow
triggers: triggers:
- type: cron - type: cron
schedule: "*/20 * * * *" schedule: "*/20 * * * *"
@@ -21,7 +21,7 @@ scripts:
} }
- script: ntfy.js - script: ntfy.js
config: config:
url: https://ntfy.sh/scrunner url: https://ntfy.sh/jerapah-flow
triggers: triggers:
- type: HTTP - type: HTTP
@@ -8,7 +8,7 @@ scripts:
name: send-gmail name: send-gmail
expression: | expression: |
{ {
"subject": "scrunner test", "subject": "JerapahFlow test",
"text": "Hello from test-send-gmail", "text": "Hello from test-send-gmail",
"html": "<p>Hello from <strong>test-send-gmail</strong></p>" "html": "<p>Hello from <strong>test-send-gmail</strong></p>"
} }
@@ -29,7 +29,7 @@ scripts:
script: ntfy.js script: ntfy.js
needs: [compose] needs: [compose]
config: config:
url: https://ntfy.sh/scrunner url: https://ntfy.sh/jerapah-flow
triggers: triggers:
- type: HTTP - type: HTTP
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"name": "@scrunner/web", "name": "@jerapah-flow/web",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
+2 -2
View File
@@ -27,8 +27,8 @@ export function App() {
function onUnauthorized() { function onUnauthorized() {
qc.setQueryData(["me"], null); qc.setQueryData(["me"], null);
} }
window.addEventListener("scrunner:unauthorized", onUnauthorized); window.addEventListener("jerapah-flow:unauthorized", onUnauthorized);
return () => window.removeEventListener("scrunner:unauthorized", onUnauthorized); return () => window.removeEventListener("jerapah-flow:unauthorized", onUnauthorized);
}, [qc]); }, [qc]);
const user = me.data?.user; const user = me.data?.user;
+1 -1
View File
@@ -11,7 +11,7 @@ api.interceptors.response.use(
const url = err.config?.url ?? ""; const url = err.config?.url ?? "";
const isAuthCall = url.includes("/auth/login") || url.includes("/auth/register") || url.includes("/auth/me") || url.includes("/auth/bootstrap"); const isAuthCall = url.includes("/auth/login") || url.includes("/auth/register") || url.includes("/auth/me") || url.includes("/auth/bootstrap");
if (err.response?.status === 401 && !isAuthCall) { if (err.response?.status === 401 && !isAuthCall) {
window.dispatchEvent(new Event("scrunner:unauthorized")); window.dispatchEvent(new Event("jerapah-flow:unauthorized"));
} }
return Promise.reject(err); return Promise.reject(err);
}, },