@boardwalk-labs/engine 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +2 -2
- package/dist/engine.js +4 -4
- package/dist/run/supervisor.js +1 -1
- package/dist/server/routes/api.d.ts +2 -2
- package/dist/server/routes/api.js +7 -7
- package/dist/server/routes/hooks.d.ts +1 -1
- package/dist/server/routes/hooks.js +17 -17
- package/dist/server_main.js +1 -1
- package/package.json +1 -1
package/dist/engine.d.ts
CHANGED
|
@@ -65,7 +65,7 @@ export declare class Engine {
|
|
|
65
65
|
/** Subscribe to every stamped run event (feeds SSE, the CLI renderer, the log UI). */
|
|
66
66
|
onEvent(listener: (row: EventRow) => void): () => void;
|
|
67
67
|
/**
|
|
68
|
-
* Deploy (or redeploy, by manifest
|
|
68
|
+
* Deploy (or redeploy, by manifest slug) a workflow from its bundled program source. The
|
|
69
69
|
* manifest is DERIVED from the program's pure-literal `meta` — the program file is the
|
|
70
70
|
* author's source of truth, manifest drift is impossible by construction.
|
|
71
71
|
*/
|
|
@@ -74,7 +74,7 @@ export declare class Engine {
|
|
|
74
74
|
* Queue a run and dispatch it through the concurrency gate. Returns the queued row
|
|
75
75
|
* immediately; `waitForRun` for the terminal row.
|
|
76
76
|
*/
|
|
77
|
-
startRun(
|
|
77
|
+
startRun(slug: string, opts?: {
|
|
78
78
|
input?: JsonValue;
|
|
79
79
|
triggerKind?: TriggerKind;
|
|
80
80
|
}): RunRow;
|
package/dist/engine.js
CHANGED
|
@@ -75,7 +75,7 @@ export class Engine {
|
|
|
75
75
|
return this.supervisor.onEvent(listener);
|
|
76
76
|
}
|
|
77
77
|
/**
|
|
78
|
-
* Deploy (or redeploy, by manifest
|
|
78
|
+
* Deploy (or redeploy, by manifest slug) a workflow from its bundled program source. The
|
|
79
79
|
* manifest is DERIVED from the program's pure-literal `meta` — the program file is the
|
|
80
80
|
* author's source of truth, manifest drift is impossible by construction.
|
|
81
81
|
*/
|
|
@@ -106,11 +106,11 @@ export class Engine {
|
|
|
106
106
|
* Queue a run and dispatch it through the concurrency gate. Returns the queued row
|
|
107
107
|
* immediately; `waitForRun` for the terminal row.
|
|
108
108
|
*/
|
|
109
|
-
startRun(
|
|
109
|
+
startRun(slug, opts = {}) {
|
|
110
110
|
this.assertOpen();
|
|
111
|
-
const workflow = this.store.getWorkflow(
|
|
111
|
+
const workflow = this.store.getWorkflow(slug);
|
|
112
112
|
if (workflow === null) {
|
|
113
|
-
throw new EngineError("NOT_FOUND", `Workflow "${
|
|
113
|
+
throw new EngineError("NOT_FOUND", `Workflow "${slug}" is not deployed on this engine.`);
|
|
114
114
|
}
|
|
115
115
|
const { run } = this.store.createRun({
|
|
116
116
|
workflowId: workflow.id,
|
package/dist/run/supervisor.js
CHANGED
|
@@ -510,7 +510,7 @@ export class RunSupervisor {
|
|
|
510
510
|
startChildRun(parentRunId, slug, input, idempotencyKey) {
|
|
511
511
|
const target = this.store.getWorkflow(slug);
|
|
512
512
|
if (target === null) {
|
|
513
|
-
throw new EngineError("NOT_FOUND", `workflows.call target "${slug}" is not deployed on this engine.`, `Deploy it first — the engine only runs workflows it knows by
|
|
513
|
+
throw new EngineError("NOT_FOUND", `workflows.call target "${slug}" is not deployed on this engine.`, `Deploy it first — the engine only runs workflows it knows by slug.`);
|
|
514
514
|
}
|
|
515
515
|
// Crossed the JSON IPC channel, but narrow instead of assuming — and
|
|
516
516
|
// the canonical default key requires a JSON tree anyway.
|
|
@@ -3,8 +3,8 @@ import type { RouteContext } from "./router.js";
|
|
|
3
3
|
export declare function handleListWorkflows(ctx: RouteContext): void;
|
|
4
4
|
/** GET /api/runs?workflow=&status=&limit=&offset= — newest first, full RunRow shape. */
|
|
5
5
|
export declare function handleListRuns(ctx: RouteContext): void;
|
|
6
|
-
/** POST /api/workflows/:
|
|
7
|
-
export declare function handleStartRun(ctx: RouteContext,
|
|
6
|
+
/** POST /api/workflows/:slug/runs — start a manual run; 201 with the queued row. */
|
|
7
|
+
export declare function handleStartRun(ctx: RouteContext, slug: string): Promise<void>;
|
|
8
8
|
/** GET /api/runs/:id */
|
|
9
9
|
export declare function handleGetRun(ctx: RouteContext, runId: string): void;
|
|
10
10
|
/**
|
|
@@ -43,11 +43,11 @@ export function handleListRuns(ctx) {
|
|
|
43
43
|
limit: parseNonNegativeInt(ctx.url, "limit", DEFAULT_RUNS_LIMIT),
|
|
44
44
|
offset: parseNonNegativeInt(ctx.url, "offset", 0),
|
|
45
45
|
};
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
const workflow = ctx.engine.store.getWorkflow(
|
|
46
|
+
const slug = ctx.url.searchParams.get("workflow");
|
|
47
|
+
if (slug !== null) {
|
|
48
|
+
const workflow = ctx.engine.store.getWorkflow(slug);
|
|
49
49
|
if (workflow === null) {
|
|
50
|
-
throw new HttpError(404, "NOT_FOUND", `Workflow "${
|
|
50
|
+
throw new HttpError(404, "NOT_FOUND", `Workflow "${slug}" is not deployed on this engine.`);
|
|
51
51
|
}
|
|
52
52
|
filter.workflowId = workflow.id;
|
|
53
53
|
}
|
|
@@ -60,12 +60,12 @@ export function handleListRuns(ctx) {
|
|
|
60
60
|
}
|
|
61
61
|
sendJson(ctx.res, 200, { runs: ctx.engine.store.listRuns(filter) });
|
|
62
62
|
}
|
|
63
|
-
/** POST /api/workflows/:
|
|
64
|
-
export async function handleStartRun(ctx,
|
|
63
|
+
/** POST /api/workflows/:slug/runs — start a manual run; 201 with the queued row. */
|
|
64
|
+
export async function handleStartRun(ctx, slug) {
|
|
65
65
|
const raw = await readBody(ctx.req, MAX_BODY_BYTES);
|
|
66
66
|
// An empty body means "no input" — the curl-without-data ergonomics of a run-now button.
|
|
67
67
|
const body = raw.length === 0 ? {} : parseJsonBody(raw, startRunBodySchema, "run-start body");
|
|
68
|
-
const run = ctx.engine.startRun(
|
|
68
|
+
const run = ctx.engine.startRun(slug, {
|
|
69
69
|
triggerKind: "manual",
|
|
70
70
|
...(body.input !== undefined ? { input: body.input } : {}),
|
|
71
71
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RouteContext } from "./router.js";
|
|
2
|
-
export declare function handleWebhook(ctx: RouteContext,
|
|
2
|
+
export declare function handleWebhook(ctx: RouteContext, slug: string, triggerIndexRaw: string): Promise<void>;
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
// token auth: BOARDWALK_WEBHOOK_TOKEN__<NAME> vs `Authorization: Bearer <token>`
|
|
6
6
|
// signature auth: BOARDWALK_WEBHOOK_SECRET__<NAME> vs `X-Boardwalk-Signature: sha256=<hex>`
|
|
7
7
|
// (HMAC-SHA256 over the raw request body)
|
|
8
|
-
// where <NAME> is the workflow
|
|
8
|
+
// where <NAME> is the workflow slug upper-cased with `-` → `_`. Missing variable = 503 (fail
|
|
9
9
|
// closed, hint names the variable); bad credential = 401. These are server config, not
|
|
10
10
|
// workflow secrets, so they resolve from process.env — never from the engine's env map.
|
|
11
11
|
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
12
12
|
import { HttpError, MAX_BODY_BYTES, jsonValueSchema, parseJsonBody, readBody, sendJson, } from "../http.js";
|
|
13
|
-
export async function handleWebhook(ctx,
|
|
13
|
+
export async function handleWebhook(ctx, slug, triggerIndexRaw) {
|
|
14
14
|
// The raw body is read up front: signature auth signs the exact bytes on the wire, before
|
|
15
15
|
// any JSON parsing can normalize them away.
|
|
16
16
|
const rawBody = await readBody(ctx.req, MAX_BODY_BYTES);
|
|
17
17
|
// One identical 404 for "no workflow", "no such trigger index", and "not a webhook
|
|
18
18
|
// trigger" — an unauthenticated caller learns nothing about what is deployed here.
|
|
19
|
-
const notFound = new HttpError(404, "NOT_FOUND", `No webhook trigger at /hooks/${
|
|
20
|
-
const workflow = ctx.engine.store.getWorkflow(
|
|
19
|
+
const notFound = new HttpError(404, "NOT_FOUND", `No webhook trigger at /hooks/${slug}/${triggerIndexRaw}.`);
|
|
20
|
+
const workflow = ctx.engine.store.getWorkflow(slug);
|
|
21
21
|
if (workflow === null)
|
|
22
22
|
throw notFound;
|
|
23
23
|
if (!/^\d+$/.test(triggerIndexRaw))
|
|
@@ -26,27 +26,27 @@ export async function handleWebhook(ctx, workflowName, triggerIndexRaw) {
|
|
|
26
26
|
if (trigger === undefined || trigger.kind !== "webhook")
|
|
27
27
|
throw notFound;
|
|
28
28
|
if (trigger.auth === "token")
|
|
29
|
-
authorizeToken(ctx.req,
|
|
29
|
+
authorizeToken(ctx.req, slug);
|
|
30
30
|
else
|
|
31
|
-
authorizeSignature(ctx.req,
|
|
31
|
+
authorizeSignature(ctx.req, slug, rawBody);
|
|
32
32
|
const input = rawBody.length === 0 ? null : parseJsonBody(rawBody, jsonValueSchema, "webhook payload");
|
|
33
|
-
const run = ctx.engine.startRun(
|
|
33
|
+
const run = ctx.engine.startRun(slug, { input, triggerKind: "webhook" });
|
|
34
34
|
sendJson(ctx.res, 201, { run: { id: run.id, status: run.status } });
|
|
35
35
|
}
|
|
36
|
-
/** `BOARDWALK_WEBHOOK_<kind>__<NAME>`: the workflow
|
|
37
|
-
function webhookEnvVarName(kind,
|
|
38
|
-
return `BOARDWALK_WEBHOOK_${kind}__${
|
|
36
|
+
/** `BOARDWALK_WEBHOOK_<kind>__<NAME>`: the workflow slug upper-cased, hyphens → underscores. */
|
|
37
|
+
function webhookEnvVarName(kind, slug) {
|
|
38
|
+
return `BOARDWALK_WEBHOOK_${kind}__${slug.toUpperCase().replaceAll("-", "_")}`;
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
41
|
* Read the trigger's credential from the server environment, failing CLOSED when unset: a
|
|
42
42
|
* webhook that nobody configured must never become an open trigger. Read lazily per request
|
|
43
43
|
* so an operator can fix the environment without redeploying workflows.
|
|
44
44
|
*/
|
|
45
|
-
function requiredCredential(kind,
|
|
46
|
-
const name = webhookEnvVarName(kind,
|
|
45
|
+
function requiredCredential(kind, slug) {
|
|
46
|
+
const name = webhookEnvVarName(kind, slug);
|
|
47
47
|
const value = process.env[name];
|
|
48
48
|
if (value === undefined || value === "") {
|
|
49
|
-
throw new HttpError(503, "WEBHOOK_UNCONFIGURED", `Webhook auth for workflow "${
|
|
49
|
+
throw new HttpError(503, "WEBHOOK_UNCONFIGURED", `Webhook auth for workflow "${slug}" is not configured on this server.`, `Set the environment variable ${name} and restart the server.`);
|
|
50
50
|
}
|
|
51
51
|
return value;
|
|
52
52
|
}
|
|
@@ -54,16 +54,16 @@ function requiredCredential(kind, workflowName) {
|
|
|
54
54
|
function unauthorized() {
|
|
55
55
|
return new HttpError(401, "UNAUTHORIZED", "Invalid webhook credentials.");
|
|
56
56
|
}
|
|
57
|
-
function authorizeToken(req,
|
|
58
|
-
const expected = requiredCredential("TOKEN",
|
|
57
|
+
function authorizeToken(req, slug) {
|
|
58
|
+
const expected = requiredCredential("TOKEN", slug);
|
|
59
59
|
const header = req.headers.authorization;
|
|
60
60
|
if (header === undefined || !header.startsWith("Bearer "))
|
|
61
61
|
throw unauthorized();
|
|
62
62
|
if (!constantTimeEquals(header.slice("Bearer ".length), expected))
|
|
63
63
|
throw unauthorized();
|
|
64
64
|
}
|
|
65
|
-
function authorizeSignature(req,
|
|
66
|
-
const secret = requiredCredential("SECRET",
|
|
65
|
+
function authorizeSignature(req, slug, rawBody) {
|
|
66
|
+
const secret = requiredCredential("SECRET", slug);
|
|
67
67
|
const header = req.headers["x-boardwalk-signature"];
|
|
68
68
|
if (typeof header !== "string")
|
|
69
69
|
throw unauthorized();
|
package/dist/server_main.js
CHANGED
|
@@ -122,7 +122,7 @@ export function loadServerConfig(env) {
|
|
|
122
122
|
/**
|
|
123
123
|
* Deploy every built workflow in `dir` on boot — the self-host deploy mechanism. Each `.mjs`/`.js`
|
|
124
124
|
* file is one workflow's program (single-file, `@boardwalk-labs/workflow` external — what
|
|
125
|
-
* `boardwalk build` emits). Idempotent by manifest
|
|
125
|
+
* `boardwalk build` emits). Idempotent by manifest slug (re-boot re-syncs the dir into the store);
|
|
126
126
|
* a removed file leaves its last-deployed workflow in place (no un-deploy in v0). A missing dir is
|
|
127
127
|
* fine (an operator may deploy by other means); a bad file is logged and skipped, never fatal.
|
|
128
128
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boardwalk-labs/engine",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "The Boardwalk single-node engine: cron scheduling, run lifecycle, SQLite state, and the local run log. Powers `boardwalk dev` and the self-hosted server.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|