Page 1 of 1

Evernode Christmas Special - Portable Evdevkit

Posted: Thu Dec 25, 2025 12:01 am
by deployking
How to create a portable evdevkit software:

Evdevkit for private use on Windows + Mac + Linux:
Create file evdevkit.js

Code: Select all

#!/usr/bin/env node

const { spawnSync } = require("child_process");

const evdevkitPath = require.resolve("evdevkit/index.js");


const args = process.argv.slice(2);

let secret;
let privateKey;
let randomPrivateKey = false;
const passthrough = [];

for (let i = 0; i < args.length; i++) {
  const a = args[i];

  if (a === "--secret") {
    secret = args[++i];
  } else if (a === "--privatekey") {
    privateKey = args[++i];
  } else if (a === "--randomprivatekey") {
    randomPrivateKey = true;
  } else {
    passthrough.push(a);
  }
}

if (secret) {
  process.env.EV_TENANT_SECRET = secret;
}

if (randomPrivateKey) {
  const result = spawnSync(
    process.execPath,
    [evdevkitPath, "keygen"],
    { encoding: "utf8" }
  );

  if (result.status !== 0) {
    console.error("Failed to generate key pair");
    process.exit(1);
  }

  const privMatch = result.stdout.match(/privateKey:\s*'([^']+)'/);
  const pubMatch  = result.stdout.match(/publicKey:\s*'([^']+)'/);

  if (!privMatch || !pubMatch) {
    console.error("Could not parse generated keys");
    process.exit(1);
  }

  privateKey = privMatch[1];

  console.log("");
  console.log("New key pair generated");
  console.log("Private key:", privateKey);
  console.log("Public key :", pubMatch[1]);
  console.log("");
}

if (privateKey) {
  process.env.EV_USER_PRIVATE_KEY = privateKey;
}

const run = spawnSync(
  process.execPath,
  [evdevkitPath, ...passthrough],
  {
    stdio: "inherit",
    env: process.env
  }
);

process.exit(run.status ?? 0);
Create package.json

Code: Select all

{
  "name": "evdevkit-bin",
  "private": true,
  "bin": "evdevkit.js",
"pkg": {
    "scripts": [
      "node_modules/evdevkit/index.js"
    ]
  }
}
Make sure you install evdevkit with npm i evdevkit

Build with pkg (install with npm i pkg -g):
pkg evdevkit.js --targets node18-linux-x64 --output evdevkit
pkg evdevkit.js --targets node18-win-x64 --output evdevkit.exe
pkg evdevkit.js --targets node18-macos-x64 --output evdevkit-macos


You can now deploy with the file by using two different commands:
./evdevkit --secret seed --privatekey mykey
./evdevkit --secret seed --randomprivatekey

One of them will create a random evdevkit key for you, the other one allows you to input your own.

Everything else is as usual, acquire -i -m rAddress.


Evdevkit as a deamon

When you want to use a portable evdevkit as a deamon you need to use stdin to protect your secret seeds to be visible in the processes. That's an other bin file, and you can see how to pkg it below. You will also see how we need external scripts to grant more memory to the bin file when being ran. Keep in mind that memory leaks (etc) can still happen, so never treat this as secure.

Acquire with:
echo "myseed" | ./evdevkit-runner.sh rAddress

Extend with:
echo "myseed" | ./evdevkit-runner.sh whateveryouwant instanceID rAddress moments

PS: These sh files have hardcoded image, so you can fix that if you want to pass the image in cli.

Build bin with:
pkg evdevkitstdin.js --targets node18-linux-x64 --output evdevkit

Log files in rent folder and instance folder.

That version looks like this:
evdevkitstdin.js

Code: Select all

#!/usr/bin/env node
const fs = require("fs");
const { spawnSync } = require("child_process");
function readStdin() {
  try {
    return fs.readFileSync(0, "utf8").trim();
  } catch {
    return "";
  }
}

const args = process.argv.slice(2);

let secret;
let privateKey;
let randomPrivateKey = false;
const passthrough = [];

for (let i = 0; i < args.length; i++) {
  const a = args[i];

  if (a === "--secret-stdin") {
    secret = readStdin();
  } else if (a === "--privatekey") {
    privateKey = args[++i];
  } else if (a === "--randomprivatekey") {
    randomPrivateKey = true;
  } else {
    passthrough.push(a);
  }
}

if (secret && secret.length < 20) {
  console.error("Invalid secret from stdin");
  process.exit(1);
}

