feat(server): enhance secret management and logging functionality

- Added SCRUNNER_SECRETS_KEY to README as a required variable for production.
- Implemented redaction of sensitive information in logs across various components.
- Enhanced script execution functions to include an owner parameter for better secret management.
- Introduced a secrets API in the script sandbox for retrieving and managing secrets.
- Updated UI components to support owner selection for script execution and secret management.
This commit is contained in:
2026-08-14 22:11:03 +07:00
parent 94aa5b2efa
commit 3e4d5f443d
19 changed files with 886 additions and 39 deletions
+15 -10
View File
@@ -1,4 +1,5 @@
import pino from "pino";
import { redactString } from "../../secret-value.js";
const LEVEL_TO_NUM = {
trace: 10,
@@ -22,7 +23,9 @@ export function createDryRunLogger() {
write(line) {
let record;
try {
record = JSON.parse(typeof line === "string" ? line : String(line));
record = JSON.parse(
redactString(typeof line === "string" ? line : String(line)),
);
} catch {
return;
}
@@ -43,7 +46,7 @@ export function createDryRunLogger() {
logs.push({
ts,
level,
msg,
msg: typeof msg === "string" ? redactString(msg) : msg,
payload: Object.keys(rest).length ? rest : null,
});
},
@@ -60,14 +63,16 @@ export function safeSerialize(value) {
const seen = new WeakSet();
try {
return JSON.parse(
JSON.stringify(value, (_key, v) => {
if (typeof v === "bigint") return v.toString();
if (typeof v === "object" && v !== null) {
if (seen.has(v)) return "[Circular]";
seen.add(v);
}
return v;
}),
redactString(
JSON.stringify(value, (_key, v) => {
if (typeof v === "bigint") return v.toString();
if (typeof v === "object" && v !== null) {
if (seen.has(v)) return "[Circular]";
seen.add(v);
}
return v;
}),
),
);
} catch (err) {
return {
+11 -1
View File
@@ -85,13 +85,22 @@ export default function scriptsPluginFactory(registry) {
return reply.code(err.statusCode ?? 400).send({ error: err.message });
}
const body = /** @type {{ content?: string, data?: unknown, config?: unknown }} */ (
const body = /** @type {{ content?: string, data?: unknown, config?: unknown, owner?: string }} */ (
req.body ?? {}
);
if (typeof body.content !== "string") {
return reply.code(400).send({ error: "content is required" });
}
let owner = "default";
if (body.owner != null && body.owner !== "") {
try {
owner = fsStore.assertOwner(String(body.owner));
} catch (err) {
return reply.code(err.statusCode ?? 400).send({ error: err.message });
}
}
const ctx = {
data: body.data ?? null,
config: body.config ?? null,
@@ -104,6 +113,7 @@ export default function scriptsPluginFactory(registry) {
const { fn, meta, metaError } = instantiateScriptSource(name, body.content, {
log,
workflowName: "dry-run",
owner,
});
const output = await fn(ctx);
return {
+67
View File
@@ -0,0 +1,67 @@
import * as fsStore from "../../fs-store.js";
import {
assertSecretName,
deleteSecret,
getSecretById,
listSecrets,
upsertSecret,
} from "../../secrets-store.js";
import { MIN_SECRET_LENGTH } from "../../secret-value.js";
/**
* @param {import("fastify").FastifyInstance} fastify
*/
export default async function secretsPlugin(fastify) {
fastify.addHook("onRequest", fastify.requireAdmin);
fastify.get("/secrets", async (req, reply) => {
const q = /** @type {{ owner?: string }} */ (req.query ?? {});
try {
const owner = q.owner ? fsStore.assertOwner(q.owner) : undefined;
const secrets = await listSecrets({ owner });
return { secrets };
} catch (err) {
return reply.code(err.statusCode ?? 500).send({ error: err.message });
}
});
fastify.put("/secrets", async (req, reply) => {
const body = /** @type {{ owner?: string, name?: string, value?: string }} */ (
req.body ?? {}
);
try {
fsStore.assertOwner(String(body.owner ?? ""));
assertSecretName(String(body.name ?? ""));
} catch (err) {
return reply.code(err.statusCode ?? 400).send({ error: err.message });
}
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` });
}
try {
const secret = await upsertSecret({
owner: String(body.owner),
name: String(body.name),
value,
});
return reply.send({ secret });
} catch (err) {
return reply.code(err.statusCode ?? 500).send({ error: err.message });
}
});
fastify.delete("/secrets/:id", async (req, reply) => {
const { id } = /** @type {{ id: string }} */ (req.params);
const existing = await getSecretById(id);
if (!existing) {
return reply.code(404).send({ error: "secret not found" });
}
await deleteSecret(id);
return { ok: true };
});
}