@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 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 name) a workflow from its bundled program source. The
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(workflowName: string, opts?: {
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 name) a workflow from its bundled program source. The
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(workflowName, opts = {}) {
109
+ startRun(slug, opts = {}) {
110
110
  this.assertOpen();
111
- const workflow = this.store.getWorkflow(workflowName);
111
+ const workflow = this.store.getWorkflow(slug);
112
112
  if (workflow === null) {
113
- throw new EngineError("NOT_FOUND", `Workflow "${workflowName}" is not deployed on this engine.`);
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,
@@ -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 name.`);
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/:name/runs — start a manual run; 201 with the queued row. */
7
- export declare function handleStartRun(ctx: RouteContext, workflowName: string): Promise<void>;
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 workflowName = ctx.url.searchParams.get("workflow");
47
- if (workflowName !== null) {
48
- const workflow = ctx.engine.store.getWorkflow(workflowName);
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 "${workflowName}" is not deployed on this engine.`);
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/:name/runs — start a manual run; 201 with the queued row. */
64
- export async function handleStartRun(ctx, workflowName) {
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(workflowName, {
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, workflowName: string, triggerIndexRaw: string): Promise<void>;
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 name upper-cased with `-` → `_`. Missing variable = 503 (fail
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, workflowName, triggerIndexRaw) {
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/${workflowName}/${triggerIndexRaw}.`);
20
- const workflow = ctx.engine.store.getWorkflow(workflowName);
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, workflowName);
29
+ authorizeToken(ctx.req, slug);
30
30
  else
31
- authorizeSignature(ctx.req, workflowName, rawBody);
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(workflowName, { input, triggerKind: "webhook" });
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 name upper-cased, hyphens → underscores. */
37
- function webhookEnvVarName(kind, workflowName) {
38
- return `BOARDWALK_WEBHOOK_${kind}__${workflowName.toUpperCase().replaceAll("-", "_")}`;
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, workflowName) {
46
- const name = webhookEnvVarName(kind, workflowName);
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 "${workflowName}" is not configured on this server.`, `Set the environment variable ${name} and restart the server.`);
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, workflowName) {
58
- const expected = requiredCredential("TOKEN", workflowName);
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, workflowName, rawBody) {
66
- const secret = requiredCredential("SECRET", workflowName);
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();
@@ -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 name (re-boot re-syncs the dir into the store);
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.5",
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": {