if (randomPrivateKey) {
  const result = spawnSync(
    process.execPath,
    [process.argv[1], "keygen"],
    {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "inherit"]
    }
  );

  if (result.status !== 0 || !result.stdout) {
    console.error("Failed to generate key pair");
    process.exit(1);
  }

  const privMatch = result.stdout.match(/privateKey:\s*'([^']+)'/);
  const pubMatch  = result.stdout.match(/publicKey:\s*'([^']+)'/);

  if (!privMatch || !pubMatch) {
    console.error("Could not parse generated keys");
    process.exit(1);
  }

  privateKey = privMatch[1];

  console.log("");
  console.log("New key pair generated");
  console.log("Public key :", pubMatch[1]);
  console.log("");
}

if (secret) {
  process.env.EV_TENANT_SECRET = secret;
}

if (privateKey) {
  process.env.EV_USER_PRIVATE_KEY = privateKey;
}

process.argv = [
  process.argv[0],
  process.argv[1],
  ...passthrough
];

require("evdevkit/index.js");
To use it in a deamon you need a script that runs it (place the script in the same folder as evdevkit bin file). Remember to send the seed as a stdin.

Create folder /usr/local/bin/evernoderunner and make sure you have permission for it, also maks sure evdevkit-runner.sh and evdevkit-extender.sh got chmod +x.

Remember to have your evdevkit bin file in here aswell with +x.

evdevkit-runner.sh

Code: Select all

#!/bin/bash
set -euo pipefail
read -r SEED || exit 1
ADDRESS="$1"
[ -n "$SEED" ] || exit 1


ulimit -v unlimited
ulimit -m unlimited
ulimit -s unlimited

export WASMTIME_MAX_MEMORY=8G
export WASM_STACK_SIZE=64M



BASE_DIR="/usr/local/bin/evernoderunner"
LOG_DIR="$BASE_DIR/rent"
INSTANCES_DIR="$BASE_DIR/instances"

TS=$(date +%s)
LOG_FILE="$LOG_DIR/rent_${ADDRESS}.log"

mkdir -p "$LOG_DIR" "$INSTANCES_DIR"
chmod 700 "$LOG_DIR" "$INSTANCES_DIR"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

HB_PID=""

cleanup() {
  [ -n "$HB_PID" ] && kill "$HB_PID" 2>/dev/null || true
}
trap cleanup EXIT

{
  log "=== Instance acquirement started ==="
  log "Host: $ADDRESS"
  log "User: $(whoami)"
  log "Image: yourimage/yourimage:latest"
  log "Seed: [REDACTED]"
  log "evdevkit: $(command -v evdevkit)"

  (
    while true; do
      sleep 30
      log "Still running..."
    done
  ) &
  HB_PID=$!

printf '%s' "$SEED" | /usr/local/bin/evdevkit \
  --secret-stdin \
  --randomprivatekey \
  acquire \
  -i yourimage/yourimage:latest \
  -m 1 \
  "$ADDRESS"
  unset SEED
  
  kill "$HB_PID" 2>/dev/null || true

  INSTANCE_BLOCK=$(sed -n "/Instance created!/,/}/p" "$LOG_FILE")
  if [ -z "$INSTANCE_BLOCK" ]; then
    log "ERROR: Instance block not found"
    log "STATUS: FAILURE"
    exit 1
  fi
  log "Parsing instance metadata"
  NAME=$(echo "$INSTANCE_BLOCK" | sed -n "s/.*name: '\([^']*\)'.*/\1/p")
  USER_PORT=$(echo "$INSTANCE_BLOCK" | sed -n "s/.*user_port: '\([^']*\)'.*/\1/p")
  DOMAIN=$(echo "$INSTANCE_BLOCK" | sed -n "s/.*domain: '\([^']*\)'.*/\1/p")
  GP_TCP_PORT=$(echo "$INSTANCE_BLOCK" | sed -n "s/.*gp_tcp_port: '\([^']*\)'.*/\1/p")
  GP_UDP_PORT=$(echo "$INSTANCE_BLOCK" | sed -n "s/.*gp_udp_port: '\([^']*\)'.*/\1/p")
  CREATED_TS=$(echo "$INSTANCE_BLOCK" | sed -n "s/.*created_timestamp: \([0-9]*\).*/\1/p")

  if [ -z "$NAME" ] || [ -z "$CREATED_TS" ]; then
    log "ERROR: Failed to extract required fields"
    log "STATUS: FAILURE"
    exit 1
  fi
  EXPIRES_TS=$((CREATED_TS + 3600000))
  CREATED_AT=$(date -d "@$((CREATED_TS/1000))" "+%Y-%m-%d %H:%M:%S")
  EXPIRES_AT=$(date -d "@$((EXPIRES_TS/1000))" "+%Y-%m-%d %H:%M:%S")

  jq -n \
    --arg instance_id "$NAME" \
    --arg host "$ADDRESS" \
    --arg domain "$DOMAIN" \
    --argjson user_port "$USER_PORT" \
    --argjson gp_tcp_port "$GP_TCP_PORT" \
    --argjson gp_udp_port "$GP_UDP_PORT" \
    --argjson created_ts "$CREATED_TS" \
    --argjson expires_ts "$EXPIRES_TS" \
    --arg created_at "$CREATED_AT" \
    --arg expires_at "$EXPIRES_AT" \
    '{
      instance_id: $instance_id,
      host_address: $host,
      domain: $domain,
      user_port: $user_port,
      gp_tcp_port: $gp_tcp_port,
      gp_udp_port: $gp_udp_port,
      created_timestamp: $created_ts,
      expires_timestamp: $expires_ts,
      created_at: $created_at,
      expires_at: $expires_at,
      status: "active"
    }' > "$INSTANCES_DIR/$NAME.json"
  log "Instance JSON written: $INSTANCES_DIR/$NAME.json"
  log "STATUS: SUCCESS"
  log "Finished at $(date)"

} >>"$LOG_FILE" 2>&1


