@hasna/workflows 0.1.0 → 0.1.1

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/handlers.js CHANGED
@@ -1,5 +1,23 @@
1
1
  // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+ var __require = import.meta.require;
18
+
2
19
  // src/handlers.ts
20
+ import { createHash, timingSafeEqual } from "crypto";
3
21
  function jsonResponse(body, status = 200) {
4
22
  return new Response(JSON.stringify(body), {
5
23
  status,
@@ -13,10 +31,85 @@ function openApiDoc(service) {
13
31
  paths: {
14
32
  "/health": { get: { summary: "Service health", responses: { "200": { description: "ok" } } } },
15
33
  "/ready": { get: { summary: "Service readiness", responses: { "200": { description: "ok" } } } },
16
- "/version": { get: { summary: "Service version", responses: { "200": { description: "ok" } } } }
34
+ "/version": { get: { summary: "Service version", responses: { "200": { description: "ok" } } } },
35
+ "/trigger": {
36
+ post: {
37
+ summary: "Authenticated trigger: run a workflow graph to a terminal state",
38
+ security: [{ bearerAuth: [] }],
39
+ requestBody: {
40
+ content: {
41
+ "application/json": {
42
+ schema: {
43
+ type: "object",
44
+ required: ["graph"],
45
+ properties: {
46
+ graph: { type: "object", description: "the workflow graph" },
47
+ context: { type: "object", description: "optional run context" },
48
+ idempotencyKey: { type: "string", description: "re-run with the same key returns the same run" },
49
+ maxCycles: { type: "number", description: "bound on reap cycles (default 500, max 2000)" }
50
+ }
51
+ }
52
+ }
53
+ }
54
+ },
55
+ responses: {
56
+ "200": { description: "run summary" },
57
+ "401": { description: "missing or invalid Bearer token" },
58
+ "503": { description: "trigger not configured (WORKFLOWS_API_KEY unset)" }
59
+ }
60
+ }
61
+ }
62
+ },
63
+ components: {
64
+ securitySchemes: {
65
+ bearerAuth: { type: "http", scheme: "bearer" }
66
+ }
17
67
  }
18
68
  };
19
69
  }
70
+ function bearerMatches(authHeader, expected) {
71
+ if (!authHeader.startsWith("Bearer "))
72
+ return false;
73
+ const supplied = authHeader.slice("Bearer ".length);
74
+ if (supplied.length === 0)
75
+ return false;
76
+ const a = createHash("sha256").update(supplied).digest();
77
+ const b = createHash("sha256").update(expected).digest();
78
+ return timingSafeEqual(a, b);
79
+ }
80
+ async function handleTrigger(service, req) {
81
+ const expected = service.config.apiKey;
82
+ if (!expected) {
83
+ return jsonResponse({ error: "trigger_not_configured", message: "WORKFLOWS_API_KEY is not configured \u2014 set it to enable the authenticated trigger" }, 503);
84
+ }
85
+ const auth = req.headers.get("authorization") ?? "";
86
+ if (!bearerMatches(auth, expected)) {
87
+ return jsonResponse({ error: "unauthorized", message: "a valid Bearer token is required" }, 401);
88
+ }
89
+ let body;
90
+ try {
91
+ body = await req.json();
92
+ } catch {
93
+ return jsonResponse({ error: "invalid_json", message: "request body must be JSON" }, 400);
94
+ }
95
+ const record = body;
96
+ if (record.graph === null || typeof record.graph !== "object" || Array.isArray(record.graph)) {
97
+ return jsonResponse({ error: "graph_required", message: "body.graph must be a workflow graph object" }, 400);
98
+ }
99
+ const context = record.context ?? {};
100
+ if (typeof context !== "object" || Array.isArray(context)) {
101
+ return jsonResponse({ error: "invalid_context", message: "body.context must be an object" }, 400);
102
+ }
103
+ const maxCycles = typeof record.maxCycles === "number" ? record.maxCycles : undefined;
104
+ const idempotencyKey = typeof record.idempotencyKey === "string" ? record.idempotencyKey : undefined;
105
+ try {
106
+ const summary = await service.triggerRun(record.graph, context, { maxCycles, idempotencyKey });
107
+ return jsonResponse(summary);
108
+ } catch (err) {
109
+ const message = err instanceof Error ? err.message : String(err);
110
+ return jsonResponse({ error: "trigger_failed", message }, 400);
111
+ }
112
+ }
20
113
  function createRequestHandler(service) {
21
114
  return (req) => {
22
115
  const url = new URL(req.url);
@@ -27,6 +120,8 @@ function createRequestHandler(service) {
27
120
  return jsonResponse(service.ready());
28
121
  case "/version":
29
122
  return jsonResponse({ service: service.name, version: service.version });
123
+ case "/trigger":
124
+ return handleTrigger(service, req);
30
125
  case "/openapi.json":
31
126
  return jsonResponse(openApiDoc(service));
32
127
  default: