@moostjs/event-wf 0.6.4 → 0.6.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/index.cjs +272 -2
- package/dist/index.d.ts +34 -3
- package/dist/index.mjs +231 -4
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
const __wooksjs_event_wf = __toESM(require("@wooksjs/event-wf"));
|
|
25
25
|
const moost = __toESM(require("moost"));
|
|
26
26
|
const __prostojs_wf = __toESM(require("@prostojs/wf"));
|
|
27
|
+
const node_crypto = __toESM(require("node:crypto"));
|
|
27
28
|
|
|
28
29
|
//#region packages/event-wf/src/meta-types.ts
|
|
29
30
|
function getWfMate() {
|
|
@@ -76,6 +77,24 @@ function getWfMate() {
|
|
|
76
77
|
return getWfMate().decorate("wfSchema", schema);
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
80
|
+
* Sets TTL (in ms) for the workflow state when this step pauses.
|
|
81
|
+
* The adapter wraps the step handler to set `expires` on the outlet signal,
|
|
82
|
+
* which is then passed to `strategy.persist(state, { ttl })`.
|
|
83
|
+
*
|
|
84
|
+
* @param ttlMs - Time-to-live in milliseconds for the paused state token.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* @Step('send-invite')
|
|
89
|
+
* @StepTTL(60 * 60 * 1000) // 1 hour
|
|
90
|
+
* async sendInvite(@WorkflowParam('context') ctx: any) {
|
|
91
|
+
* return outletEmail(ctx.email, 'invite')
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/ function StepTTL(ttlMs) {
|
|
95
|
+
return getWfMate().decorate("wfStepTTL", ttlMs);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
79
98
|
* Parameter decorator that resolves a workflow context value into a step handler argument.
|
|
80
99
|
*
|
|
81
100
|
* @param name - The workflow value to resolve:
|
|
@@ -160,6 +179,23 @@ const CONTEXT_TYPE = "WF";
|
|
|
160
179
|
this.wfApp.detachSpy(fn);
|
|
161
180
|
}
|
|
162
181
|
/**
|
|
182
|
+
* Handles an outlet trigger request within an HTTP handler.
|
|
183
|
+
*
|
|
184
|
+
* Reads `wfid` (workflow ID) and `wfs` (state token) from the HTTP request,
|
|
185
|
+
* starts or resumes the workflow, and dispatches pauses to registered outlets.
|
|
186
|
+
*
|
|
187
|
+
* Must be called from within an HTTP event context (e.g. a `@Post` handler)
|
|
188
|
+
* so that wooks HTTP composables are available.
|
|
189
|
+
*
|
|
190
|
+
* @param config - Outlet trigger configuration (allowed workflows, state strategy, outlets, etc.)
|
|
191
|
+
* @returns The outlet result (form payload + token), finished response, or error.
|
|
192
|
+
*/ handleOutlet(config) {
|
|
193
|
+
return (0, __wooksjs_event_wf.handleWfOutletRequest)(config, {
|
|
194
|
+
start: (schemaId, context, opts) => this.start(schemaId, context, opts?.input),
|
|
195
|
+
resume: (state, opts) => this.resume(state, opts?.input)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
163
199
|
* Starts a new workflow execution.
|
|
164
200
|
*
|
|
165
201
|
* @param schemaId - Identifier of the registered workflow schema.
|
|
@@ -203,17 +239,31 @@ const CONTEXT_TYPE = "WF";
|
|
|
203
239
|
resolveArgs: opts.resolveArgs,
|
|
204
240
|
manualUnscope: true,
|
|
205
241
|
targetPath,
|
|
242
|
+
controllerPrefix: opts.prefix,
|
|
206
243
|
handlerType: handler.type
|
|
207
244
|
});
|
|
208
245
|
if (handler.type === "WF_STEP") {
|
|
209
|
-
|
|
246
|
+
const stepTTL = getWfMate().read(opts.fakeInstance, opts.method)?.wfStepTTL;
|
|
247
|
+
let stepHandler = fn;
|
|
248
|
+
if (stepTTL !== void 0) {
|
|
249
|
+
const wrapped = stepHandler;
|
|
250
|
+
stepHandler = async () => {
|
|
251
|
+
const result = await wrapped();
|
|
252
|
+
if (result && typeof result === "object" && "inputRequired" in result) return {
|
|
253
|
+
...result,
|
|
254
|
+
expires: Date.now() + stepTTL
|
|
255
|
+
};
|
|
256
|
+
return result;
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
this.wfApp.step(targetPath, { handler: stepHandler });
|
|
210
260
|
opts.logHandler(`[36m(${handler.type})[32m${targetPath}`);
|
|
211
261
|
} else {
|
|
212
262
|
const mate = getWfMate();
|
|
213
263
|
let wfSchema = mate.read(opts.fakeInstance, opts.method)?.wfSchema;
|
|
214
264
|
if (!wfSchema) wfSchema = mate.read(opts.fakeInstance)?.wfSchema;
|
|
215
265
|
const _fn = async () => {
|
|
216
|
-
(0, moost.setControllerContext)(this.moost, "bindHandler", targetPath);
|
|
266
|
+
(0, moost.setControllerContext)(this.moost, "bindHandler", targetPath, { prefix: opts.prefix });
|
|
217
267
|
return fn();
|
|
218
268
|
};
|
|
219
269
|
this.toInit.push(() => {
|
|
@@ -241,6 +291,185 @@ const CONTEXT_TYPE = "WF";
|
|
|
241
291
|
};
|
|
242
292
|
|
|
243
293
|
//#endregion
|
|
294
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.1.1/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
295
|
+
/**
|
|
296
|
+
* Generic outlet request. Use for custom outlets.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* return outlet('pending-task', {
|
|
300
|
+
* payload: ApprovalForm,
|
|
301
|
+
* target: managerId,
|
|
302
|
+
* context: { orderId, amount },
|
|
303
|
+
* })
|
|
304
|
+
*/
|
|
305
|
+
function outlet(name, data) {
|
|
306
|
+
return { inputRequired: {
|
|
307
|
+
outlet: name,
|
|
308
|
+
...data
|
|
309
|
+
} };
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
313
|
+
* and state token in the HTTP response.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* return outletHttp(LoginForm)
|
|
317
|
+
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
318
|
+
*/
|
|
319
|
+
function outletHttp(payload, context) {
|
|
320
|
+
return outlet("http", {
|
|
321
|
+
payload,
|
|
322
|
+
context
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Pause and send email with a magic link containing the state token.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
330
|
+
*/
|
|
331
|
+
function outletEmail(target, template, context) {
|
|
332
|
+
return outlet("email", {
|
|
333
|
+
target,
|
|
334
|
+
template,
|
|
335
|
+
context
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Self-contained AES-256-GCM encrypted state strategy.
|
|
340
|
+
*
|
|
341
|
+
* Workflow state is encrypted into a base64url token that travels with the
|
|
342
|
+
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
343
|
+
*
|
|
344
|
+
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* const strategy = new EncapsulatedStateStrategy({
|
|
348
|
+
* secret: crypto.randomBytes(32),
|
|
349
|
+
* defaultTtl: 3600_000, // 1 hour
|
|
350
|
+
* });
|
|
351
|
+
* const token = await strategy.persist(state);
|
|
352
|
+
* const recovered = await strategy.retrieve(token);
|
|
353
|
+
*/
|
|
354
|
+
var EncapsulatedStateStrategy = class {
|
|
355
|
+
/** @throws if secret is not exactly 32 bytes */
|
|
356
|
+
constructor(config) {
|
|
357
|
+
this.config = config;
|
|
358
|
+
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
359
|
+
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Encrypt workflow state into a self-contained token.
|
|
363
|
+
* @param state — workflow state to persist
|
|
364
|
+
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
365
|
+
* @returns base64url-encoded encrypted token
|
|
366
|
+
*/
|
|
367
|
+
async persist(state, options) {
|
|
368
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
369
|
+
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
370
|
+
const payload = JSON.stringify({
|
|
371
|
+
s: state,
|
|
372
|
+
e: exp
|
|
373
|
+
});
|
|
374
|
+
const iv = (0, node_crypto.randomBytes)(12);
|
|
375
|
+
const cipher = (0, node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
|
|
376
|
+
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
377
|
+
const tag = cipher.getAuthTag();
|
|
378
|
+
return Buffer.concat([
|
|
379
|
+
iv,
|
|
380
|
+
tag,
|
|
381
|
+
encrypted
|
|
382
|
+
]).toString("base64url");
|
|
383
|
+
}
|
|
384
|
+
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
385
|
+
async retrieve(token) {
|
|
386
|
+
return this.decrypt(token);
|
|
387
|
+
}
|
|
388
|
+
/** Same as retrieve (stateless — cannot truly invalidate a token). */
|
|
389
|
+
async consume(token) {
|
|
390
|
+
return this.decrypt(token);
|
|
391
|
+
}
|
|
392
|
+
decrypt(token) {
|
|
393
|
+
try {
|
|
394
|
+
const buf = Buffer.from(token, "base64url");
|
|
395
|
+
if (buf.length < 28) return null;
|
|
396
|
+
const iv = buf.subarray(0, 12);
|
|
397
|
+
const tag = buf.subarray(12, 28);
|
|
398
|
+
const ciphertext = buf.subarray(28);
|
|
399
|
+
const decipher = (0, node_crypto.createDecipheriv)("aes-256-gcm", this.key, iv);
|
|
400
|
+
decipher.setAuthTag(tag);
|
|
401
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
402
|
+
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
403
|
+
if (exp > 0 && Date.now() > exp) return null;
|
|
404
|
+
return state;
|
|
405
|
+
} catch {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var HandleStateStrategy = class {
|
|
411
|
+
constructor(config) {
|
|
412
|
+
this.config = config;
|
|
413
|
+
}
|
|
414
|
+
async persist(state, options) {
|
|
415
|
+
const handle = (this.config.generateHandle ?? node_crypto.randomUUID)();
|
|
416
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
417
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
418
|
+
await this.config.store.set(handle, state, expiresAt);
|
|
419
|
+
return handle;
|
|
420
|
+
}
|
|
421
|
+
async retrieve(token) {
|
|
422
|
+
return (await this.config.store.get(token))?.state ?? null;
|
|
423
|
+
}
|
|
424
|
+
async consume(token) {
|
|
425
|
+
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* In-memory state store for development and testing.
|
|
430
|
+
* State is lost on process restart.
|
|
431
|
+
*/
|
|
432
|
+
var WfStateStoreMemory = class {
|
|
433
|
+
constructor() {
|
|
434
|
+
this.store = /* @__PURE__ */ new Map();
|
|
435
|
+
}
|
|
436
|
+
async set(handle, state, expiresAt) {
|
|
437
|
+
this.store.set(handle, {
|
|
438
|
+
state,
|
|
439
|
+
expiresAt
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async get(handle) {
|
|
443
|
+
const entry = this.store.get(handle);
|
|
444
|
+
if (!entry) return null;
|
|
445
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
446
|
+
this.store.delete(handle);
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
return entry;
|
|
450
|
+
}
|
|
451
|
+
async delete(handle) {
|
|
452
|
+
this.store.delete(handle);
|
|
453
|
+
}
|
|
454
|
+
async getAndDelete(handle) {
|
|
455
|
+
const entry = await this.get(handle);
|
|
456
|
+
if (entry) this.store.delete(handle);
|
|
457
|
+
return entry;
|
|
458
|
+
}
|
|
459
|
+
async cleanup() {
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
let count = 0;
|
|
462
|
+
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
463
|
+
this.store.delete(handle);
|
|
464
|
+
count++;
|
|
465
|
+
}
|
|
466
|
+
return count;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
//#endregion
|
|
471
|
+
exports.EncapsulatedStateStrategy = EncapsulatedStateStrategy;
|
|
472
|
+
exports.HandleStateStrategy = HandleStateStrategy;
|
|
244
473
|
exports.MoostWf = MoostWf;
|
|
245
474
|
exports.Step = Step;
|
|
246
475
|
Object.defineProperty(exports, 'StepRetriableError', {
|
|
@@ -249,9 +478,50 @@ Object.defineProperty(exports, 'StepRetriableError', {
|
|
|
249
478
|
return __prostojs_wf.StepRetriableError;
|
|
250
479
|
}
|
|
251
480
|
});
|
|
481
|
+
exports.StepTTL = StepTTL;
|
|
482
|
+
exports.WfStateStoreMemory = WfStateStoreMemory;
|
|
252
483
|
exports.Workflow = Workflow;
|
|
253
484
|
exports.WorkflowParam = WorkflowParam;
|
|
254
485
|
exports.WorkflowSchema = WorkflowSchema;
|
|
486
|
+
Object.defineProperty(exports, 'createEmailOutlet', {
|
|
487
|
+
enumerable: true,
|
|
488
|
+
get: function () {
|
|
489
|
+
return __wooksjs_event_wf.createEmailOutlet;
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
Object.defineProperty(exports, 'createHttpOutlet', {
|
|
493
|
+
enumerable: true,
|
|
494
|
+
get: function () {
|
|
495
|
+
return __wooksjs_event_wf.createHttpOutlet;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
Object.defineProperty(exports, 'createOutletHandler', {
|
|
499
|
+
enumerable: true,
|
|
500
|
+
get: function () {
|
|
501
|
+
return __wooksjs_event_wf.createOutletHandler;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
Object.defineProperty(exports, 'handleWfOutletRequest', {
|
|
505
|
+
enumerable: true,
|
|
506
|
+
get: function () {
|
|
507
|
+
return __wooksjs_event_wf.handleWfOutletRequest;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
exports.outlet = outlet;
|
|
511
|
+
exports.outletEmail = outletEmail;
|
|
512
|
+
exports.outletHttp = outletHttp;
|
|
513
|
+
Object.defineProperty(exports, 'useWfFinished', {
|
|
514
|
+
enumerable: true,
|
|
515
|
+
get: function () {
|
|
516
|
+
return __wooksjs_event_wf.useWfFinished;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
Object.defineProperty(exports, 'useWfOutlet', {
|
|
520
|
+
enumerable: true,
|
|
521
|
+
get: function () {
|
|
522
|
+
return __wooksjs_event_wf.useWfOutlet;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
255
525
|
Object.defineProperty(exports, 'useWfState', {
|
|
256
526
|
enumerable: true,
|
|
257
527
|
get: function () {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { TWorkflowSchema, TWorkflowSpy, TFlowOutput } from '@prostojs/wf';
|
|
2
2
|
export { StepRetriableError, TFlowOutput, TWorkflowSchema } from '@prostojs/wf';
|
|
3
|
-
import { WooksWf, TWooksWfOptions } from '@wooksjs/event-wf';
|
|
4
|
-
export { useWfState, wfKind } from '@wooksjs/event-wf';
|
|
3
|
+
import { WooksWf, TWooksWfOptions, WfOutletTriggerConfig } from '@wooksjs/event-wf';
|
|
4
|
+
export { WfFinishedResponse, WfOutletTokenConfig, WfOutletTriggerConfig, WfOutletTriggerDeps, createEmailOutlet, createHttpOutlet, createOutletHandler, handleWfOutletRequest, useWfFinished, useWfOutlet, useWfState, wfKind } from '@wooksjs/event-wf';
|
|
5
5
|
import { TMoostAdapter, Moost, TMoostAdapterOptions } from 'moost';
|
|
6
|
+
export { EncapsulatedStateStrategy, HandleStateStrategy, WfOutlet, WfOutletRequest, WfOutletResult, WfState, WfStateStore, WfStateStoreMemory, WfStateStrategy, outlet, outletEmail, outletHttp } from '@prostojs/wf/outlets';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Registers a method as a workflow step handler.
|
|
@@ -38,6 +39,23 @@ declare function Workflow(path?: string): MethodDecorator;
|
|
|
38
39
|
* @param schema - Array of step definitions composing the workflow.
|
|
39
40
|
*/
|
|
40
41
|
declare function WorkflowSchema<T>(schema: TWorkflowSchema<T>): MethodDecorator;
|
|
42
|
+
/**
|
|
43
|
+
* Sets TTL (in ms) for the workflow state when this step pauses.
|
|
44
|
+
* The adapter wraps the step handler to set `expires` on the outlet signal,
|
|
45
|
+
* which is then passed to `strategy.persist(state, { ttl })`.
|
|
46
|
+
*
|
|
47
|
+
* @param ttlMs - Time-to-live in milliseconds for the paused state token.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* @Step('send-invite')
|
|
52
|
+
* @StepTTL(60 * 60 * 1000) // 1 hour
|
|
53
|
+
* async sendInvite(@WorkflowParam('context') ctx: any) {
|
|
54
|
+
* return outletEmail(ctx.email, 'invite')
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare function StepTTL(ttlMs: number): MethodDecorator;
|
|
41
59
|
/**
|
|
42
60
|
* Parameter decorator that resolves a workflow context value into a step handler argument.
|
|
43
61
|
*
|
|
@@ -92,6 +110,19 @@ declare class MoostWf<T = any, IR = any> implements TMoostAdapter<TWfHandlerMeta
|
|
|
92
110
|
attachSpy<I>(fn: TWorkflowSpy<T, I, IR>): () => void;
|
|
93
111
|
/** Detaches a previously attached workflow spy. */
|
|
94
112
|
detachSpy<I>(fn: TWorkflowSpy<T, I, IR>): void;
|
|
113
|
+
/**
|
|
114
|
+
* Handles an outlet trigger request within an HTTP handler.
|
|
115
|
+
*
|
|
116
|
+
* Reads `wfid` (workflow ID) and `wfs` (state token) from the HTTP request,
|
|
117
|
+
* starts or resumes the workflow, and dispatches pauses to registered outlets.
|
|
118
|
+
*
|
|
119
|
+
* Must be called from within an HTTP event context (e.g. a `@Post` handler)
|
|
120
|
+
* so that wooks HTTP composables are available.
|
|
121
|
+
*
|
|
122
|
+
* @param config - Outlet trigger configuration (allowed workflows, state strategy, outlets, etc.)
|
|
123
|
+
* @returns The outlet result (form payload + token), finished response, or error.
|
|
124
|
+
*/
|
|
125
|
+
handleOutlet(config: WfOutletTriggerConfig): Promise<unknown>;
|
|
95
126
|
/**
|
|
96
127
|
* Starts a new workflow execution.
|
|
97
128
|
*
|
|
@@ -114,5 +145,5 @@ declare class MoostWf<T = any, IR = any> implements TMoostAdapter<TWfHandlerMeta
|
|
|
114
145
|
bindHandler<T extends object = object>(opts: TMoostAdapterOptions<TWfHandlerMeta, T>): void;
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
export { MoostWf, Step, Workflow, WorkflowParam, WorkflowSchema };
|
|
148
|
+
export { MoostWf, Step, StepTTL, Workflow, WorkflowParam, WorkflowSchema };
|
|
118
149
|
export type { TWfHandlerMeta };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { WooksWf, createWfApp, useWfState, useWfState as useWfState$1, wfKind } from "@wooksjs/event-wf";
|
|
1
|
+
import { WooksWf, createEmailOutlet, createHttpOutlet, createOutletHandler, createWfApp, handleWfOutletRequest, handleWfOutletRequest as handleWfOutletRequest$1, useWfFinished, useWfOutlet, useWfState, useWfState as useWfState$1, wfKind } from "@wooksjs/event-wf";
|
|
2
2
|
import { Resolve, defineMoostEventHandler, getMoostInfact, getMoostMate, setControllerContext, useScopeId } from "moost";
|
|
3
3
|
import { StepRetriableError } from "@prostojs/wf";
|
|
4
|
+
import { createCipheriv, createDecipheriv, randomBytes, randomUUID } from "node:crypto";
|
|
4
5
|
|
|
5
6
|
//#region packages/event-wf/src/meta-types.ts
|
|
6
7
|
function getWfMate() {
|
|
@@ -53,6 +54,24 @@ function getWfMate() {
|
|
|
53
54
|
return getWfMate().decorate("wfSchema", schema);
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
57
|
+
* Sets TTL (in ms) for the workflow state when this step pauses.
|
|
58
|
+
* The adapter wraps the step handler to set `expires` on the outlet signal,
|
|
59
|
+
* which is then passed to `strategy.persist(state, { ttl })`.
|
|
60
|
+
*
|
|
61
|
+
* @param ttlMs - Time-to-live in milliseconds for the paused state token.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* @Step('send-invite')
|
|
66
|
+
* @StepTTL(60 * 60 * 1000) // 1 hour
|
|
67
|
+
* async sendInvite(@WorkflowParam('context') ctx: any) {
|
|
68
|
+
* return outletEmail(ctx.email, 'invite')
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
*/ function StepTTL(ttlMs) {
|
|
72
|
+
return getWfMate().decorate("wfStepTTL", ttlMs);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
56
75
|
* Parameter decorator that resolves a workflow context value into a step handler argument.
|
|
57
76
|
*
|
|
58
77
|
* @param name - The workflow value to resolve:
|
|
@@ -137,6 +156,23 @@ const CONTEXT_TYPE = "WF";
|
|
|
137
156
|
this.wfApp.detachSpy(fn);
|
|
138
157
|
}
|
|
139
158
|
/**
|
|
159
|
+
* Handles an outlet trigger request within an HTTP handler.
|
|
160
|
+
*
|
|
161
|
+
* Reads `wfid` (workflow ID) and `wfs` (state token) from the HTTP request,
|
|
162
|
+
* starts or resumes the workflow, and dispatches pauses to registered outlets.
|
|
163
|
+
*
|
|
164
|
+
* Must be called from within an HTTP event context (e.g. a `@Post` handler)
|
|
165
|
+
* so that wooks HTTP composables are available.
|
|
166
|
+
*
|
|
167
|
+
* @param config - Outlet trigger configuration (allowed workflows, state strategy, outlets, etc.)
|
|
168
|
+
* @returns The outlet result (form payload + token), finished response, or error.
|
|
169
|
+
*/ handleOutlet(config) {
|
|
170
|
+
return handleWfOutletRequest$1(config, {
|
|
171
|
+
start: (schemaId, context, opts) => this.start(schemaId, context, opts?.input),
|
|
172
|
+
resume: (state, opts) => this.resume(state, opts?.input)
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
140
176
|
* Starts a new workflow execution.
|
|
141
177
|
*
|
|
142
178
|
* @param schemaId - Identifier of the registered workflow schema.
|
|
@@ -180,17 +216,31 @@ const CONTEXT_TYPE = "WF";
|
|
|
180
216
|
resolveArgs: opts.resolveArgs,
|
|
181
217
|
manualUnscope: true,
|
|
182
218
|
targetPath,
|
|
219
|
+
controllerPrefix: opts.prefix,
|
|
183
220
|
handlerType: handler.type
|
|
184
221
|
});
|
|
185
222
|
if (handler.type === "WF_STEP") {
|
|
186
|
-
|
|
223
|
+
const stepTTL = getWfMate().read(opts.fakeInstance, opts.method)?.wfStepTTL;
|
|
224
|
+
let stepHandler = fn;
|
|
225
|
+
if (stepTTL !== void 0) {
|
|
226
|
+
const wrapped = stepHandler;
|
|
227
|
+
stepHandler = async () => {
|
|
228
|
+
const result = await wrapped();
|
|
229
|
+
if (result && typeof result === "object" && "inputRequired" in result) return {
|
|
230
|
+
...result,
|
|
231
|
+
expires: Date.now() + stepTTL
|
|
232
|
+
};
|
|
233
|
+
return result;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
this.wfApp.step(targetPath, { handler: stepHandler });
|
|
187
237
|
opts.logHandler(`[36m(${handler.type})[32m${targetPath}`);
|
|
188
238
|
} else {
|
|
189
239
|
const mate = getWfMate();
|
|
190
240
|
let wfSchema = mate.read(opts.fakeInstance, opts.method)?.wfSchema;
|
|
191
241
|
if (!wfSchema) wfSchema = mate.read(opts.fakeInstance)?.wfSchema;
|
|
192
242
|
const _fn = async () => {
|
|
193
|
-
setControllerContext(this.moost, "bindHandler", targetPath);
|
|
243
|
+
setControllerContext(this.moost, "bindHandler", targetPath, { prefix: opts.prefix });
|
|
194
244
|
return fn();
|
|
195
245
|
};
|
|
196
246
|
this.toInit.push(() => {
|
|
@@ -218,4 +268,181 @@ const CONTEXT_TYPE = "WF";
|
|
|
218
268
|
};
|
|
219
269
|
|
|
220
270
|
//#endregion
|
|
221
|
-
|
|
271
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.1.1/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
272
|
+
/**
|
|
273
|
+
* Generic outlet request. Use for custom outlets.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* return outlet('pending-task', {
|
|
277
|
+
* payload: ApprovalForm,
|
|
278
|
+
* target: managerId,
|
|
279
|
+
* context: { orderId, amount },
|
|
280
|
+
* })
|
|
281
|
+
*/
|
|
282
|
+
function outlet(name, data) {
|
|
283
|
+
return { inputRequired: {
|
|
284
|
+
outlet: name,
|
|
285
|
+
...data
|
|
286
|
+
} };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
290
|
+
* and state token in the HTTP response.
|
|
291
|
+
*
|
|
292
|
+
* @example
|
|
293
|
+
* return outletHttp(LoginForm)
|
|
294
|
+
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
295
|
+
*/
|
|
296
|
+
function outletHttp(payload, context) {
|
|
297
|
+
return outlet("http", {
|
|
298
|
+
payload,
|
|
299
|
+
context
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Pause and send email with a magic link containing the state token.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
307
|
+
*/
|
|
308
|
+
function outletEmail(target, template, context) {
|
|
309
|
+
return outlet("email", {
|
|
310
|
+
target,
|
|
311
|
+
template,
|
|
312
|
+
context
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Self-contained AES-256-GCM encrypted state strategy.
|
|
317
|
+
*
|
|
318
|
+
* Workflow state is encrypted into a base64url token that travels with the
|
|
319
|
+
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
320
|
+
*
|
|
321
|
+
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* const strategy = new EncapsulatedStateStrategy({
|
|
325
|
+
* secret: crypto.randomBytes(32),
|
|
326
|
+
* defaultTtl: 3600_000, // 1 hour
|
|
327
|
+
* });
|
|
328
|
+
* const token = await strategy.persist(state);
|
|
329
|
+
* const recovered = await strategy.retrieve(token);
|
|
330
|
+
*/
|
|
331
|
+
var EncapsulatedStateStrategy = class {
|
|
332
|
+
/** @throws if secret is not exactly 32 bytes */
|
|
333
|
+
constructor(config) {
|
|
334
|
+
this.config = config;
|
|
335
|
+
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
336
|
+
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Encrypt workflow state into a self-contained token.
|
|
340
|
+
* @param state — workflow state to persist
|
|
341
|
+
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
342
|
+
* @returns base64url-encoded encrypted token
|
|
343
|
+
*/
|
|
344
|
+
async persist(state, options) {
|
|
345
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
346
|
+
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
347
|
+
const payload = JSON.stringify({
|
|
348
|
+
s: state,
|
|
349
|
+
e: exp
|
|
350
|
+
});
|
|
351
|
+
const iv = randomBytes(12);
|
|
352
|
+
const cipher = createCipheriv("aes-256-gcm", this.key, iv);
|
|
353
|
+
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
354
|
+
const tag = cipher.getAuthTag();
|
|
355
|
+
return Buffer.concat([
|
|
356
|
+
iv,
|
|
357
|
+
tag,
|
|
358
|
+
encrypted
|
|
359
|
+
]).toString("base64url");
|
|
360
|
+
}
|
|
361
|
+
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
362
|
+
async retrieve(token) {
|
|
363
|
+
return this.decrypt(token);
|
|
364
|
+
}
|
|
365
|
+
/** Same as retrieve (stateless — cannot truly invalidate a token). */
|
|
366
|
+
async consume(token) {
|
|
367
|
+
return this.decrypt(token);
|
|
368
|
+
}
|
|
369
|
+
decrypt(token) {
|
|
370
|
+
try {
|
|
371
|
+
const buf = Buffer.from(token, "base64url");
|
|
372
|
+
if (buf.length < 28) return null;
|
|
373
|
+
const iv = buf.subarray(0, 12);
|
|
374
|
+
const tag = buf.subarray(12, 28);
|
|
375
|
+
const ciphertext = buf.subarray(28);
|
|
376
|
+
const decipher = createDecipheriv("aes-256-gcm", this.key, iv);
|
|
377
|
+
decipher.setAuthTag(tag);
|
|
378
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
379
|
+
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
380
|
+
if (exp > 0 && Date.now() > exp) return null;
|
|
381
|
+
return state;
|
|
382
|
+
} catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
var HandleStateStrategy = class {
|
|
388
|
+
constructor(config) {
|
|
389
|
+
this.config = config;
|
|
390
|
+
}
|
|
391
|
+
async persist(state, options) {
|
|
392
|
+
const handle = (this.config.generateHandle ?? randomUUID)();
|
|
393
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
394
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
395
|
+
await this.config.store.set(handle, state, expiresAt);
|
|
396
|
+
return handle;
|
|
397
|
+
}
|
|
398
|
+
async retrieve(token) {
|
|
399
|
+
return (await this.config.store.get(token))?.state ?? null;
|
|
400
|
+
}
|
|
401
|
+
async consume(token) {
|
|
402
|
+
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* In-memory state store for development and testing.
|
|
407
|
+
* State is lost on process restart.
|
|
408
|
+
*/
|
|
409
|
+
var WfStateStoreMemory = class {
|
|
410
|
+
constructor() {
|
|
411
|
+
this.store = /* @__PURE__ */ new Map();
|
|
412
|
+
}
|
|
413
|
+
async set(handle, state, expiresAt) {
|
|
414
|
+
this.store.set(handle, {
|
|
415
|
+
state,
|
|
416
|
+
expiresAt
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async get(handle) {
|
|
420
|
+
const entry = this.store.get(handle);
|
|
421
|
+
if (!entry) return null;
|
|
422
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
423
|
+
this.store.delete(handle);
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return entry;
|
|
427
|
+
}
|
|
428
|
+
async delete(handle) {
|
|
429
|
+
this.store.delete(handle);
|
|
430
|
+
}
|
|
431
|
+
async getAndDelete(handle) {
|
|
432
|
+
const entry = await this.get(handle);
|
|
433
|
+
if (entry) this.store.delete(handle);
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
436
|
+
async cleanup() {
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
let count = 0;
|
|
439
|
+
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
440
|
+
this.store.delete(handle);
|
|
441
|
+
count++;
|
|
442
|
+
}
|
|
443
|
+
return count;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
//#endregion
|
|
448
|
+
export { EncapsulatedStateStrategy, HandleStateStrategy, MoostWf, Step, StepRetriableError, StepTTL, WfStateStoreMemory, Workflow, WorkflowParam, WorkflowSchema, createEmailOutlet, createHttpOutlet, createOutletHandler, handleWfOutletRequest, outlet, outletEmail, outletHttp, useWfFinished, useWfOutlet, useWfState, wfKind };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moostjs/event-wf",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "@moostjs/event-wf",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"composables",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@prostojs/wf": "^0.
|
|
47
|
-
"@wooksjs/event-wf": "^0.7.
|
|
46
|
+
"@prostojs/wf": "^0.1.1",
|
|
47
|
+
"@wooksjs/event-wf": "^0.7.8"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"vitest": "3.2.4"
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@prostojs/infact": "^0.4.1",
|
|
54
54
|
"@prostojs/mate": "^0.4.0",
|
|
55
|
-
"@wooksjs/event-core": "^0.7.
|
|
56
|
-
"wooks": "^0.7.
|
|
57
|
-
"moost": "^0.6.
|
|
55
|
+
"@wooksjs/event-core": "^0.7.8",
|
|
56
|
+
"wooks": "^0.7.8",
|
|
57
|
+
"moost": "^0.6.6"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"pub": "pnpm publish --access public",
|