@bpmnkit/proxy 0.0.17 → 0.0.21
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/README.md +2 -0
- package/dist/adapters/copilot.js +1 -1
- package/dist/adapters/gemini.js +1 -1
- package/dist/index.js +459 -5
- package/dist/routes/run-history.js +307 -0
- package/dist/triggers/file-watcher.js +215 -0
- package/dist/triggers/index.js +18 -0
- package/dist/triggers/timer.js +239 -0
- package/dist/triggers/webhook.js +76 -0
- package/dist/worker-templates.js +612 -0
- package/dist/worker.js +166 -0
- package/dist/workers/cli.js +72 -0
- package/dist/workers/email.js +102 -0
- package/dist/workers/fs.js +90 -0
- package/dist/workers/http.js +46 -0
- package/dist/workers/js.js +21 -0
- package/dist/workers/llm.js +55 -0
- package/package.json +8 -4
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timer trigger — scans deployed processes for BPMN timer start events and
|
|
3
|
+
* fires `POST /v2/process-instances` at the appropriate times.
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* timeDuration ISO 8601 duration (PT1H → repeat every 1 hour)
|
|
7
|
+
* timeDate ISO 8601 datetime (2026-01-01T00:00:00Z → fire once)
|
|
8
|
+
* timeCycle ISO 8601 repeating (R/PT30M or R3/PT1H → repeat N or ∞ times)
|
|
9
|
+
*
|
|
10
|
+
* Last-fired timestamps are persisted to `~/.bpmnkit/timer-state.json` to
|
|
11
|
+
* survive proxy restarts.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { getActiveProfile, getAuthHeader } from "@bpmnkit/profiles";
|
|
17
|
+
// ── Persistence ───────────────────────────────────────────────────────────────
|
|
18
|
+
const STATE_PATH = join(homedir(), ".bpmnkit", "timer-state.json");
|
|
19
|
+
function loadState() {
|
|
20
|
+
try {
|
|
21
|
+
if (existsSync(STATE_PATH)) {
|
|
22
|
+
return JSON.parse(readFileSync(STATE_PATH, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// corrupt file — start fresh
|
|
27
|
+
}
|
|
28
|
+
return { lastFired: {} };
|
|
29
|
+
}
|
|
30
|
+
function saveState(state) {
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// best-effort
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// ── ISO 8601 duration parser ──────────────────────────────────────────────────
|
|
39
|
+
const DURATION_RE = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/;
|
|
40
|
+
function parseDurationMs(iso) {
|
|
41
|
+
const m = DURATION_RE.exec(iso);
|
|
42
|
+
if (!m)
|
|
43
|
+
return null;
|
|
44
|
+
const years = Number(m[1] ?? 0);
|
|
45
|
+
const months = Number(m[2] ?? 0);
|
|
46
|
+
const days = Number(m[3] ?? 0);
|
|
47
|
+
const hours = Number(m[4] ?? 0);
|
|
48
|
+
const minutes = Number(m[5] ?? 0);
|
|
49
|
+
const seconds = Number(m[6] ?? 0);
|
|
50
|
+
// Approximate: 1 year = 365 days, 1 month = 30 days
|
|
51
|
+
return ((years * 365 * 24 * 60 * 60 +
|
|
52
|
+
months * 30 * 24 * 60 * 60 +
|
|
53
|
+
days * 24 * 60 * 60 +
|
|
54
|
+
hours * 60 * 60 +
|
|
55
|
+
minutes * 60 +
|
|
56
|
+
seconds) *
|
|
57
|
+
1000);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse an ISO 8601 repeating interval: `R<n>/<duration>` or `R/<duration>`.
|
|
61
|
+
* Returns { durationMs, remaining } where remaining=undefined means infinite.
|
|
62
|
+
*/
|
|
63
|
+
function parseCycle(value) {
|
|
64
|
+
const m = /^R(\d*)\/(.+)$/.exec(value);
|
|
65
|
+
if (!m)
|
|
66
|
+
return null;
|
|
67
|
+
const durationMs = parseDurationMs(m[2] ?? "");
|
|
68
|
+
if (!durationMs)
|
|
69
|
+
return null;
|
|
70
|
+
const n = m[1] ? Number(m[1]) : undefined;
|
|
71
|
+
return { durationMs, remaining: n };
|
|
72
|
+
}
|
|
73
|
+
async function fetchDeployedProcesses(baseUrl, authHeader) {
|
|
74
|
+
const res = await fetch(`${baseUrl}/v2/process-definitions/search`, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ pageSize: 100 }),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
return [];
|
|
81
|
+
const data = (await res.json());
|
|
82
|
+
return data.items ?? [];
|
|
83
|
+
}
|
|
84
|
+
async function fetchProcessXml(baseUrl, authHeader, processId) {
|
|
85
|
+
const res = await fetch(`${baseUrl}/v2/process-definitions/${encodeURIComponent(processId)}/xml`, {
|
|
86
|
+
headers: { authorization: authHeader },
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
return null;
|
|
90
|
+
const data = (await res.json());
|
|
91
|
+
return data.bpmnXml ?? null;
|
|
92
|
+
}
|
|
93
|
+
/** Extract timer start event definitions from BPMN XML (simple regex approach). */
|
|
94
|
+
function extractTimerDefs(processId, xml) {
|
|
95
|
+
const defs = [];
|
|
96
|
+
// Match <timerEventDefinition> children: timeDuration, timeDate, timeCycle
|
|
97
|
+
const timerRe = /<timerEventDefinition[^>]*>[\s\S]*?<(timeDuration|timeDate|timeCycle)[^>]*>\s*([^<]+)\s*<\/\1>/g;
|
|
98
|
+
let m;
|
|
99
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex loop
|
|
100
|
+
while ((m = timerRe.exec(xml)) !== null) {
|
|
101
|
+
const kind = m[1];
|
|
102
|
+
const raw = m[2]?.trim() ?? "";
|
|
103
|
+
// Strip FEEL expression wrapper if present (e.g. = "PT1H" or = PT1H)
|
|
104
|
+
const value = raw
|
|
105
|
+
.replace(/^=\s*["']?/, "")
|
|
106
|
+
.replace(/["']$/, "")
|
|
107
|
+
.trim();
|
|
108
|
+
if (!value)
|
|
109
|
+
continue;
|
|
110
|
+
if (kind === "timeDuration") {
|
|
111
|
+
defs.push({ processId, type: "duration", value });
|
|
112
|
+
}
|
|
113
|
+
else if (kind === "timeDate") {
|
|
114
|
+
defs.push({ processId, type: "date", value });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
const cycle = parseCycle(value);
|
|
118
|
+
if (cycle) {
|
|
119
|
+
defs.push({ processId, type: "cycle", value, remaining: cycle.remaining });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return defs;
|
|
124
|
+
}
|
|
125
|
+
// ── Firing ────────────────────────────────────────────────────────────────────
|
|
126
|
+
async function fireProcess(baseUrl, authHeader, processId) {
|
|
127
|
+
const res = await fetch(`${baseUrl}/v2/process-instances`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
130
|
+
body: JSON.stringify({ processDefinitionId: processId, variables: {} }),
|
|
131
|
+
});
|
|
132
|
+
if (res.ok) {
|
|
133
|
+
const data = (await res.json());
|
|
134
|
+
console.log(`[trigger:timer] fired ${processId} → instance ${data.processInstanceKey ?? "?"}`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
console.error(`[trigger:timer] failed to start ${processId}: ${res.status}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ── Scheduler ─────────────────────────────────────────────────────────────────
|
|
141
|
+
let timerDefs = [];
|
|
142
|
+
const timerState = loadState();
|
|
143
|
+
async function scanAndSchedule() {
|
|
144
|
+
const profile = getActiveProfile();
|
|
145
|
+
if (!profile?.config.baseUrl)
|
|
146
|
+
return;
|
|
147
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
148
|
+
if (!baseUrl.startsWith("http"))
|
|
149
|
+
return;
|
|
150
|
+
let authHeader = "";
|
|
151
|
+
try {
|
|
152
|
+
authHeader = await getAuthHeader(profile.config);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// proceed unauthenticated
|
|
156
|
+
}
|
|
157
|
+
let processes;
|
|
158
|
+
try {
|
|
159
|
+
processes = await fetchDeployedProcesses(baseUrl, authHeader);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const defs = [];
|
|
165
|
+
for (const proc of processes) {
|
|
166
|
+
const xml = await fetchProcessXml(baseUrl, authHeader, proc.processDefinitionId);
|
|
167
|
+
if (xml) {
|
|
168
|
+
defs.push(...extractTimerDefs(proc.processDefinitionId, xml));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
timerDefs = defs;
|
|
172
|
+
if (defs.length > 0) {
|
|
173
|
+
console.log(`[trigger:timer] tracking ${defs.length} timer(s)`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function tickTimers() {
|
|
177
|
+
const profile = getActiveProfile();
|
|
178
|
+
if (!profile?.config.baseUrl)
|
|
179
|
+
return;
|
|
180
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
181
|
+
if (!baseUrl.startsWith("http"))
|
|
182
|
+
return;
|
|
183
|
+
let authHeader = "";
|
|
184
|
+
try {
|
|
185
|
+
authHeader = await getAuthHeader(profile.config);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
// proceed unauthenticated
|
|
189
|
+
}
|
|
190
|
+
const now = Date.now();
|
|
191
|
+
for (const def of timerDefs) {
|
|
192
|
+
const lastFired = timerState.lastFired[def.processId] ?? 0;
|
|
193
|
+
if (def.type === "date") {
|
|
194
|
+
const target = new Date(def.value).getTime();
|
|
195
|
+
if (Number.isNaN(target))
|
|
196
|
+
continue;
|
|
197
|
+
if (now >= target && lastFired < target) {
|
|
198
|
+
await fireProcess(baseUrl, authHeader, def.processId);
|
|
199
|
+
timerState.lastFired[def.processId] = now;
|
|
200
|
+
saveState(timerState);
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (def.type === "duration" || def.type === "cycle") {
|
|
205
|
+
const durationMs = def.type === "duration"
|
|
206
|
+
? parseDurationMs(def.value)
|
|
207
|
+
: (parseCycle(def.value)?.durationMs ?? null);
|
|
208
|
+
if (!durationMs || durationMs <= 0)
|
|
209
|
+
continue;
|
|
210
|
+
// Skip if this process has exhausted its repeat count
|
|
211
|
+
if (def.remaining !== undefined && def.remaining <= 0)
|
|
212
|
+
continue;
|
|
213
|
+
const due = lastFired + durationMs;
|
|
214
|
+
if (now >= due) {
|
|
215
|
+
await fireProcess(baseUrl, authHeader, def.processId);
|
|
216
|
+
timerState.lastFired[def.processId] = now;
|
|
217
|
+
if (def.remaining !== undefined)
|
|
218
|
+
def.remaining--;
|
|
219
|
+
saveState(timerState);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
225
|
+
const SCAN_INTERVAL_MS = 60_000;
|
|
226
|
+
const TICK_INTERVAL_MS = 5_000;
|
|
227
|
+
export function startTimerTrigger() {
|
|
228
|
+
// Initial scan
|
|
229
|
+
void scanAndSchedule();
|
|
230
|
+
// Re-scan every 60 s for newly deployed processes
|
|
231
|
+
setInterval(() => {
|
|
232
|
+
void scanAndSchedule();
|
|
233
|
+
}, SCAN_INTERVAL_MS);
|
|
234
|
+
// Tick every 5 s to check for due timers
|
|
235
|
+
setInterval(() => {
|
|
236
|
+
void tickTimers();
|
|
237
|
+
}, TICK_INTERVAL_MS);
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=timer.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getActiveProfile, getAuthHeader } from "@bpmnkit/profiles";
|
|
2
|
+
// ── Request matcher ───────────────────────────────────────────────────────────
|
|
3
|
+
/** Returns the processId if the request matches `POST /webhooks/:processId`. */
|
|
4
|
+
export function matchWebhookRoute(req) {
|
|
5
|
+
if (req.method !== "POST")
|
|
6
|
+
return null;
|
|
7
|
+
const m = /^\/webhooks\/([^/?]+)/.exec(req.url ?? "");
|
|
8
|
+
if (!m || !m[1])
|
|
9
|
+
return null;
|
|
10
|
+
return { processId: m[1] };
|
|
11
|
+
}
|
|
12
|
+
// ── Handler ───────────────────────────────────────────────────────────────────
|
|
13
|
+
function readBody(req) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const chunks = [];
|
|
16
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
17
|
+
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
18
|
+
req.on("error", reject);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function sendJson(res, status, body) {
|
|
22
|
+
const payload = JSON.stringify(body);
|
|
23
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
24
|
+
res.end(payload);
|
|
25
|
+
}
|
|
26
|
+
export async function handleWebhook(req, res, processId) {
|
|
27
|
+
// Token guard (optional)
|
|
28
|
+
const token = process.env.WEBHOOK_TOKEN;
|
|
29
|
+
if (token) {
|
|
30
|
+
const auth = req.headers.authorization ?? "";
|
|
31
|
+
if (auth !== `Bearer ${token}`) {
|
|
32
|
+
sendJson(res, 401, { error: "Unauthorized" });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Parse body
|
|
37
|
+
let variables = {};
|
|
38
|
+
try {
|
|
39
|
+
const raw = await readBody(req);
|
|
40
|
+
if (raw.trim()) {
|
|
41
|
+
variables = JSON.parse(raw);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
sendJson(res, 400, { error: "Invalid JSON body" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Start process instance via reebe
|
|
49
|
+
const profile = getActiveProfile();
|
|
50
|
+
if (!profile?.config.baseUrl) {
|
|
51
|
+
sendJson(res, 503, { error: "No active reebe profile" });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const baseUrl = profile.config.baseUrl.replace(/\/$/, "");
|
|
55
|
+
let authHeader = "";
|
|
56
|
+
try {
|
|
57
|
+
authHeader = await getAuthHeader(profile.config);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// proceed without auth
|
|
61
|
+
}
|
|
62
|
+
const startRes = await fetch(`${baseUrl}/v2/process-instances`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { authorization: authHeader, "content-type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ processDefinitionId: processId, variables }),
|
|
66
|
+
});
|
|
67
|
+
if (!startRes.ok) {
|
|
68
|
+
const text = await startRes.text();
|
|
69
|
+
sendJson(res, startRes.status, { error: text });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const data = (await startRes.json());
|
|
73
|
+
console.log(`[trigger:webhook] started ${processId} → instance ${data.processInstanceKey ?? "?"}`);
|
|
74
|
+
sendJson(res, 200, { processInstanceKey: data.processInstanceKey });
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=webhook.js.map
|