Merge branch 'main' of https://github.com/nasyarobby/jerapah-flow into cursor/ftp-sftp-script-3038

This commit is contained in:
2026-08-19 15:20:58 +07:00
15 changed files with 170 additions and 41 deletions
+15 -22
View File
@@ -1,41 +1,34 @@
### Manual trigger (default owner)
POST http://localhost:9000/u/default/mt
POST http://localhost:8700/u/default/mt
Content-Type: application/json
0
###
POST http://localhost:9000/u/default/time-to-ntfy
POST http://localhost:8700/u/default/time-to-ntfy
Content-Type: application/json
{}
### Auth bootstrap
GET http://localhost:9000/api/auth/bootstrap
### Login
POST http://localhost:9000/api/auth/login
###
GET http://localhost:8700/api/auth/bootstrap
###
POST http://localhost:8700/api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "changeme1"
}
### Dashboard
GET http://localhost:9000/api/dashboard
### Runs
GET http://localhost:9000/api/runs?owner=default&limit=20
### Reregister
POST http://localhost:9000/api/workflows/reregister
###
GET http://localhost:8700/api/dashboard
###
GET http://localhost:8700/api/runs?owner=default&limit=20
###
POST http://localhost:8700/api/workflows/reregister
Content-Type: application/json
{}
### Run workflow manually
POST http://localhost:9000/api/workflows/default/manual-trigger.yaml/run
###
POST http://localhost:8700/api/workflows/default/manual-trigger.yaml/run
Content-Type: application/json
{}
{}
+43
View File
@@ -0,0 +1,43 @@
const BUFFER_PREVIEW_BYTES = 16;
/**
* @param {unknown} value
*/
export function isBinary(value) {
return (
Buffer.isBuffer(value) ||
ArrayBuffer.isView(value) ||
value instanceof ArrayBuffer
);
}
/**
* Compact stand-in for JSON (Buffer.toJSON dumps every byte as a number).
* @param {Buffer | ArrayBufferView | ArrayBuffer} value
*/
export function summarizeBinary(value) {
const buf = Buffer.isBuffer(value)
? value
: value instanceof ArrayBuffer
? Buffer.from(value)
: Buffer.from(value.buffer, value.byteOffset, value.byteLength);
const take = Math.min(buf.length, BUFFER_PREVIEW_BYTES);
return {
type: "Buffer",
length: buf.length,
preview: buf.subarray(0, take).toString("hex"),
truncated: buf.length > take,
};
}
/**
* JSON.stringify replacer. Must be a real function so `this` is the holder:
* Buffer#toJSON already ran on `value`, but `this[key]` is still the Buffer.
* @param {string} key
* @param {unknown} value
*/
export function jsonPreviewReplacer(key, value) {
const raw = this[key];
if (isBinary(raw)) return summarizeBinary(raw);
return value;
}
+1 -1
View File
@@ -174,7 +174,7 @@ async function shutdown() {
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
const port = Number(process.env.PORT ?? 9000);
const port = Number(process.env.PORT ?? 8700);
server
.listen({
+1
View File
@@ -1,6 +1,7 @@
import { inspect } from "node:util";
export const REDACTED = "[secret]";
/** Short values are stored, but skipped in log redaction to avoid false positives. */
export const MIN_SECRET_LENGTH = 8;
/** @type {Set<string>} */
+3 -3
View File
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
import { db } from "./db.js";
import { assertOwner } from "./fs-store.js";
import { decryptSecret, encryptSecret } from "./secrets.js";
import { MIN_SECRET_LENGTH, registerPlaintext } from "./secret-value.js";
import { registerPlaintext } from "./secret-value.js";
const MAX_NAME_LENGTH = 128;
const SECRET_NAME_RE = /^[A-Za-z0-9._-]+$/;
@@ -68,8 +68,8 @@ export async function getSecretById(id) {
* @param {{ owner: string, name: string, value: string }} opts
*/
export async function upsertSecret({ owner, name, value }) {
if (typeof value !== "string" || value.length < MIN_SECRET_LENGTH) {
const err = new Error(`value must be at least ${MIN_SECRET_LENGTH} characters`);
if (typeof value !== "string" || value.length === 0) {
const err = new Error("value is required");
err.statusCode = 400;
throw err;
}
+4 -1
View File
@@ -1,4 +1,5 @@
import pino from "pino";
import { isBinary, summarizeBinary } from "../../json-preview.js";
import { redactString } from "../../secret-value.js";
const LEVEL_TO_NUM = {
@@ -64,7 +65,9 @@ export function safeSerialize(value) {
try {
return JSON.parse(
redactString(
JSON.stringify(value, (_key, v) => {
JSON.stringify(value, function (key, v) {
const raw = this[key];
if (isBinary(raw)) return summarizeBinary(raw);
if (typeof v === "bigint") return v.toString();
if (typeof v === "object" && v !== null) {
if (seen.has(v)) return "[Circular]";
+2 -5
View File
@@ -6,7 +6,6 @@ import {
listSecrets,
upsertSecret,
} from "../../secrets-store.js";
import { MIN_SECRET_LENGTH } from "../../secret-value.js";
/**
* @param {import("fastify").FastifyInstance} fastify
@@ -37,10 +36,8 @@ export default async function secretsPlugin(fastify) {
}
const value = String(body.value ?? "");
if (value.length < MIN_SECRET_LENGTH) {
return reply
.code(400)
.send({ error: `value must be at least ${MIN_SECRET_LENGTH} characters` });
if (value.length === 0) {
return reply.code(400).send({ error: "value is required" });
}
try {
+1 -1
View File
@@ -418,7 +418,7 @@ export default function workflowsPluginFactory(registry) {
return {
runId: result.runId,
status: result.status,
result: result.result,
result: store.toDisplayValue(result.result),
};
});
+16 -1
View File
@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { db } from "./db.js";
import { jsonPreviewReplacer } from "./json-preview.js";
import { redactString } from "./secret-value.js";
const MAX_JSON_BYTES = 64 * 1024;
@@ -12,7 +13,7 @@ export function serialize(value) {
if (value === undefined || value === null) return null;
let json;
try {
json = JSON.stringify(value);
json = JSON.stringify(value, jsonPreviewReplacer);
} catch {
json = JSON.stringify({ truncated: true, reason: "unserializable" });
}
@@ -24,6 +25,20 @@ export function serialize(value) {
});
}
/**
* Parsed JSON-safe copy for API / UI (buffers summarized, size-capped).
* @param {unknown} value
*/
export function toDisplayValue(value) {
const json = serialize(value);
if (json == null) return null;
try {
return JSON.parse(json);
} catch {
return null;
}
}
/**
* @param {string | null} value
* @returns {unknown}
@@ -0,0 +1,56 @@
import { jsonPreviewReplacer, summarizeBinary } from "../json-preview.js";
import { serialize, toDisplayValue } from "../store.js";
import { safeSerialize } from "../src/api/dry-run-logger.js";
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);
const summary = summarizeBinary(png);
if (summary.length !== png.length || summary.preview !== "89504e470d0a1a0a010203" || summary.truncated !== false) {
throw new Error(`summarizeBinary: ${JSON.stringify(summary)}`);
}
const long = Buffer.alloc(32, 0xff);
const longSummary = summarizeBinary(long);
if (longSummary.length !== 32 || longSummary.preview.length !== 32 || longSummary.truncated !== true) {
throw new Error(`long summarizeBinary: ${JSON.stringify(longSummary)}`);
}
const dumped = JSON.stringify({ file: png });
if (!dumped.includes('"data":[')) {
throw new Error("expected default Buffer JSON to include data array");
}
const previewed = JSON.stringify({ file: png }, jsonPreviewReplacer);
if (previewed.includes('"data":[')) {
throw new Error(`replacer still dumped bytes: ${previewed}`);
}
if (!previewed.includes('"preview":"89504e470d0a1a0a010203"')) {
throw new Error(`replacer missing hex preview: ${previewed}`);
}
const stored = serialize({ output: { file: png, filename: "test.png" } });
if (stored.includes('"data":[')) {
throw new Error(`serialize dumped bytes: ${stored.slice(0, 200)}`);
}
const display = toDisplayValue({
output: { file: png },
context: { file: png },
});
if (display.output.file.length !== png.length || display.context.file.truncated !== false) {
throw new Error(`toDisplayValue: ${JSON.stringify(display)}`);
}
if (Array.isArray(display.output.file.data)) {
throw new Error("toDisplayValue should not keep Buffer.data");
}
const dry = safeSerialize({ file: png, n: 1n });
if (dry.n !== "1" || Array.isArray(dry.file.data)) {
throw new Error(`safeSerialize: ${JSON.stringify(dry)}`);
}
const typed = safeSerialize({ file: new Uint8Array(png) });
if (typed.file.length !== png.length || typed.file.type !== "Buffer") {
throw new Error(`Uint8Array: ${JSON.stringify(typed)}`);
}
console.log("json-preview-smoke: ok");
@@ -10,3 +10,4 @@ scripts:
- test-send-gmail.yaml
- track.yaml
- rss-devto-to-ntfy.yaml
- test-minio.yaml
@@ -0,0 +1,20 @@
name: Test MinIO
scripts:
- script: fetch-binary.js
config:
outputVar: file
url: https://nsrb:error403@dav.0dev.web.id/ntfy/IyM9784UdG4S
filename: test.png
- script: s3.js
config:
action: write
endpoint: http://localhost:9000
bucket: default
forcePathStyle: true
accessKeyIdSecret: minio_user
secretAccessKeySecret: minio_pass
key: file.png
triggers:
- type: HTTP
method: POST
path: /new