diff --git a/README.md b/README.md index c9d420a..633e9ef 100644 --- a/README.md +++ b/README.md @@ -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. ## Packages -- `packages/server` — Fastify runner, HTTP/cron triggers, admin REST API -- `packages/web` — React admin UI (Vite, DaisyUI, React Query) +- `@jerapah-flow/server` (`packages/server`) — Fastify runner, HTTP/cron triggers, admin REST API +- `@jerapah-flow/web` (`packages/web`) — React admin UI (Vite, DaisyUI, React Query) ## Setup @@ -34,12 +36,12 @@ The first account created becomes **admin**. Later accounts are created from Use | Variable | Default | Notes | |---|---|---| -| `SCRUNNER_JWT_SECRET` | `scrunner-dev-secret` (dev only) | **Required in production** | -| `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. | -| `SCRUNNER_DB_PATH` | `packages/server/data/scrunner.db` | SQLite file | -| `SCRUNNER_LOG_LEVEL` | `debug` | Pino level | -| `SCRUNNER_RETENTION_DAYS` | `30` | Run history prune | -| `SCRUNNER_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev | +| `JERAPAH_FLOW_JWT_SECRET` | `scrunner-dev-secret` (dev only) | **Required in production**. `SCRUNNER_JWT_SECRET` is still accepted. | +| `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. | +| `JERAPAH_FLOW_DB_PATH` | `packages/server/data/jerapah-flow.db` (or existing `scrunner.db`) | SQLite file. `SCRUNNER_DB_PATH` is still accepted. | +| `JERAPAH_FLOW_LOG_LEVEL` | `debug` | Pino level | +| `JERAPAH_FLOW_RETENTION_DAYS` | `30` | Run history prune | +| `JERAPAH_FLOW_CORS_ORIGIN` | `http://localhost:5173` | Vite origin in dev | | `PORT` | `9000` | HTTP port | | `NODE_ENV` | — | Set `production` for secure cookies | @@ -48,7 +50,7 @@ The first account created becomes **admin**. Later accounts are created from Use ```bash pnpm install 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. diff --git a/package.json b/package.json index 040fda0..d001c21 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,16 @@ { - "name": "scrunner", + "name": "jerapah-flow", "version": "1.0.0", "private": true, "description": "Script/workflow runner with admin UI", "type": "module", "scripts": { - "dev": "pnpm --filter @scrunner/server --filter @scrunner/web --parallel dev", - "dev:server": "pnpm --filter @scrunner/server dev", - "dev:web": "pnpm --filter @scrunner/web dev", - "build": "pnpm --filter @scrunner/web build", - "start": "pnpm --filter @scrunner/server start", - "migrate": "pnpm --filter @scrunner/server migrate" + "dev": "pnpm --filter @jerapah-flow/server --filter @jerapah-flow/web --parallel dev", + "dev:server": "pnpm --filter @jerapah-flow/server dev", + "dev:web": "pnpm --filter @jerapah-flow/web dev", + "build": "pnpm --filter @jerapah-flow/web build", + "start": "pnpm --filter @jerapah-flow/server start", + "migrate": "pnpm --filter @jerapah-flow/server migrate" }, "packageManager": "pnpm@10.25.0", "pnpm": { diff --git a/packages/server/knexfile.js b/packages/server/knexfile.js index eed0814..495f345 100644 --- a/packages/server/knexfile.js +++ b/packages/server/knexfile.js @@ -2,7 +2,12 @@ import fs from "fs"; import path from "path"; 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 }); /** @type {import("knex").Knex.Config} */ diff --git a/packages/server/logger.js b/packages/server/logger.js index 0c19b24..2cf80b6 100644 --- a/packages/server/logger.js +++ b/packages/server/logger.js @@ -105,7 +105,7 @@ fs.mkdirSync(LOGS_DIR, { recursive: true }); const rollingFile = pino.transport({ target: "pino-roll", options: { - file: path.join(LOGS_DIR, "scrunner.log"), + file: path.join(LOGS_DIR, "jerapah-flow.log"), size: "10m", mkdir: true, limit: { count: 5 }, @@ -113,7 +113,7 @@ const rollingFile = pino.transport({ }); 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([ { level: "debug", stream: redactStream(process.stdout) }, { level: "debug", stream: redactStream(rollingFile) }, diff --git a/packages/server/package.json b/packages/server/package.json index e5bc384..c3c1eb5 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,5 +1,5 @@ { - "name": "@scrunner/server", + "name": "@jerapah-flow/server", "version": "1.0.0", "private": true, "type": "module", diff --git a/packages/server/registry.js b/packages/server/registry.js index d25bc4c..474da1d 100644 --- a/packages/server/registry.js +++ b/packages/server/registry.js @@ -312,7 +312,7 @@ export function createRegistry(server) { pruneTask.destroy(); 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( "0 0 * * *", async () => { diff --git a/packages/server/runner.js b/packages/server/runner.js index 452e380..6ef41ce 100644 --- a/packages/server/runner.js +++ b/packages/server/runner.js @@ -26,11 +26,12 @@ await migrate(); enableLogPersistence(); const jwtSecret = + process.env.JERAPAH_FLOW_JWT_SECRET ?? process.env.SCRUNNER_JWT_SECRET ?? (process.env.NODE_ENV === "production" ? "" : "scrunner-dev-secret"); if (!jwtSecret) { - log.error("SCRUNNER_JWT_SECRET is required in production"); + log.error("JERAPAH_FLOW_JWT_SECRET is required in production"); process.exit(1); } @@ -52,7 +53,7 @@ await server.register(jwt, { }, }); 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, }); diff --git a/packages/server/script-sandbox.js b/packages/server/script-sandbox.js index aa22fc6..d9ab9c0 100644 --- a/packages/server/script-sandbox.js +++ b/packages/server/script-sandbox.js @@ -395,7 +395,7 @@ function createScriptSandbox({ }; vm.createContext(sandbox, { - name: `scrunner:${workflowName}:${script}`, + name: `jerapah-flow:${workflowName}:${script}`, codeGeneration: { strings: false, wasm: false }, }); diff --git a/packages/server/scripts/ntfy.js b/packages/server/scripts/ntfy.js index 892db01..408c600 100644 --- a/packages/server/scripts/ntfy.js +++ b/packages/server/scripts/ntfy.js @@ -83,7 +83,7 @@ async function ntfy(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) { 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 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, }); } @@ -132,10 +132,11 @@ async function ntfy(ctx) { ntfy.meta = { description: "Send a message or file to an ntfy topic", previewConfigKey: "url", + tags: ["channel"], config: { url: { type: "string", - default: "https://ntfy.sh/scrunner", + default: "https://ntfy.sh/jerapah-flow", description: "ntfy topic URL", }, fingerprint: { @@ -166,8 +167,8 @@ ntfy.meta = { sent: { type: "string", description: "Replaces the workflow context with { sent: \"true\" }" }, }, example: { - data: { title: "Hello", message: "Hello from scrunner" }, - config: { url: "https://ntfy.sh/scrunner", fingerprint: true }, + data: { title: "Hello", message: "Hello from jerapah-flow" }, + config: { url: "https://ntfy.sh/jerapah-flow", fingerprint: true }, }, }; diff --git a/packages/server/scripts/send-email.js b/packages/server/scripts/send-email.js index 840bf94..9d090e5 100644 --- a/packages/server/scripts/send-email.js +++ b/packages/server/scripts/send-email.js @@ -231,6 +231,7 @@ async function sendEmail(ctx) { sendEmail.meta = { description: "Send an email via SMTP (nodemailer); plain text, HTML, or both", previewConfigKey: "from", + tags: ["channel"], config: { service: { type: "string", @@ -427,7 +428,7 @@ sendEmail.meta = { to: ["recipient@example.com", "other@example.com"], cc: "manager@example.com", bcc: "audit@example.com", - subject: "Hello from scrunner", + subject: "Hello from JerapahFlow", text: "This is a plain-text test message.", }, config: { diff --git a/packages/server/secrets.js b/packages/server/secrets.js index 86f0865..45da609 100644 --- a/packages/server/secrets.js +++ b/packages/server/secrets.js @@ -11,10 +11,11 @@ const AUTH_TAG_LEN = 16; */ export function resolveSecretsKeyMaterial() { const raw = + process.env.JERAPAH_FLOW_SECRETS_KEY ?? process.env.SCRUNNER_SECRETS_KEY ?? (process.env.NODE_ENV === "production" ? "" : DEV_DEFAULT); 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; } diff --git a/packages/server/src/api/auth.js b/packages/server/src/api/auth.js index d877c3d..8683230 100644 --- a/packages/server/src/api/auth.js +++ b/packages/server/src/api/auth.js @@ -1,7 +1,7 @@ import bcrypt from "bcryptjs"; import * as store from "../../store.js"; -export const COOKIE = "scrunner_token"; +export const COOKIE = "jerapah_flow_token"; export const OPEN_API_ROUTES = new Set([ "GET /auth/bootstrap", "POST /auth/register", diff --git a/packages/server/workflows/default/comic-monkeyuser-to-ntfy.yaml b/packages/server/workflows/default/comic-monkeyuser-to-ntfy.yaml index 345725f..c0161a2 100644 --- a/packages/server/workflows/default/comic-monkeyuser-to-ntfy.yaml +++ b/packages/server/workflows/default/comic-monkeyuser-to-ntfy.yaml @@ -14,7 +14,7 @@ scripts: - fetch-binary.js - script: ntfy.js config: - url: https://ntfy.sh/scrunner + url: https://ntfy.sh/jerapah-flow triggers: - type: HTTP diff --git a/packages/server/workflows/default/cron-example.yaml b/packages/server/workflows/default/cron-example.yaml index 5d2e484..ce1f57e 100644 --- a/packages/server/workflows/default/cron-example.yaml +++ b/packages/server/workflows/default/cron-example.yaml @@ -4,7 +4,7 @@ description: | scripts: - script: ntfy.js config: - url: https://ntfy.sh/scrunner + url: https://ntfy.sh/jerapah-flow triggers: - type: cron schedule: "*/20 * * * *" diff --git a/packages/server/workflows/default/rss-selfhst-to-ntfy.yaml b/packages/server/workflows/default/rss-selfhst-to-ntfy.yaml index dba12c2..32b05b4 100644 --- a/packages/server/workflows/default/rss-selfhst-to-ntfy.yaml +++ b/packages/server/workflows/default/rss-selfhst-to-ntfy.yaml @@ -21,7 +21,7 @@ scripts: } - script: ntfy.js config: - url: https://ntfy.sh/scrunner + url: https://ntfy.sh/jerapah-flow triggers: - type: HTTP diff --git a/packages/server/workflows/default/test-send-gmail.yaml b/packages/server/workflows/default/test-send-gmail.yaml index 4b5b699..0e67a39 100644 --- a/packages/server/workflows/default/test-send-gmail.yaml +++ b/packages/server/workflows/default/test-send-gmail.yaml @@ -8,7 +8,7 @@ scripts: name: send-gmail expression: | { - "subject": "scrunner test", + "subject": "JerapahFlow test", "text": "Hello from test-send-gmail", "html": "