evdevkit-extender.sh

Code: Select all

#!/bin/bash
set -euo pipefail

read -r SEED || exit 1
RUN_ID="$1"
INSTANCE_NAME="$2"
ADDRESS="$3"
MOMENTS="$4"

[ -n "$SEED" ] || exit 1
[ -n "$INSTANCE_NAME" ] || exit 1
[ -n "$ADDRESS" ] || exit 1
[ -n "$MOMENTS" ] || exit 1

if ! [[ "$MOMENTS" =~ ^[0-9]+$ ]]; then
  exit 1
fi

ulimit -v unlimited
ulimit -m unlimited
ulimit -s unlimited

export WASMTIME_MAX_MEMORY=8G
export WASM_STACK_SIZE=64M

BASE_DIR="/usr/local/bin/evernoderunner"
EXTEND_DIR="$BASE_DIR/extend"
INSTANCES_DIR="$BASE_DIR/instances"

LOG_FILE="$EXTEND_DIR/extend_${INSTANCE_NAME}.log"
TMP_FILE="/tmp/extend_${INSTANCE_NAME}.txt"
mkdir -p "$EXTEND_DIR"
chmod 700 "$EXTEND_DIR"

log() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

cleanup() {
  rm -f "$TMP_FILE"
}
trap cleanup EXIT

{
  log "=== EXTEND START RUN_ID=$RUN_ID ==="
  log "Instance: $INSTANCE_NAME"
  log "Host: $ADDRESS"
  log "Moments: $MOMENTS"
  log "User: $(whoami)"
  log "Seed: [REDACTED]"
  log "evdevkit: $(command -v evdevkit)"

  echo "${ADDRESS}:${INSTANCE_NAME}:${MOMENTS}" > "$TMP_FILE"
  chmod 600 "$TMP_FILE"

  log "Instance file created"
if ! printf '%s' "$SEED" | evdevkit \
    --secret-stdin \
    --randomprivatekey \
    extend "$TMP_FILE"
then
  log "ERROR: evdevkit extend failed"
  log "=== EXTEND FAILURE RUN_ID=$RUN_ID ==="
  exit 1
fi
  unset SEED
  INSTANCE_JSON="$INSTANCES_DIR/$INSTANCE_NAME.json"
  if [ ! -f "$INSTANCE_JSON" ]; then
    log "ERROR: Instance JSON not found"
log "=== EXTEND FAILURE RUN_ID=$RUN_ID ==="
    exit 1
  fi

  OLD_EXPIRES_TS=$(jq -r '.expires_timestamp' "$INSTANCE_JSON")

  if ! [[ "$OLD_EXPIRES_TS" =~ ^[0-9]+$ ]]; then
    log "ERROR: Invalid expires_timestamp"
log "=== EXTEND FAILURE RUN_ID=$RUN_ID ==="
 
    exit 1
  fi
  ADD_MS=$((MOMENTS * 3600000))
  NEW_EXPIRES_TS=$((OLD_EXPIRES_TS + ADD_MS))
  NEW_EXPIRES_AT=$(date -d "@$((NEW_EXPIRES_TS/1000))" "+%Y-%m-%d %H:%M:%S")
  jq \
    --argjson ts "$NEW_EXPIRES_TS" \
    --arg at "$NEW_EXPIRES_AT" \
    '.expires_timestamp = $ts | .expires_at = $at' \
    "$INSTANCE_JSON" > "$INSTANCE_JSON.tmp"
  mv "$INSTANCE_JSON.tmp" "$INSTANCE_JSON"
  log "Instance expiration extended"
  log "New expires_at: $NEW_EXPIRES_AT"
  log "=== EXTEND SUCCESS RUN_ID=$RUN_ID ==="
  log "Finished at $(date)"
} >>"$LOG_FILE" 2>&1