@moostjs/event-wf 0.6.3 → 0.6.5
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 +270 -1
- package/dist/index.d.ts +34 -3
- package/dist/index.mjs +229 -3
- 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.
|
|
@@ -206,7 +242,20 @@ const CONTEXT_TYPE = "WF";
|
|
|
206
242
|
handlerType: handler.type
|
|
207
243
|
});
|
|
208
244
|
if (handler.type === "WF_STEP") {
|
|
209
|
-
|
|
245
|
+
const stepTTL = getWfMate().read(opts.fakeInstance, opts.method)?.wfStepTTL;
|
|
246
|
+
let stepHandler = fn;
|
|
247
|
+
if (stepTTL !== void 0) {
|
|
248
|
+
const wrapped = stepHandler;
|
|
249
|
+
stepHandler = async () => {
|
|
250
|
+
const result = await wrapped();
|
|
251
|
+
if (result && typeof result === "object" && "inputRequired" in result) return {
|
|
252
|
+
...result,
|
|
253
|
+
expires: Date.now() + stepTTL
|
|
254
|
+
};
|
|
255
|
+
return result;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
this.wfApp.step(targetPath, { handler: stepHandler });
|
|
210
259
|
opts.logHandler(`[36m(${handler.type})[32m${targetPath}`);
|
|
211
260
|
} else {
|
|
212
261
|
const mate = getWfMate();
|
|
@@ -241,6 +290,185 @@ const CONTEXT_TYPE = "WF";
|
|
|
241
290
|
};
|
|
242
291
|
|
|
243
292
|
//#endregion
|
|
293
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.1.1/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
294
|
+
/**
|
|
295
|
+
* Generic outlet request. Use for custom outlets.
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* return outlet('pending-task', {
|
|
299
|
+
* payload: ApprovalForm,
|
|
300
|
+
* target: managerId,
|
|
301
|
+
* context: { orderId, amount },
|
|
302
|
+
* })
|
|
303
|
+
*/
|
|
304
|
+
function outlet(name, data) {
|
|
305
|
+
return { inputRequired: {
|
|
306
|
+
outlet: name,
|
|
307
|
+
...data
|
|
308
|
+
} };
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
312
|
+
* and state token in the HTTP response.
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* return outletHttp(LoginForm)
|
|
316
|
+
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
317
|
+
*/
|
|
318
|
+
function outletHttp(payload, context) {
|
|
319
|
+
return outlet("http", {
|
|
320
|
+
payload,
|
|
321
|
+
context
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Pause and send email with a magic link containing the state token.
|
|
326
|
+
*
|
|
327
|
+
* @example
|
|
328
|
+
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
329
|
+
*/
|
|
330
|
+
function outletEmail(target, template, context) {
|
|
331
|
+
return outlet("email", {
|
|
332
|
+
target,
|
|
333
|
+
template,
|
|
334
|
+
context
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Self-contained AES-256-GCM encrypted state strategy.
|
|
339
|
+
*
|
|
340
|
+
* Workflow state is encrypted into a base64url token that travels with the
|
|
341
|
+
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
342
|
+
*
|
|
343
|
+
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
344
|
+
*
|
|
345
|
+
* @example
|
|
346
|
+
* const strategy = new EncapsulatedStateStrategy({
|
|
347
|
+
* secret: crypto.randomBytes(32),
|
|
348
|
+
* defaultTtl: 3600_000, // 1 hour
|
|
349
|
+
* });
|
|
350
|
+
* const token = await strategy.persist(state);
|
|
351
|
+
* const recovered = await strategy.retrieve(token);
|
|
352
|
+
*/
|
|
353
|
+
var EncapsulatedStateStrategy = class {
|
|
354
|
+
/** @throws if secret is not exactly 32 bytes */
|
|
355
|
+
constructor(config) {
|
|
356
|
+
this.config = config;
|
|
357
|
+
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
358
|
+
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Encrypt workflow state into a self-contained token.
|
|
362
|
+
* @param state — workflow state to persist
|
|
363
|
+
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
364
|
+
* @returns base64url-encoded encrypted token
|
|
365
|
+
*/
|
|
366
|
+
async persist(state, options) {
|
|
367
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
368
|
+
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
369
|
+
const payload = JSON.stringify({
|
|
370
|
+
s: state,
|
|
371
|
+
e: exp
|
|
372
|
+
});
|
|
373
|
+
const iv = (0, node_crypto.randomBytes)(12);
|
|
374
|
+
const cipher = (0, node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
|
|
375
|
+
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
376
|
+
const tag = cipher.getAuthTag();
|
|
377
|
+
return Buffer.concat([
|
|
378
|
+
iv,
|
|
379
|
+
tag,
|
|
380
|
+
encrypted
|
|
381
|
+
]).toString("base64url");
|
|
382
|
+
}
|
|
383
|
+
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
384
|
+
async retrieve(token) {
|
|
385
|
+
return this.decrypt(token);
|
|
386
|
+
}
|
|
387
|
+
/** Same as retrieve (stateless — cannot truly invalidate a token). */
|
|
388
|
+
async consume(token) {
|
|
389
|
+
return this.decrypt(token);
|
|
390
|
+
}
|
|
391
|
+
decrypt(token) {
|
|
392
|
+
try {
|
|
393
|
+
const buf = Buffer.from(token, "base64url");
|
|
394
|
+
if (buf.length < 28) return null;
|
|
395
|
+
const iv = buf.subarray(0, 12);
|
|
396
|
+
const tag = buf.subarray(12, 28);
|
|
397
|
+
const ciphertext = buf.subarray(28);
|
|
398
|
+
const decipher = (0, node_crypto.createDecipheriv)("aes-256-gcm", this.key, iv);
|
|
399
|
+
decipher.setAuthTag(tag);
|
|
400
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
401
|
+
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
402
|
+
if (exp > 0 && Date.now() > exp) return null;
|
|
403
|
+
return state;
|
|
404
|
+
} catch {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var HandleStateStrategy = class {
|
|
410
|
+
constructor(config) {
|
|
411
|
+
this.config = config;
|
|
412
|
+
}
|
|
413
|
+
async persist(state, options) {
|
|
414
|
+
const handle = (this.config.generateHandle ?? node_crypto.randomUUID)();
|
|
415
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
416
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
417
|
+
await this.config.store.set(handle, state, expiresAt);
|
|
418
|
+
return handle;
|
|
419
|
+
}
|
|
420
|
+
async retrieve(token) {
|
|
421
|
+
return (await this.config.store.get(token))?.state ?? null;
|
|
422
|
+
}
|
|
423
|
+
async consume(token) {
|
|
424
|
+
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
/**
|
|
428
|
+
* In-memory state store for development and testing.
|
|
429
|
+
* State is lost on process restart.
|
|
430
|
+
*/
|
|
431
|
+
var WfStateStoreMemory = class {
|
|
432
|
+
constructor() {
|
|
433
|
+
this.store = /* @__PURE__ */ new Map();
|
|
434
|
+
}
|
|
435
|
+
async set(handle, state, expiresAt) {
|
|
436
|
+
this.store.set(handle, {
|
|
437
|
+
state,
|
|
438
|
+
expiresAt
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async get(handle) {
|
|
442
|
+
const entry = this.store.get(handle);
|
|
443
|
+
if (!entry) return null;
|
|
444
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
445
|
+
this.store.delete(handle);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return entry;
|
|
449
|
+
}
|
|
450
|
+
async delete(handle) {
|
|
451
|
+
this.store.delete(handle);
|
|
452
|
+
}
|
|
453
|
+
async getAndDelete(handle) {
|
|
454
|
+
const entry = await this.get(handle);
|
|
455
|
+
if (entry) this.store.delete(handle);
|
|
456
|
+
return entry;
|
|
457
|
+
}
|
|
458
|
+
async cleanup() {
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
let count = 0;
|
|
461
|
+
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
462
|
+
this.store.delete(handle);
|
|
463
|
+
count++;
|
|
464
|
+
}
|
|
465
|
+
return count;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
//#endregion
|
|
470
|
+
exports.EncapsulatedStateStrategy = EncapsulatedStateStrategy;
|
|
471
|
+
exports.HandleStateStrategy = HandleStateStrategy;
|
|
244
472
|
exports.MoostWf = MoostWf;
|
|
245
473
|
exports.Step = Step;
|
|
246
474
|
Object.defineProperty(exports, 'StepRetriableError', {
|
|
@@ -249,9 +477,50 @@ Object.defineProperty(exports, 'StepRetriableError', {
|
|
|
249
477
|
return __prostojs_wf.StepRetriableError;
|
|
250
478
|
}
|
|
251
479
|
});
|
|
480
|
+
exports.StepTTL = StepTTL;
|
|
481
|
+
exports.WfStateStoreMemory = WfStateStoreMemory;
|
|
252
482
|
exports.Workflow = Workflow;
|
|
253
483
|
exports.WorkflowParam = WorkflowParam;
|
|
254
484
|
exports.WorkflowSchema = WorkflowSchema;
|
|
485
|
+
Object.defineProperty(exports, 'createEmailOutlet', {
|
|
486
|
+
enumerable: true,
|
|
487
|
+
get: function () {
|
|
488
|
+
return __wooksjs_event_wf.createEmailOutlet;
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
Object.defineProperty(exports, 'createHttpOutlet', {
|
|
492
|
+
enumerable: true,
|
|
493
|
+
get: function () {
|
|
494
|
+
return __wooksjs_event_wf.createHttpOutlet;
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
Object.defineProperty(exports, 'createOutletHandler', {
|
|
498
|
+
enumerable: true,
|
|
499
|
+
get: function () {
|
|
500
|
+
return __wooksjs_event_wf.createOutletHandler;
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
Object.defineProperty(exports, 'handleWfOutletRequest', {
|
|
504
|
+
enumerable: true,
|
|
505
|
+
get: function () {
|
|
506
|
+
return __wooksjs_event_wf.handleWfOutletRequest;
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
exports.outlet = outlet;
|
|
510
|
+
exports.outletEmail = outletEmail;
|
|
511
|
+
exports.outletHttp = outletHttp;
|
|
512
|
+
Object.defineProperty(exports, 'useWfFinished', {
|
|
513
|
+
enumerable: true,
|
|
514
|
+
get: function () {
|
|
515
|
+
return __wooksjs_event_wf.useWfFinished;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
Object.defineProperty(exports, 'useWfOutlet', {
|
|
519
|
+
enumerable: true,
|
|
520
|
+
get: function () {
|
|
521
|
+
return __wooksjs_event_wf.useWfOutlet;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
255
524
|
Object.defineProperty(exports, 'useWfState', {
|
|
256
525
|
enumerable: true,
|
|
257
526
|
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.
|
|
@@ -183,7 +219,20 @@ const CONTEXT_TYPE = "WF";
|
|
|
183
219
|
handlerType: handler.type
|
|
184
220
|
});
|
|
185
221
|
if (handler.type === "WF_STEP") {
|
|
186
|
-
|
|
222
|
+
const stepTTL = getWfMate().read(opts.fakeInstance, opts.method)?.wfStepTTL;
|
|
223
|
+
let stepHandler = fn;
|
|
224
|
+
if (stepTTL !== void 0) {
|
|
225
|
+
const wrapped = stepHandler;
|
|
226
|
+
stepHandler = async () => {
|
|
227
|
+
const result = await wrapped();
|
|
228
|
+
if (result && typeof result === "object" && "inputRequired" in result) return {
|
|
229
|
+
...result,
|
|
230
|
+
expires: Date.now() + stepTTL
|
|
231
|
+
};
|
|
232
|
+
return result;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
this.wfApp.step(targetPath, { handler: stepHandler });
|
|
187
236
|
opts.logHandler(`[36m(${handler.type})[32m${targetPath}`);
|
|
188
237
|
} else {
|
|
189
238
|
const mate = getWfMate();
|
|
@@ -218,4 +267,181 @@ const CONTEXT_TYPE = "WF";
|
|
|
218
267
|
};
|
|
219
268
|
|
|
220
269
|
//#endregion
|
|
221
|
-
|
|
270
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.1.1/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
271
|
+
/**
|
|
272
|
+
* Generic outlet request. Use for custom outlets.
|
|
273
|
+
*
|
|
274
|
+
* @example
|
|
275
|
+
* return outlet('pending-task', {
|
|
276
|
+
* payload: ApprovalForm,
|
|
277
|
+
* target: managerId,
|
|
278
|
+
* context: { orderId, amount },
|
|
279
|
+
* })
|
|
280
|
+
*/
|
|
281
|
+
function outlet(name, data) {
|
|
282
|
+
return { inputRequired: {
|
|
283
|
+
outlet: name,
|
|
284
|
+
...data
|
|
285
|
+
} };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
289
|
+
* and state token in the HTTP response.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* return outletHttp(LoginForm)
|
|
293
|
+
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
294
|
+
*/
|
|
295
|
+
function outletHttp(payload, context) {
|
|
296
|
+
return outlet("http", {
|
|
297
|
+
payload,
|
|
298
|
+
context
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Pause and send email with a magic link containing the state token.
|
|
303
|
+
*
|
|
304
|
+
* @example
|
|
305
|
+
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
306
|
+
*/
|
|
307
|
+
function outletEmail(target, template, context) {
|
|
308
|
+
return outlet("email", {
|
|
309
|
+
target,
|
|
310
|
+
template,
|
|
311
|
+
context
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Self-contained AES-256-GCM encrypted state strategy.
|
|
316
|
+
*
|
|
317
|
+
* Workflow state is encrypted into a base64url token that travels with the
|
|
318
|
+
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
319
|
+
*
|
|
320
|
+
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
321
|
+
*
|
|
322
|
+
* @example
|
|
323
|
+
* const strategy = new EncapsulatedStateStrategy({
|
|
324
|
+
* secret: crypto.randomBytes(32),
|
|
325
|
+
* defaultTtl: 3600_000, // 1 hour
|
|
326
|
+
* });
|
|
327
|
+
* const token = await strategy.persist(state);
|
|
328
|
+
* const recovered = await strategy.retrieve(token);
|
|
329
|
+
*/
|
|
330
|
+
var EncapsulatedStateStrategy = class {
|
|
331
|
+
/** @throws if secret is not exactly 32 bytes */
|
|
332
|
+
constructor(config) {
|
|
333
|
+
this.config = config;
|
|
334
|
+
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
335
|
+
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Encrypt workflow state into a self-contained token.
|
|
339
|
+
* @param state — workflow state to persist
|
|
340
|
+
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
341
|
+
* @returns base64url-encoded encrypted token
|
|
342
|
+
*/
|
|
343
|
+
async persist(state, options) {
|
|
344
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
345
|
+
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
346
|
+
const payload = JSON.stringify({
|
|
347
|
+
s: state,
|
|
348
|
+
e: exp
|
|
349
|
+
});
|
|
350
|
+
const iv = randomBytes(12);
|
|
351
|
+
const cipher = createCipheriv("aes-256-gcm", this.key, iv);
|
|
352
|
+
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
353
|
+
const tag = cipher.getAuthTag();
|
|
354
|
+
return Buffer.concat([
|
|
355
|
+
iv,
|
|
356
|
+
tag,
|
|
357
|
+
encrypted
|
|
358
|
+
]).toString("base64url");
|
|
359
|
+
}
|
|
360
|
+
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
361
|
+
async retrieve(token) {
|
|
362
|
+
return this.decrypt(token);
|
|
363
|
+
}
|
|
364
|
+
/** Same as retrieve (stateless — cannot truly invalidate a token). */
|
|
365
|
+
async consume(token) {
|
|
366
|
+
return this.decrypt(token);
|
|
367
|
+
}
|
|
368
|
+
decrypt(token) {
|
|
369
|
+
try {
|
|
370
|
+
const buf = Buffer.from(token, "base64url");
|
|
371
|
+
if (buf.length < 28) return null;
|
|
372
|
+
const iv = buf.subarray(0, 12);
|
|
373
|
+
const tag = buf.subarray(12, 28);
|
|
374
|
+
const ciphertext = buf.subarray(28);
|
|
375
|
+
const decipher = createDecipheriv("aes-256-gcm", this.key, iv);
|
|
376
|
+
decipher.setAuthTag(tag);
|
|
377
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
378
|
+
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
379
|
+
if (exp > 0 && Date.now() > exp) return null;
|
|
380
|
+
return state;
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var HandleStateStrategy = class {
|
|
387
|
+
constructor(config) {
|
|
388
|
+
this.config = config;
|
|
389
|
+
}
|
|
390
|
+
async persist(state, options) {
|
|
391
|
+
const handle = (this.config.generateHandle ?? randomUUID)();
|
|
392
|
+
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
393
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
394
|
+
await this.config.store.set(handle, state, expiresAt);
|
|
395
|
+
return handle;
|
|
396
|
+
}
|
|
397
|
+
async retrieve(token) {
|
|
398
|
+
return (await this.config.store.get(token))?.state ?? null;
|
|
399
|
+
}
|
|
400
|
+
async consume(token) {
|
|
401
|
+
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
/**
|
|
405
|
+
* In-memory state store for development and testing.
|
|
406
|
+
* State is lost on process restart.
|
|
407
|
+
*/
|
|
408
|
+
var WfStateStoreMemory = class {
|
|
409
|
+
constructor() {
|
|
410
|
+
this.store = /* @__PURE__ */ new Map();
|
|
411
|
+
}
|
|
412
|
+
async set(handle, state, expiresAt) {
|
|
413
|
+
this.store.set(handle, {
|
|
414
|
+
state,
|
|
415
|
+
expiresAt
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async get(handle) {
|
|
419
|
+
const entry = this.store.get(handle);
|
|
420
|
+
if (!entry) return null;
|
|
421
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
422
|
+
this.store.delete(handle);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return entry;
|
|
426
|
+
}
|
|
427
|
+
async delete(handle) {
|
|
428
|
+
this.store.delete(handle);
|
|
429
|
+
}
|
|
430
|
+
async getAndDelete(handle) {
|
|
431
|
+
const entry = await this.get(handle);
|
|
432
|
+
if (entry) this.store.delete(handle);
|
|
433
|
+
return entry;
|
|
434
|
+
}
|
|
435
|
+
async cleanup() {
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
let count = 0;
|
|
438
|
+
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
439
|
+
this.store.delete(handle);
|
|
440
|
+
count++;
|
|
441
|
+
}
|
|
442
|
+
return count;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
//#endregion
|
|
447
|
+
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.5",
|
|
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.5"
|
|
58
58
|
},
|
|
59
59
|
"scripts": {
|
|
60
60
|
"pub": "pnpm publish --access public",
|