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:
@@ -1,11 +1,13 @@
|
|||||||
# scrunner
|
# JerapahFlow
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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
@@ -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": {
|
||||||
|
|||||||
@@ -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} */
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,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,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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user