@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 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
- this.wfApp.step(targetPath, { handler: fn });
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(`(${handler.type})${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
- this.wfApp.step(targetPath, { handler: fn });
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(`(${handler.type})${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
- export { MoostWf, Step, StepRetriableError, Workflow, WorkflowParam, WorkflowSchema, useWfState, wfKind };
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.4",
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.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.6"
58
58
  },
59
59
  "scripts": {
60
60
  "pub": "pnpm publish --access public",