@wooksjs/event-wf 0.7.7 → 0.7.9
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/index.cjs +468 -16
- package/dist/index.d.ts +192 -6
- package/dist/index.mjs +453 -13
- package/package.json +20 -14
- package/scripts/setup-skills.js +0 -77
- package/skills/wooksjs-event-wf/SKILL.md +0 -42
- package/skills/wooksjs-event-wf/core.md +0 -466
- package/skills/wooksjs-event-wf/event-core.md +0 -449
- package/skills/wooksjs-event-wf/workflows.md +0 -570
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { createEventContext, current, defineEventKind, key, slot, useLogger, useRouteParams } from "@wooksjs/event-core";
|
|
1
|
+
import { createEventContext, current, defineEventKind, defineWook, key, slot, useLogger, useRouteParams } from "@wooksjs/event-core";
|
|
2
|
+
import { useCookies, useResponse, useUrlParams } from "@wooksjs/event-http";
|
|
3
|
+
import { useBody } from "@wooksjs/http-body";
|
|
4
|
+
import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "node:crypto";
|
|
2
5
|
import { StepRetriableError, Workflow, createStep } from "@prostojs/wf";
|
|
3
6
|
import { WooksAdapterBase } from "wooks";
|
|
4
7
|
|
|
@@ -23,17 +26,14 @@ const resumeKey = key("wf.resume");
|
|
|
23
26
|
* const stepInput = input<MyInput>()
|
|
24
27
|
* ```
|
|
25
28
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
resume: c.get(resumeKey)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
29
|
+
const useWfState = defineWook((c) => ({
|
|
30
|
+
ctx: () => c.get(wfKind.keys.inputContext),
|
|
31
|
+
input: () => c.get(wfKind.keys.input),
|
|
32
|
+
schemaId: c.get(wfKind.keys.schemaId),
|
|
33
|
+
stepId: () => c.get(wfKind.keys.stepId),
|
|
34
|
+
indexes: () => c.get(wfKind.keys.indexes),
|
|
35
|
+
resume: c.get(resumeKey)
|
|
36
|
+
}));
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region packages/event-wf/src/event-wf.ts
|
|
@@ -52,6 +52,446 @@ function resumeWfContext(options, seeds, fn) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region packages/event-wf/src/outlets/outlet-context.ts
|
|
57
|
+
/** Registered outlet handlers, keyed by name */
|
|
58
|
+
const outletsRegistryKey = key("wf.outlets.registry");
|
|
59
|
+
/** Active state strategy for current request */
|
|
60
|
+
const stateStrategyKey = key("wf.outlets.stateStrategy");
|
|
61
|
+
/** Finished response set by workflow steps */
|
|
62
|
+
const wfFinishedKey = key("wf.outlets.finished");
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region packages/event-wf/src/outlets/use-wf-outlet.ts
|
|
66
|
+
/**
|
|
67
|
+
* Composable for accessing outlet infrastructure from within workflow steps.
|
|
68
|
+
*
|
|
69
|
+
* Most steps don't need this — they just return `outletHttp(form)` or
|
|
70
|
+
* `outletEmail(to, template)`. This composable is for advanced cases
|
|
71
|
+
* where steps need to inspect or modify outlet state directly.
|
|
72
|
+
*/
|
|
73
|
+
const useWfOutlet = defineWook((ctx) => ({
|
|
74
|
+
getStateStrategy: () => ctx.get(stateStrategyKey),
|
|
75
|
+
getOutlets: () => ctx.get(outletsRegistryKey),
|
|
76
|
+
getOutlet: (name) => ctx.get(outletsRegistryKey)?.get(name) ?? null
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region packages/event-wf/src/outlets/use-wf-finished.ts
|
|
81
|
+
/**
|
|
82
|
+
* Composable to set the completion response for a finished workflow.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* // Redirect after login
|
|
87
|
+
* useWfFinished().set({ type: 'redirect', value: '/dashboard' })
|
|
88
|
+
*
|
|
89
|
+
* // Return data
|
|
90
|
+
* useWfFinished().set({ type: 'data', value: { success: true } })
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
const useWfFinished = defineWook((ctx) => ({
|
|
94
|
+
set: (response) => ctx.set(wfFinishedKey, response),
|
|
95
|
+
get: () => ctx.has(wfFinishedKey) ? ctx.get(wfFinishedKey) : void 0
|
|
96
|
+
}));
|
|
97
|
+
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region packages/event-wf/src/outlets/trigger.ts
|
|
100
|
+
const DEFAULT_CONSUME_TOKEN = { email: true };
|
|
101
|
+
/**
|
|
102
|
+
* Handle an HTTP request that starts or resumes a workflow.
|
|
103
|
+
*
|
|
104
|
+
* Reads wfs (state token) and wfid (workflow ID) from request body, query params,
|
|
105
|
+
* or cookies — configurable via `config.token`. On workflow pause, persists state
|
|
106
|
+
* and dispatches to the named outlet. On finish, returns the finished response.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* // In a wooks HTTP handler:
|
|
111
|
+
* app.post('/workflow', () => handleWfOutletRequest(config, deps))
|
|
112
|
+
*
|
|
113
|
+
* // Better — use createOutletHandler():
|
|
114
|
+
* const handle = createOutletHandler(wfApp)
|
|
115
|
+
* app.post('/workflow', () => handle(config))
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
async function handleWfOutletRequest(config, deps) {
|
|
119
|
+
const tok = config.token ?? {};
|
|
120
|
+
const tokenName = tok.name ?? "wfs";
|
|
121
|
+
const tokenRead = tok.read ?? [
|
|
122
|
+
"body",
|
|
123
|
+
"query",
|
|
124
|
+
"cookie"
|
|
125
|
+
];
|
|
126
|
+
const tokenWrite = tok.write ?? "body";
|
|
127
|
+
const wfidName = config.wfidName ?? "wfid";
|
|
128
|
+
const ctx = current();
|
|
129
|
+
const registry = new Map(config.outlets.map((o) => [o.name, o]));
|
|
130
|
+
ctx.set(outletsRegistryKey, registry);
|
|
131
|
+
ctx.set(wfFinishedKey, void 0);
|
|
132
|
+
const { parseBody } = useBody();
|
|
133
|
+
const { params } = useUrlParams();
|
|
134
|
+
const { getCookie } = useCookies();
|
|
135
|
+
const response = useResponse();
|
|
136
|
+
const body = await parseBody().catch(() => void 0);
|
|
137
|
+
const queryParams = params();
|
|
138
|
+
let token;
|
|
139
|
+
for (const source of tokenRead) {
|
|
140
|
+
if (source === "body") token = body?.[tokenName];
|
|
141
|
+
else if (source === "query") token = queryParams.get(tokenName) ?? void 0;
|
|
142
|
+
else if (source === "cookie") token = getCookie(tokenName) ?? void 0;
|
|
143
|
+
if (token) break;
|
|
144
|
+
}
|
|
145
|
+
const wfid = body?.[wfidName] ?? queryParams.get(wfidName) ?? void 0;
|
|
146
|
+
const input = body?.input;
|
|
147
|
+
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
148
|
+
const shouldConsume = (outletName) => {
|
|
149
|
+
if (typeof tok.consume === "boolean") return tok.consume;
|
|
150
|
+
return (tok.consume ?? DEFAULT_CONSUME_TOKEN)[outletName] ?? false;
|
|
151
|
+
};
|
|
152
|
+
let output;
|
|
153
|
+
if (token) {
|
|
154
|
+
const strategy = resolveStrategy(wfid ?? "");
|
|
155
|
+
ctx.set(stateStrategyKey, strategy);
|
|
156
|
+
const state = await strategy.retrieve(token);
|
|
157
|
+
if (!state) return {
|
|
158
|
+
error: "Invalid or expired workflow state",
|
|
159
|
+
status: 400
|
|
160
|
+
};
|
|
161
|
+
if (state.schemaId !== (wfid ?? "")) {
|
|
162
|
+
const realStrategy = resolveStrategy(state.schemaId);
|
|
163
|
+
ctx.set(stateStrategyKey, realStrategy);
|
|
164
|
+
}
|
|
165
|
+
const outletName = state.meta?.outlet;
|
|
166
|
+
if (outletName && shouldConsume(outletName)) await strategy.consume(token);
|
|
167
|
+
output = await deps.resume(state, {
|
|
168
|
+
input,
|
|
169
|
+
eventContext: ctx
|
|
170
|
+
});
|
|
171
|
+
} else if (wfid) {
|
|
172
|
+
if (config.allow?.length && !config.allow.includes(wfid)) return {
|
|
173
|
+
error: `Workflow '${wfid}' is not allowed`,
|
|
174
|
+
status: 403
|
|
175
|
+
};
|
|
176
|
+
if (config.block?.includes(wfid)) return {
|
|
177
|
+
error: `Workflow '${wfid}' is blocked`,
|
|
178
|
+
status: 403
|
|
179
|
+
};
|
|
180
|
+
const strategy = resolveStrategy(wfid);
|
|
181
|
+
ctx.set(stateStrategyKey, strategy);
|
|
182
|
+
const initialContext = config.initialContext ? config.initialContext(body, wfid) : {};
|
|
183
|
+
output = await deps.start(wfid, initialContext, {
|
|
184
|
+
input,
|
|
185
|
+
eventContext: ctx
|
|
186
|
+
});
|
|
187
|
+
} else return {
|
|
188
|
+
error: "Missing wfs (state token) or wfid (workflow ID)",
|
|
189
|
+
status: 400
|
|
190
|
+
};
|
|
191
|
+
if (output.finished) {
|
|
192
|
+
if (config.onFinished) return config.onFinished({
|
|
193
|
+
context: output.state.context,
|
|
194
|
+
schemaId: output.state.schemaId
|
|
195
|
+
});
|
|
196
|
+
const finished = ctx.get(wfFinishedKey);
|
|
197
|
+
if (finished?.cookies) for (const [name, cookie] of Object.entries(finished.cookies)) response.setCookie(name, cookie.value, cookie.options);
|
|
198
|
+
if (finished?.type === "redirect") {
|
|
199
|
+
response.setHeader("location", finished.value);
|
|
200
|
+
return { status: finished.status ?? 302 };
|
|
201
|
+
}
|
|
202
|
+
if (finished) return finished.value;
|
|
203
|
+
return { finished: true };
|
|
204
|
+
}
|
|
205
|
+
if (output.inputRequired) {
|
|
206
|
+
const outletReq = output.inputRequired;
|
|
207
|
+
const outletHandler = registry.get(outletReq.outlet);
|
|
208
|
+
if (!outletHandler) return {
|
|
209
|
+
error: `Unknown outlet: '${outletReq.outlet}'`,
|
|
210
|
+
status: 500
|
|
211
|
+
};
|
|
212
|
+
const strategy = ctx.get(stateStrategyKey);
|
|
213
|
+
const stateWithMeta = {
|
|
214
|
+
...output.state,
|
|
215
|
+
meta: { outlet: outletReq.outlet }
|
|
216
|
+
};
|
|
217
|
+
const newToken = await strategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0);
|
|
218
|
+
if (tokenWrite === "cookie") response.setCookie(tokenName, newToken, {
|
|
219
|
+
httpOnly: true,
|
|
220
|
+
sameSite: "Strict",
|
|
221
|
+
path: "/"
|
|
222
|
+
});
|
|
223
|
+
const result = await outletHandler.deliver(outletReq, newToken);
|
|
224
|
+
if (tokenWrite === "body" && result?.response && typeof result.response === "object") return {
|
|
225
|
+
...result.response,
|
|
226
|
+
[tokenName]: newToken
|
|
227
|
+
};
|
|
228
|
+
return result?.response ?? { waiting: true };
|
|
229
|
+
}
|
|
230
|
+
if (output.error) return {
|
|
231
|
+
error: output.error.message,
|
|
232
|
+
errorList: output.errorList
|
|
233
|
+
};
|
|
234
|
+
return { error: "Unexpected workflow state" };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region packages/event-wf/src/outlets/create-outlet.ts
|
|
239
|
+
/**
|
|
240
|
+
* Creates an HTTP outlet that passes through the outlet payload as
|
|
241
|
+
* the HTTP response body. This is the most common outlet — it returns
|
|
242
|
+
* forms, prompts, or data to the client.
|
|
243
|
+
*
|
|
244
|
+
* @example
|
|
245
|
+
* ```ts
|
|
246
|
+
* const httpOutlet = createHttpOutlet()
|
|
247
|
+
* // Step does: return outletHttp({ fields: ['email', 'password'] })
|
|
248
|
+
* // Client receives: { fields: ['email', 'password'] }
|
|
249
|
+
* ```
|
|
250
|
+
*/
|
|
251
|
+
function createHttpOutlet(opts) {
|
|
252
|
+
return {
|
|
253
|
+
name: "http",
|
|
254
|
+
async deliver(request, _token) {
|
|
255
|
+
const body = opts?.transform ? opts.transform(request.payload, request.context) : typeof request.payload === "object" && request.payload !== null ? {
|
|
256
|
+
...request.payload,
|
|
257
|
+
...request.context
|
|
258
|
+
} : request.payload;
|
|
259
|
+
return { response: body };
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Creates an email outlet that delegates to a user-provided send function.
|
|
265
|
+
* The send function receives the target, template, context, and the state
|
|
266
|
+
* token (for embedding in magic links / verification URLs).
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* const emailOutlet = createEmailOutlet(async (opts) => {
|
|
271
|
+
* await mailer.send({
|
|
272
|
+
* to: opts.target,
|
|
273
|
+
* template: opts.template,
|
|
274
|
+
* data: { ...opts.context, verifyUrl: `/verify?wfs=${opts.token}` },
|
|
275
|
+
* })
|
|
276
|
+
* })
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
function createEmailOutlet(send) {
|
|
280
|
+
return {
|
|
281
|
+
name: "email",
|
|
282
|
+
async deliver(request, token) {
|
|
283
|
+
await send({
|
|
284
|
+
target: request.target ?? "",
|
|
285
|
+
template: request.template ?? "",
|
|
286
|
+
context: request.context ?? {},
|
|
287
|
+
token
|
|
288
|
+
});
|
|
289
|
+
return { response: {
|
|
290
|
+
sent: true,
|
|
291
|
+
outlet: "email"
|
|
292
|
+
} };
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region packages/event-wf/src/outlets/create-handler.ts
|
|
299
|
+
/**
|
|
300
|
+
* Creates a pre-wired outlet handler from a workflow app instance.
|
|
301
|
+
* Eliminates the need to manually construct `WfOutletTriggerDeps`.
|
|
302
|
+
*
|
|
303
|
+
* Accepts any object with `start` and `resume` methods (WooksWf, MoostWf, etc.).
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```ts
|
|
307
|
+
* const handle = createOutletHandler(wfApp)
|
|
308
|
+
* httpApp.post('/workflow', () => handle(config))
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
function createOutletHandler(wfApp) {
|
|
312
|
+
return (config) => handleWfOutletRequest(config, {
|
|
313
|
+
start: (schemaId, context, opts) => wfApp.start(schemaId, context, opts),
|
|
314
|
+
resume: (state, opts) => wfApp.resume(state, opts)
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.1.1/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
320
|
+
/**
|
|
321
|
+
* Generic outlet request. Use for custom outlets.
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* return outlet('pending-task', {
|
|
325
|
+
* payload: ApprovalForm,
|
|
326
|
+
* target: managerId,
|
|
327
|
+
* context: { orderId, amount },
|
|
328
|
+
* })
|
|
329
|
+
*/
|
|
330
|
+
function outlet(name, data) {
|
|
331
|
+
return { inputRequired: {
|
|
332
|
+
outlet: name,
|
|
333
|
+
...data
|
|
334
|
+
} };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
338
|
+
* and state token in the HTTP response.
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* return outletHttp(LoginForm)
|
|
342
|
+
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
343
|
+
*/
|
|
344
|
+
function outletHttp(payload, context) {
|
|
345
|
+
return outlet("http", {
|
|
346
|
+
payload,
|
|
347
|
+
context
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Pause and send email with a magic link containing the state token.
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
355
|
+
*/
|
|
356
|
+
function outletEmail(target, template, context) {
|
|
357
|
+
return outlet("email", {
|
|
358
|
+
target,
|
|
359
|
+
template,
|
|
360
|
+
context
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Self-contained AES-256-GCM encrypted state strategy.
|
|
365
|
+
*
|
|
366
|
+
* Workflow state is encrypted into a base64url token that travels with the
|
|
367
|
+
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
368
|
+
*
|
|
369
|
+
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* const strategy = new EncapsulatedStateStrategy({
|
|
373
|
+
* secret: crypto.randomBytes(32),
|
|
374
|
+
* defaultTtl: 3600_000, // 1 hour
|
|
375
|
+
* });
|
|
376
|
+
* const token = await strategy.persist(state);
|
|
377
|
+
* const recovered = await strategy.retrieve(token);
|
|
378
|
+
*/
|
|
379
|
+
var EncapsulatedStateStrategy = class {
|
|
380
|
+
/** @throws if secret is not exactly 32 bytes */
|
|
381
|
+
constructor(config) {
|
|
382
|
+
this.config = config;
|
|
383
|
+
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
384
|
+
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Encrypt workflow state into a self-contained token.
|
|
388
|
+
* @param state — workflow state to persist
|
|
389
|
+
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
390
|
+
* @returns base64url-encoded encrypted token
|
|
391
|
+
*/
|
|
392
|
+
async persist(state, options) {
|
|
393
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
394
|
+
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
395
|
+
const payload = JSON.stringify({
|
|
396
|
+
s: state,
|
|
397
|
+
e: exp
|
|
398
|
+
});
|
|
399
|
+
const iv = randomBytes(12);
|
|
400
|
+
const cipher = createCipheriv("aes-256-gcm", this.key, iv);
|
|
401
|
+
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
402
|
+
const tag = cipher.getAuthTag();
|
|
403
|
+
return Buffer.concat([
|
|
404
|
+
iv,
|
|
405
|
+
tag,
|
|
406
|
+
encrypted
|
|
407
|
+
]).toString("base64url");
|
|
408
|
+
}
|
|
409
|
+
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
410
|
+
async retrieve(token) {
|
|
411
|
+
return this.decrypt(token);
|
|
412
|
+
}
|
|
413
|
+
/** Same as retrieve (stateless — cannot truly invalidate a token). */
|
|
414
|
+
async consume(token) {
|
|
415
|
+
return this.decrypt(token);
|
|
416
|
+
}
|
|
417
|
+
decrypt(token) {
|
|
418
|
+
try {
|
|
419
|
+
const buf = Buffer.from(token, "base64url");
|
|
420
|
+
if (buf.length < 28) return null;
|
|
421
|
+
const iv = buf.subarray(0, 12);
|
|
422
|
+
const tag = buf.subarray(12, 28);
|
|
423
|
+
const ciphertext = buf.subarray(28);
|
|
424
|
+
const decipher = createDecipheriv("aes-256-gcm", this.key, iv);
|
|
425
|
+
decipher.setAuthTag(tag);
|
|
426
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
427
|
+
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
428
|
+
if (exp > 0 && Date.now() > exp) return null;
|
|
429
|
+
return state;
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
var HandleStateStrategy = class {
|
|
436
|
+
constructor(config) {
|
|
437
|
+
this.config = config;
|
|
438
|
+
}
|
|
439
|
+
async persist(state, options) {
|
|
440
|
+
const handle = (this.config.generateHandle ?? randomUUID)();
|
|
441
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
442
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
443
|
+
await this.config.store.set(handle, state, expiresAt);
|
|
444
|
+
return handle;
|
|
445
|
+
}
|
|
446
|
+
async retrieve(token) {
|
|
447
|
+
return (await this.config.store.get(token))?.state ?? null;
|
|
448
|
+
}
|
|
449
|
+
async consume(token) {
|
|
450
|
+
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
/**
|
|
454
|
+
* In-memory state store for development and testing.
|
|
455
|
+
* State is lost on process restart.
|
|
456
|
+
*/
|
|
457
|
+
var WfStateStoreMemory = class {
|
|
458
|
+
constructor() {
|
|
459
|
+
this.store = /* @__PURE__ */ new Map();
|
|
460
|
+
}
|
|
461
|
+
async set(handle, state, expiresAt) {
|
|
462
|
+
this.store.set(handle, {
|
|
463
|
+
state,
|
|
464
|
+
expiresAt
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
async get(handle) {
|
|
468
|
+
const entry = this.store.get(handle);
|
|
469
|
+
if (!entry) return null;
|
|
470
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
471
|
+
this.store.delete(handle);
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return entry;
|
|
475
|
+
}
|
|
476
|
+
async delete(handle) {
|
|
477
|
+
this.store.delete(handle);
|
|
478
|
+
}
|
|
479
|
+
async getAndDelete(handle) {
|
|
480
|
+
const entry = await this.get(handle);
|
|
481
|
+
if (entry) this.store.delete(handle);
|
|
482
|
+
return entry;
|
|
483
|
+
}
|
|
484
|
+
async cleanup() {
|
|
485
|
+
const now = Date.now();
|
|
486
|
+
let count = 0;
|
|
487
|
+
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
488
|
+
this.store.delete(handle);
|
|
489
|
+
count++;
|
|
490
|
+
}
|
|
491
|
+
return count;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
55
495
|
//#endregion
|
|
56
496
|
//#region packages/event-wf/src/workflow.ts
|
|
57
497
|
/** Workflow engine that resolves steps via Wooks router lookup. */
|
|
@@ -244,4 +684,4 @@ function createWfApp(opts, wooks) {
|
|
|
244
684
|
}
|
|
245
685
|
|
|
246
686
|
//#endregion
|
|
247
|
-
export { StepRetriableError, WooksWf, createWfApp, createWfContext, resumeKey, resumeWfContext, useLogger, useRouteParams, useWfState, wfKind, wfShortcuts };
|
|
687
|
+
export { EncapsulatedStateStrategy, HandleStateStrategy, StepRetriableError, WfStateStoreMemory, WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, createWfContext, handleWfOutletRequest, outlet, outletEmail, outletHttp, resumeKey, resumeWfContext, useLogger, useRouteParams, useWfFinished, useWfOutlet, useWfState, wfKind, wfShortcuts };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-wf",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "@wooksjs/event-wf",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -22,13 +22,8 @@
|
|
|
22
22
|
"url": "git+https://github.com/wooksjs/wooksjs.git",
|
|
23
23
|
"directory": "packages/event-wf"
|
|
24
24
|
},
|
|
25
|
-
"bin": {
|
|
26
|
-
"wooksjs-event-wf-skill": "scripts/setup-skills.js"
|
|
27
|
-
},
|
|
28
25
|
"files": [
|
|
29
|
-
"dist"
|
|
30
|
-
"skills",
|
|
31
|
-
"scripts"
|
|
26
|
+
"dist"
|
|
32
27
|
],
|
|
33
28
|
"main": "dist/index.cjs",
|
|
34
29
|
"module": "dist/index.mjs",
|
|
@@ -42,21 +37,32 @@
|
|
|
42
37
|
}
|
|
43
38
|
},
|
|
44
39
|
"dependencies": {
|
|
45
|
-
"@prostojs/wf": "^0.
|
|
40
|
+
"@prostojs/wf": "^0.1.1"
|
|
46
41
|
},
|
|
47
42
|
"devDependencies": {
|
|
48
43
|
"typescript": "^5.9.3",
|
|
49
44
|
"vitest": "^3.2.4",
|
|
50
|
-
"@wooksjs/event-core": "^0.7.
|
|
51
|
-
"
|
|
45
|
+
"@wooksjs/event-core": "^0.7.9",
|
|
46
|
+
"@wooksjs/event-http": "^0.7.9",
|
|
47
|
+
"@wooksjs/http-body": "^0.7.9",
|
|
48
|
+
"wooks": "^0.7.9"
|
|
52
49
|
},
|
|
53
50
|
"peerDependencies": {
|
|
54
51
|
"@prostojs/logger": "^0.4.3",
|
|
55
|
-
"@wooksjs/event-core": "^0.7.
|
|
56
|
-
"
|
|
52
|
+
"@wooksjs/event-core": "^0.7.9",
|
|
53
|
+
"@wooksjs/event-http": "^0.7.9",
|
|
54
|
+
"@wooksjs/http-body": "^0.7.9",
|
|
55
|
+
"wooks": "^0.7.9"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"@wooksjs/event-http": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@wooksjs/http-body": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
57
64
|
},
|
|
58
65
|
"scripts": {
|
|
59
|
-
"build": "rolldown -c ../../rolldown.config.mjs"
|
|
60
|
-
"setup-skills": "node scripts/setup-skills.js"
|
|
66
|
+
"build": "rolldown -c ../../rolldown.config.mjs"
|
|
61
67
|
}
|
|
62
68
|
}
|
package/scripts/setup-skills.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* prettier-ignore */
|
|
3
|
-
'use strict'
|
|
4
|
-
|
|
5
|
-
const fs = require('fs')
|
|
6
|
-
const path = require('path')
|
|
7
|
-
const os = require('os')
|
|
8
|
-
|
|
9
|
-
const SKILL_NAME = 'wooksjs-event-wf'
|
|
10
|
-
const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
|
|
11
|
-
|
|
12
|
-
if (!fs.existsSync(SKILL_SRC)) {
|
|
13
|
-
console.error(`No skills found at ${SKILL_SRC}`)
|
|
14
|
-
console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
|
|
15
|
-
process.exit(1)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const AGENTS = {
|
|
19
|
-
'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
|
|
20
|
-
'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
|
|
21
|
-
'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
|
|
22
|
-
'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
|
|
23
|
-
'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const args = process.argv.slice(2)
|
|
27
|
-
const isGlobal = args.includes('--global') || args.includes('-g')
|
|
28
|
-
const isPostinstall = args.includes('--postinstall')
|
|
29
|
-
let installed = 0, skipped = 0
|
|
30
|
-
const installedDirs = []
|
|
31
|
-
|
|
32
|
-
for (const [agentName, cfg] of Object.entries(AGENTS)) {
|
|
33
|
-
const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
|
|
34
|
-
const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
|
|
35
|
-
|
|
36
|
-
// In postinstall mode: silently skip agents that aren't set up globally
|
|
37
|
-
if (isPostinstall || isGlobal) {
|
|
38
|
-
if (!fs.existsSync(agentRootDir)) { skipped++; continue }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const dest = path.join(targetBase, SKILL_NAME)
|
|
42
|
-
try {
|
|
43
|
-
fs.mkdirSync(dest, { recursive: true })
|
|
44
|
-
fs.cpSync(SKILL_SRC, dest, { recursive: true })
|
|
45
|
-
console.log(`✅ ${agentName}: installed to ${dest}`)
|
|
46
|
-
installed++
|
|
47
|
-
if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
|
|
48
|
-
} catch (err) {
|
|
49
|
-
console.warn(`⚠️ ${agentName}: failed — ${err.message}`)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Add locally-installed skill dirs to .gitignore
|
|
54
|
-
if (!isGlobal && installedDirs.length > 0) {
|
|
55
|
-
const gitignorePath = path.join(process.cwd(), '.gitignore')
|
|
56
|
-
let gitignoreContent = ''
|
|
57
|
-
try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
|
|
58
|
-
const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
|
|
59
|
-
if (linesToAdd.length > 0) {
|
|
60
|
-
const hasHeader = gitignoreContent.includes('# AI agent skills')
|
|
61
|
-
const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
|
|
62
|
-
+ (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
|
|
63
|
-
+ linesToAdd.join('\n') + '\n'
|
|
64
|
-
fs.appendFileSync(gitignorePath, block)
|
|
65
|
-
console.log(`📝 Added ${linesToAdd.length} entries to .gitignore`)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (installed === 0 && isPostinstall) {
|
|
70
|
-
// Silence is fine — no agents present, nothing to do
|
|
71
|
-
} else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
|
|
72
|
-
console.log('No agent directories detected. Try --global or run without it for project-local install.')
|
|
73
|
-
} else if (installed === 0) {
|
|
74
|
-
console.log('Nothing installed. Run without --global to install project-locally.')
|
|
75
|
-
} else {
|
|
76
|
-
console.log(`\n✨ Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
|
|
77
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: wooksjs-event-wf
|
|
3
|
-
description: Wooks Workflow framework — composable, step-based workflow engine for Node.js. Load when building workflow/process automation with wooks; defining workflow steps and flows; using workflow composables (useWfState, useRouteParams, useLogger); working with @wooksjs/event-core context (key, cached, defineWook, defineEventKind); creating conditional and looping flows; resuming paused workflows; handling user input requirements; using string-based step handlers; attaching workflow spies; working with StepRetriableError; creating custom event context composables for workflows; sharing parent event context (eventContext) for HTTP integration; accessing HTTP composables from workflow steps.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# @wooksjs/event-wf
|
|
7
|
-
|
|
8
|
-
A composable workflow framework for Node.js built on async context (AsyncLocalStorage) and `@prostojs/wf`. Define steps and flows as composable units — steps execute sequentially with conditional branching, loops, pause/resume, and user input handling. Context is scoped per workflow execution.
|
|
9
|
-
|
|
10
|
-
## How to use this skill
|
|
11
|
-
|
|
12
|
-
Read the domain file that matches the task. Do not load all files — only what you need.
|
|
13
|
-
|
|
14
|
-
| Domain | File | Load when... |
|
|
15
|
-
| ------------------------------ | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
-
| Event context (core machinery) | [event-core.md](event-core.md) | Understanding `EventContext`, `key()`/`cached()`/`defineWook()`/`defineEventKind()`/`slot()`, creating custom composables, lazy evaluation and caching, building your own `use*()` functions |
|
|
17
|
-
| Workflow app setup | [core.md](core.md) | Creating a workflow app, `createWfApp`, starting/resuming workflows, error handling, spies, testing, logging, sharing parent event context (`eventContext`), HTTP integration |
|
|
18
|
-
| Steps & flows | [workflows.md](workflows.md) | Defining steps (`app.step`), defining flows (`app.flow`), workflow schemas, conditions, loops, user input, parametric steps, `useWfState`, `StepRetriableError`, string-based handlers |
|
|
19
|
-
|
|
20
|
-
## Quick reference
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
import { createWfApp } from '@wooksjs/event-wf'
|
|
24
|
-
|
|
25
|
-
const app = createWfApp<{ result: number }>()
|
|
26
|
-
|
|
27
|
-
app.step('add', {
|
|
28
|
-
input: 'number',
|
|
29
|
-
handler: 'ctx.result += input',
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
app.flow('calculate', [
|
|
33
|
-
{ id: 'add', input: 5 },
|
|
34
|
-
{ id: 'add', input: 2 },
|
|
35
|
-
{ condition: 'result < 10', steps: [{ id: 'add', input: 3 }] },
|
|
36
|
-
])
|
|
37
|
-
|
|
38
|
-
const output = await app.start('calculate', { result: 0 })
|
|
39
|
-
console.log(output.state.context) // { result: 10 }
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Key exports: `createWfApp()`, `useWfState()`, `useRouteParams()`, `useLogger()`, `StepRetriableError`.
|