Hello from test-send-gmail

" } diff --git a/packages/server/workflows/default/time-and-comic-to-ntfy.yaml b/packages/server/workflows/default/time-and-comic-to-ntfy.yaml index 52e1077..640e683 100644 --- a/packages/server/workflows/default/time-and-comic-to-ntfy.yaml +++ b/packages/server/workflows/default/time-and-comic-to-ntfy.yaml @@ -29,7 +29,7 @@ scripts: script: ntfy.js needs: [compose] config: - url: https://ntfy.sh/scrunner + url: https://ntfy.sh/jerapah-flow triggers: - type: HTTP diff --git a/packages/web/package.json b/packages/web/package.json index 0778ade..9bdd243 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,5 +1,5 @@ { - "name": "@scrunner/web", + "name": "@jerapah-flow/web", "private": true, "version": "1.0.0", "type": "module", diff --git a/packages/web/src/App.jsx b/packages/web/src/App.jsx index e067c3f..903c93d 100644 --- a/packages/web/src/App.jsx +++ b/packages/web/src/App.jsx @@ -27,8 +27,8 @@ export function App() { function onUnauthorized() { qc.setQueryData(["me"], null); } - window.addEventListener("scrunner:unauthorized", onUnauthorized); - return () => window.removeEventListener("scrunner:unauthorized", onUnauthorized); + window.addEventListener("jerapah-flow:unauthorized", onUnauthorized); + return () => window.removeEventListener("jerapah-flow:unauthorized", onUnauthorized); }, [qc]); const user = me.data?.user; diff --git a/packages/web/src/api/client.js b/packages/web/src/api/client.js index 14ab21e..5d9a7aa 100644 --- a/packages/web/src/api/client.js +++ b/packages/web/src/api/client.js @@ -11,7 +11,7 @@ api.interceptors.response.use( const url = err.config?.url ?? ""; const isAuthCall = url.includes("/auth/login") || url.includes("/auth/register") || url.includes("/auth/me") || url.includes("/auth/bootstrap"); if (err.response?.status === 401 && !isAuthCall) { - window.dispatchEvent(new Event("scrunner:unauthorized")); + window.dispatchEvent(new Event("jerapah-flow:unauthorized")); } return Promise.reject(err); },