@moostjs/event-wf 0.6.4 → 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 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
- this.wfApp.step(targetPath, { handler: fn });
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(`(${handler.type})${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
- this.wfApp.step(targetPath, { handler: fn });
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(`(${handler.type})${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
- export { MoostWf, Step, StepRetriableError, Workflow, WorkflowParam, WorkflowSchema, useWfState, wfKind };
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.4",
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.0.18",
47
- "@wooksjs/event-wf": "^0.7.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.7",
56
- "wooks": "^0.7.7",
57
- "moost": "^0.6.4"
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",