@wooksjs/event-wf 0.7.13 → 0.7.15
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 +44 -216
- package/dist/index.mjs +8 -210
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -2,7 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
let _wooksjs_event_core = require("@wooksjs/event-core");
|
|
3
3
|
let _wooksjs_event_http = require("@wooksjs/event-http");
|
|
4
4
|
let _wooksjs_http_body = require("@wooksjs/http-body");
|
|
5
|
-
let
|
|
5
|
+
let _prostojs_wf_outlets = require("@prostojs/wf/outlets");
|
|
6
6
|
let _prostojs_wf = require("@prostojs/wf");
|
|
7
7
|
let wooks = require("wooks");
|
|
8
8
|
|
|
@@ -146,6 +146,7 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
146
146
|
const input = body?.input;
|
|
147
147
|
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
148
148
|
let output;
|
|
149
|
+
let strategyReResolved = false;
|
|
149
150
|
if (token) {
|
|
150
151
|
const strategy = resolveStrategy(wfid ?? "");
|
|
151
152
|
ctx.set(stateStrategyKey, strategy);
|
|
@@ -156,7 +157,10 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
156
157
|
}
|
|
157
158
|
if (state.schemaId !== (wfid ?? "")) {
|
|
158
159
|
const realStrategy = resolveStrategy(state.schemaId);
|
|
159
|
-
|
|
160
|
+
if (realStrategy !== strategy) {
|
|
161
|
+
ctx.set(stateStrategyKey, realStrategy);
|
|
162
|
+
strategyReResolved = true;
|
|
163
|
+
}
|
|
160
164
|
}
|
|
161
165
|
output = await deps.resume(state, {
|
|
162
166
|
input,
|
|
@@ -212,7 +216,8 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
212
216
|
...output.state,
|
|
213
217
|
meta: { outlet: outletReq.outlet }
|
|
214
218
|
};
|
|
215
|
-
const
|
|
219
|
+
const reuseHandle = token && !strategyReResolved ? { handle: token } : void 0;
|
|
220
|
+
const newToken = await strategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0, reuseHandle);
|
|
216
221
|
const outOfBand = outletHandler.tokenDelivery === "out-of-band";
|
|
217
222
|
if (tokenWrite === "cookie" && !outOfBand) response.setCookie(tokenName, newToken, {
|
|
218
223
|
httpOnly: true,
|
|
@@ -315,213 +320,6 @@ function createOutletHandler(wfApp) {
|
|
|
315
320
|
});
|
|
316
321
|
}
|
|
317
322
|
|
|
318
|
-
//#endregion
|
|
319
|
-
//#region node_modules/.pnpm/@prostojs+wf@0.2.0/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
320
|
-
/**
|
|
321
|
-
* Generic outlet request. Use for custom outlets.
|
|
322
|
-
*
|
|
323
|
-
* @example
|
|
324
|
-
* return outlet('pending-task', {
|
|
325
|
-
* payload: ApprovalForm,
|
|
326
|
-
* target: managerId,
|
|
327
|
-
* context: { orderId, amount },
|
|
328
|
-
* })
|
|
329
|
-
*/
|
|
330
|
-
function outlet(name, data) {
|
|
331
|
-
return { inputRequired: {
|
|
332
|
-
outlet: name,
|
|
333
|
-
...data
|
|
334
|
-
} };
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
338
|
-
* and state token in the HTTP response.
|
|
339
|
-
*
|
|
340
|
-
* @example
|
|
341
|
-
* return outletHttp(LoginForm)
|
|
342
|
-
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
343
|
-
*/
|
|
344
|
-
function outletHttp(payload, context) {
|
|
345
|
-
return outlet("http", {
|
|
346
|
-
payload,
|
|
347
|
-
context
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Pause and send email with a magic link containing the state token.
|
|
352
|
-
*
|
|
353
|
-
* @example
|
|
354
|
-
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
355
|
-
*/
|
|
356
|
-
function outletEmail(target, template, context) {
|
|
357
|
-
return outlet("email", {
|
|
358
|
-
target,
|
|
359
|
-
template,
|
|
360
|
-
context
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* Self-contained AES-256-GCM encrypted state strategy.
|
|
365
|
-
*
|
|
366
|
-
* Workflow state is encrypted into a base64url token that travels with the
|
|
367
|
-
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
368
|
-
*
|
|
369
|
-
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
370
|
-
*
|
|
371
|
-
* ## Security warning — replay
|
|
372
|
-
*
|
|
373
|
-
* This strategy is STATELESS. It cannot enforce single-use semantics:
|
|
374
|
-
* `consume()` is a no-op alias for `retrieve()` because there is no
|
|
375
|
-
* server-side record to delete. Anyone who obtains a copy of the token
|
|
376
|
-
* (browser history, server logs, shoulder-surfing, intermediate proxy,
|
|
377
|
-
* shared device) can replay it until the TTL expires.
|
|
378
|
-
*
|
|
379
|
-
* Use `EncapsulatedStateStrategy` ONLY when BOTH of the following hold:
|
|
380
|
-
*
|
|
381
|
-
* 1. Every workflow step is idempotent — re-executing a step with the same
|
|
382
|
-
* input produces no harmful side effects (pure data collection,
|
|
383
|
-
* validation-only steps).
|
|
384
|
-
* 2. The flow is not security-sensitive — no credential changes, financial
|
|
385
|
-
* operations, account provisioning, permission grants, or any other
|
|
386
|
-
* privileged action.
|
|
387
|
-
*
|
|
388
|
-
* For auth flows (login, password reset, invite accept), financial
|
|
389
|
-
* operations, or anything with meaningful side effects, use
|
|
390
|
-
* `HandleStateStrategy` with a durable `WfStateStore`. `HandleStateStrategy`
|
|
391
|
-
* supports true single-use tokens via atomic `getAndDelete` at the store
|
|
392
|
-
* layer.
|
|
393
|
-
*
|
|
394
|
-
* @example
|
|
395
|
-
* const strategy = new EncapsulatedStateStrategy({
|
|
396
|
-
* secret: crypto.randomBytes(32),
|
|
397
|
-
* defaultTtl: 3600_000, // 1 hour
|
|
398
|
-
* });
|
|
399
|
-
* const token = await strategy.persist(state);
|
|
400
|
-
* const recovered = await strategy.retrieve(token);
|
|
401
|
-
*/
|
|
402
|
-
var EncapsulatedStateStrategy = class {
|
|
403
|
-
/** @throws if secret is not exactly 32 bytes */
|
|
404
|
-
constructor(config) {
|
|
405
|
-
this.config = config;
|
|
406
|
-
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
407
|
-
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Encrypt workflow state into a self-contained token.
|
|
411
|
-
* @param state — workflow state to persist
|
|
412
|
-
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
413
|
-
* @returns base64url-encoded encrypted token
|
|
414
|
-
*/
|
|
415
|
-
async persist(state, options) {
|
|
416
|
-
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
417
|
-
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
418
|
-
const payload = JSON.stringify({
|
|
419
|
-
s: state,
|
|
420
|
-
e: exp
|
|
421
|
-
});
|
|
422
|
-
const iv = (0, node_crypto.randomBytes)(12);
|
|
423
|
-
const cipher = (0, node_crypto.createCipheriv)("aes-256-gcm", this.key, iv);
|
|
424
|
-
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
425
|
-
const tag = cipher.getAuthTag();
|
|
426
|
-
return Buffer.concat([
|
|
427
|
-
iv,
|
|
428
|
-
tag,
|
|
429
|
-
encrypted
|
|
430
|
-
]).toString("base64url");
|
|
431
|
-
}
|
|
432
|
-
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
433
|
-
async retrieve(token) {
|
|
434
|
-
return this.decrypt(token);
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Stateless — CANNOT invalidate the token. Returns identical result to
|
|
438
|
-
* `retrieve()`. See the class-level security warning.
|
|
439
|
-
*
|
|
440
|
-
* This method exists only to satisfy the `WfStateStrategy` contract.
|
|
441
|
-
* Callers that need true single-use semantics must use
|
|
442
|
-
* `HandleStateStrategy`.
|
|
443
|
-
*/
|
|
444
|
-
async consume(token) {
|
|
445
|
-
return this.decrypt(token);
|
|
446
|
-
}
|
|
447
|
-
decrypt(token) {
|
|
448
|
-
try {
|
|
449
|
-
const buf = Buffer.from(token, "base64url");
|
|
450
|
-
if (buf.length < 28) return null;
|
|
451
|
-
const iv = buf.subarray(0, 12);
|
|
452
|
-
const tag = buf.subarray(12, 28);
|
|
453
|
-
const ciphertext = buf.subarray(28);
|
|
454
|
-
const decipher = (0, node_crypto.createDecipheriv)("aes-256-gcm", this.key, iv);
|
|
455
|
-
decipher.setAuthTag(tag);
|
|
456
|
-
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
457
|
-
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
458
|
-
if (exp > 0 && Date.now() > exp) return null;
|
|
459
|
-
return state;
|
|
460
|
-
} catch {
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
};
|
|
465
|
-
var HandleStateStrategy = class {
|
|
466
|
-
constructor(config) {
|
|
467
|
-
this.config = config;
|
|
468
|
-
}
|
|
469
|
-
async persist(state, options) {
|
|
470
|
-
const handle = (this.config.generateHandle ?? node_crypto.randomUUID)();
|
|
471
|
-
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
472
|
-
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
473
|
-
await this.config.store.set(handle, state, expiresAt);
|
|
474
|
-
return handle;
|
|
475
|
-
}
|
|
476
|
-
async retrieve(token) {
|
|
477
|
-
return (await this.config.store.get(token))?.state ?? null;
|
|
478
|
-
}
|
|
479
|
-
async consume(token) {
|
|
480
|
-
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
481
|
-
}
|
|
482
|
-
};
|
|
483
|
-
/**
|
|
484
|
-
* In-memory state store for development and testing.
|
|
485
|
-
* State is lost on process restart.
|
|
486
|
-
*/
|
|
487
|
-
var WfStateStoreMemory = class {
|
|
488
|
-
constructor() {
|
|
489
|
-
this.store = /* @__PURE__ */ new Map();
|
|
490
|
-
}
|
|
491
|
-
async set(handle, state, expiresAt) {
|
|
492
|
-
this.store.set(handle, {
|
|
493
|
-
state,
|
|
494
|
-
expiresAt
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
async get(handle) {
|
|
498
|
-
const entry = this.store.get(handle);
|
|
499
|
-
if (!entry) return null;
|
|
500
|
-
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
501
|
-
this.store.delete(handle);
|
|
502
|
-
return null;
|
|
503
|
-
}
|
|
504
|
-
return entry;
|
|
505
|
-
}
|
|
506
|
-
async delete(handle) {
|
|
507
|
-
this.store.delete(handle);
|
|
508
|
-
}
|
|
509
|
-
async getAndDelete(handle) {
|
|
510
|
-
const entry = await this.get(handle);
|
|
511
|
-
if (entry) this.store.delete(handle);
|
|
512
|
-
return entry;
|
|
513
|
-
}
|
|
514
|
-
async cleanup() {
|
|
515
|
-
const now = Date.now();
|
|
516
|
-
let count = 0;
|
|
517
|
-
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
518
|
-
this.store.delete(handle);
|
|
519
|
-
count++;
|
|
520
|
-
}
|
|
521
|
-
return count;
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
|
|
525
323
|
//#endregion
|
|
526
324
|
//#region packages/event-wf/src/workflow.ts
|
|
527
325
|
/** Workflow engine that resolves steps via Wooks router lookup. */
|
|
@@ -706,15 +504,30 @@ function createWfApp(opts, wooks$2) {
|
|
|
706
504
|
}
|
|
707
505
|
|
|
708
506
|
//#endregion
|
|
709
|
-
exports
|
|
710
|
-
|
|
507
|
+
Object.defineProperty(exports, 'EncapsulatedStateStrategy', {
|
|
508
|
+
enumerable: true,
|
|
509
|
+
get: function () {
|
|
510
|
+
return _prostojs_wf_outlets.EncapsulatedStateStrategy;
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
Object.defineProperty(exports, 'HandleStateStrategy', {
|
|
514
|
+
enumerable: true,
|
|
515
|
+
get: function () {
|
|
516
|
+
return _prostojs_wf_outlets.HandleStateStrategy;
|
|
517
|
+
}
|
|
518
|
+
});
|
|
711
519
|
Object.defineProperty(exports, 'StepRetriableError', {
|
|
712
520
|
enumerable: true,
|
|
713
521
|
get: function () {
|
|
714
522
|
return _prostojs_wf.StepRetriableError;
|
|
715
523
|
}
|
|
716
524
|
});
|
|
717
|
-
exports
|
|
525
|
+
Object.defineProperty(exports, 'WfStateStoreMemory', {
|
|
526
|
+
enumerable: true,
|
|
527
|
+
get: function () {
|
|
528
|
+
return _prostojs_wf_outlets.WfStateStoreMemory;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
718
531
|
exports.WooksWf = WooksWf;
|
|
719
532
|
exports.createEmailOutlet = createEmailOutlet;
|
|
720
533
|
exports.createHttpOutlet = createHttpOutlet;
|
|
@@ -722,9 +535,24 @@ exports.createOutletHandler = createOutletHandler;
|
|
|
722
535
|
exports.createWfApp = createWfApp;
|
|
723
536
|
exports.createWfContext = createWfContext;
|
|
724
537
|
exports.handleWfOutletRequest = handleWfOutletRequest;
|
|
725
|
-
exports
|
|
726
|
-
|
|
727
|
-
|
|
538
|
+
Object.defineProperty(exports, 'outlet', {
|
|
539
|
+
enumerable: true,
|
|
540
|
+
get: function () {
|
|
541
|
+
return _prostojs_wf_outlets.outlet;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
Object.defineProperty(exports, 'outletEmail', {
|
|
545
|
+
enumerable: true,
|
|
546
|
+
get: function () {
|
|
547
|
+
return _prostojs_wf_outlets.outletEmail;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
Object.defineProperty(exports, 'outletHttp', {
|
|
551
|
+
enumerable: true,
|
|
552
|
+
get: function () {
|
|
553
|
+
return _prostojs_wf_outlets.outletHttp;
|
|
554
|
+
}
|
|
555
|
+
});
|
|
728
556
|
exports.resumeKey = resumeKey;
|
|
729
557
|
exports.resumeWfContext = resumeWfContext;
|
|
730
558
|
Object.defineProperty(exports, 'useLogger', {
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createEventContext, current, defineEventKind, defineWook, key, slot, useLogger, useRouteParams } from "@wooksjs/event-core";
|
|
2
2
|
import { useCookies, useResponse, useUrlParams } from "@wooksjs/event-http";
|
|
3
3
|
import { useBody } from "@wooksjs/http-body";
|
|
4
|
-
import {
|
|
4
|
+
import { EncapsulatedStateStrategy, HandleStateStrategy, WfStateStoreMemory, outlet, outletEmail, outletHttp } from "@prostojs/wf/outlets";
|
|
5
5
|
import { StepRetriableError, Workflow, createStep } from "@prostojs/wf";
|
|
6
6
|
import { WooksAdapterBase } from "wooks";
|
|
7
7
|
|
|
@@ -145,6 +145,7 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
145
145
|
const input = body?.input;
|
|
146
146
|
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
147
147
|
let output;
|
|
148
|
+
let strategyReResolved = false;
|
|
148
149
|
if (token) {
|
|
149
150
|
const strategy = resolveStrategy(wfid ?? "");
|
|
150
151
|
ctx.set(stateStrategyKey, strategy);
|
|
@@ -155,7 +156,10 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
155
156
|
}
|
|
156
157
|
if (state.schemaId !== (wfid ?? "")) {
|
|
157
158
|
const realStrategy = resolveStrategy(state.schemaId);
|
|
158
|
-
|
|
159
|
+
if (realStrategy !== strategy) {
|
|
160
|
+
ctx.set(stateStrategyKey, realStrategy);
|
|
161
|
+
strategyReResolved = true;
|
|
162
|
+
}
|
|
159
163
|
}
|
|
160
164
|
output = await deps.resume(state, {
|
|
161
165
|
input,
|
|
@@ -211,7 +215,8 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
211
215
|
...output.state,
|
|
212
216
|
meta: { outlet: outletReq.outlet }
|
|
213
217
|
};
|
|
214
|
-
const
|
|
218
|
+
const reuseHandle = token && !strategyReResolved ? { handle: token } : void 0;
|
|
219
|
+
const newToken = await strategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0, reuseHandle);
|
|
215
220
|
const outOfBand = outletHandler.tokenDelivery === "out-of-band";
|
|
216
221
|
if (tokenWrite === "cookie" && !outOfBand) response.setCookie(tokenName, newToken, {
|
|
217
222
|
httpOnly: true,
|
|
@@ -314,213 +319,6 @@ function createOutletHandler(wfApp) {
|
|
|
314
319
|
});
|
|
315
320
|
}
|
|
316
321
|
|
|
317
|
-
//#endregion
|
|
318
|
-
//#region node_modules/.pnpm/@prostojs+wf@0.2.0/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
319
|
-
/**
|
|
320
|
-
* Generic outlet request. Use for custom outlets.
|
|
321
|
-
*
|
|
322
|
-
* @example
|
|
323
|
-
* return outlet('pending-task', {
|
|
324
|
-
* payload: ApprovalForm,
|
|
325
|
-
* target: managerId,
|
|
326
|
-
* context: { orderId, amount },
|
|
327
|
-
* })
|
|
328
|
-
*/
|
|
329
|
-
function outlet(name, data) {
|
|
330
|
-
return { inputRequired: {
|
|
331
|
-
outlet: name,
|
|
332
|
-
...data
|
|
333
|
-
} };
|
|
334
|
-
}
|
|
335
|
-
/**
|
|
336
|
-
* Pause for HTTP form input. The outlet returns the payload (form definition)
|
|
337
|
-
* and state token in the HTTP response.
|
|
338
|
-
*
|
|
339
|
-
* @example
|
|
340
|
-
* return outletHttp(LoginForm)
|
|
341
|
-
* return outletHttp(LoginForm, { error: 'Invalid credentials' })
|
|
342
|
-
*/
|
|
343
|
-
function outletHttp(payload, context) {
|
|
344
|
-
return outlet("http", {
|
|
345
|
-
payload,
|
|
346
|
-
context
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Pause and send email with a magic link containing the state token.
|
|
351
|
-
*
|
|
352
|
-
* @example
|
|
353
|
-
* return outletEmail('user@test.com', 'invite', { name: 'Alice' })
|
|
354
|
-
*/
|
|
355
|
-
function outletEmail(target, template, context) {
|
|
356
|
-
return outlet("email", {
|
|
357
|
-
target,
|
|
358
|
-
template,
|
|
359
|
-
context
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Self-contained AES-256-GCM encrypted state strategy.
|
|
364
|
-
*
|
|
365
|
-
* Workflow state is encrypted into a base64url token that travels with the
|
|
366
|
-
* transport (cookie, URL param, hidden field). No server-side storage needed.
|
|
367
|
-
*
|
|
368
|
-
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
369
|
-
*
|
|
370
|
-
* ## Security warning — replay
|
|
371
|
-
*
|
|
372
|
-
* This strategy is STATELESS. It cannot enforce single-use semantics:
|
|
373
|
-
* `consume()` is a no-op alias for `retrieve()` because there is no
|
|
374
|
-
* server-side record to delete. Anyone who obtains a copy of the token
|
|
375
|
-
* (browser history, server logs, shoulder-surfing, intermediate proxy,
|
|
376
|
-
* shared device) can replay it until the TTL expires.
|
|
377
|
-
*
|
|
378
|
-
* Use `EncapsulatedStateStrategy` ONLY when BOTH of the following hold:
|
|
379
|
-
*
|
|
380
|
-
* 1. Every workflow step is idempotent — re-executing a step with the same
|
|
381
|
-
* input produces no harmful side effects (pure data collection,
|
|
382
|
-
* validation-only steps).
|
|
383
|
-
* 2. The flow is not security-sensitive — no credential changes, financial
|
|
384
|
-
* operations, account provisioning, permission grants, or any other
|
|
385
|
-
* privileged action.
|
|
386
|
-
*
|
|
387
|
-
* For auth flows (login, password reset, invite accept), financial
|
|
388
|
-
* operations, or anything with meaningful side effects, use
|
|
389
|
-
* `HandleStateStrategy` with a durable `WfStateStore`. `HandleStateStrategy`
|
|
390
|
-
* supports true single-use tokens via atomic `getAndDelete` at the store
|
|
391
|
-
* layer.
|
|
392
|
-
*
|
|
393
|
-
* @example
|
|
394
|
-
* const strategy = new EncapsulatedStateStrategy({
|
|
395
|
-
* secret: crypto.randomBytes(32),
|
|
396
|
-
* defaultTtl: 3600_000, // 1 hour
|
|
397
|
-
* });
|
|
398
|
-
* const token = await strategy.persist(state);
|
|
399
|
-
* const recovered = await strategy.retrieve(token);
|
|
400
|
-
*/
|
|
401
|
-
var EncapsulatedStateStrategy = class {
|
|
402
|
-
/** @throws if secret is not exactly 32 bytes */
|
|
403
|
-
constructor(config) {
|
|
404
|
-
this.config = config;
|
|
405
|
-
this.key = typeof config.secret === "string" ? Buffer.from(config.secret, "hex") : config.secret;
|
|
406
|
-
if (this.key.length !== 32) throw new Error("EncapsulatedStateStrategy: secret must be exactly 32 bytes");
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
* Encrypt workflow state into a self-contained token.
|
|
410
|
-
* @param state — workflow state to persist
|
|
411
|
-
* @param options.ttl — time-to-live in ms (overrides defaultTtl)
|
|
412
|
-
* @returns base64url-encoded encrypted token
|
|
413
|
-
*/
|
|
414
|
-
async persist(state, options) {
|
|
415
|
-
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
416
|
-
const exp = ttl > 0 ? Date.now() + ttl : 0;
|
|
417
|
-
const payload = JSON.stringify({
|
|
418
|
-
s: state,
|
|
419
|
-
e: exp
|
|
420
|
-
});
|
|
421
|
-
const iv = randomBytes(12);
|
|
422
|
-
const cipher = createCipheriv("aes-256-gcm", this.key, iv);
|
|
423
|
-
const encrypted = Buffer.concat([cipher.update(payload, "utf8"), cipher.final()]);
|
|
424
|
-
const tag = cipher.getAuthTag();
|
|
425
|
-
return Buffer.concat([
|
|
426
|
-
iv,
|
|
427
|
-
tag,
|
|
428
|
-
encrypted
|
|
429
|
-
]).toString("base64url");
|
|
430
|
-
}
|
|
431
|
-
/** Decrypt and return workflow state. Returns null if token is invalid, expired, or tampered. */
|
|
432
|
-
async retrieve(token) {
|
|
433
|
-
return this.decrypt(token);
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Stateless — CANNOT invalidate the token. Returns identical result to
|
|
437
|
-
* `retrieve()`. See the class-level security warning.
|
|
438
|
-
*
|
|
439
|
-
* This method exists only to satisfy the `WfStateStrategy` contract.
|
|
440
|
-
* Callers that need true single-use semantics must use
|
|
441
|
-
* `HandleStateStrategy`.
|
|
442
|
-
*/
|
|
443
|
-
async consume(token) {
|
|
444
|
-
return this.decrypt(token);
|
|
445
|
-
}
|
|
446
|
-
decrypt(token) {
|
|
447
|
-
try {
|
|
448
|
-
const buf = Buffer.from(token, "base64url");
|
|
449
|
-
if (buf.length < 28) return null;
|
|
450
|
-
const iv = buf.subarray(0, 12);
|
|
451
|
-
const tag = buf.subarray(12, 28);
|
|
452
|
-
const ciphertext = buf.subarray(28);
|
|
453
|
-
const decipher = createDecipheriv("aes-256-gcm", this.key, iv);
|
|
454
|
-
decipher.setAuthTag(tag);
|
|
455
|
-
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
456
|
-
const { s: state, e: exp } = JSON.parse(decrypted.toString("utf8"));
|
|
457
|
-
if (exp > 0 && Date.now() > exp) return null;
|
|
458
|
-
return state;
|
|
459
|
-
} catch {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
|
-
var HandleStateStrategy = class {
|
|
465
|
-
constructor(config) {
|
|
466
|
-
this.config = config;
|
|
467
|
-
}
|
|
468
|
-
async persist(state, options) {
|
|
469
|
-
const handle = (this.config.generateHandle ?? randomUUID)();
|
|
470
|
-
const ttl = options?.ttl ?? this.config.defaultTtl ?? 0;
|
|
471
|
-
const expiresAt = ttl > 0 ? Date.now() + ttl : void 0;
|
|
472
|
-
await this.config.store.set(handle, state, expiresAt);
|
|
473
|
-
return handle;
|
|
474
|
-
}
|
|
475
|
-
async retrieve(token) {
|
|
476
|
-
return (await this.config.store.get(token))?.state ?? null;
|
|
477
|
-
}
|
|
478
|
-
async consume(token) {
|
|
479
|
-
return (await this.config.store.getAndDelete(token))?.state ?? null;
|
|
480
|
-
}
|
|
481
|
-
};
|
|
482
|
-
/**
|
|
483
|
-
* In-memory state store for development and testing.
|
|
484
|
-
* State is lost on process restart.
|
|
485
|
-
*/
|
|
486
|
-
var WfStateStoreMemory = class {
|
|
487
|
-
constructor() {
|
|
488
|
-
this.store = /* @__PURE__ */ new Map();
|
|
489
|
-
}
|
|
490
|
-
async set(handle, state, expiresAt) {
|
|
491
|
-
this.store.set(handle, {
|
|
492
|
-
state,
|
|
493
|
-
expiresAt
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
async get(handle) {
|
|
497
|
-
const entry = this.store.get(handle);
|
|
498
|
-
if (!entry) return null;
|
|
499
|
-
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
500
|
-
this.store.delete(handle);
|
|
501
|
-
return null;
|
|
502
|
-
}
|
|
503
|
-
return entry;
|
|
504
|
-
}
|
|
505
|
-
async delete(handle) {
|
|
506
|
-
this.store.delete(handle);
|
|
507
|
-
}
|
|
508
|
-
async getAndDelete(handle) {
|
|
509
|
-
const entry = await this.get(handle);
|
|
510
|
-
if (entry) this.store.delete(handle);
|
|
511
|
-
return entry;
|
|
512
|
-
}
|
|
513
|
-
async cleanup() {
|
|
514
|
-
const now = Date.now();
|
|
515
|
-
let count = 0;
|
|
516
|
-
for (const [handle, entry] of this.store) if (entry.expiresAt && now > entry.expiresAt) {
|
|
517
|
-
this.store.delete(handle);
|
|
518
|
-
count++;
|
|
519
|
-
}
|
|
520
|
-
return count;
|
|
521
|
-
}
|
|
522
|
-
};
|
|
523
|
-
|
|
524
322
|
//#endregion
|
|
525
323
|
//#region packages/event-wf/src/workflow.ts
|
|
526
324
|
/** Workflow engine that resolves steps via Wooks router lookup. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-wf",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "@wooksjs/event-wf",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"app",
|
|
@@ -37,22 +37,22 @@
|
|
|
37
37
|
}
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@prostojs/wf": "^0.2.
|
|
40
|
+
"@prostojs/wf": "^0.2.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
|
-
"@wooksjs/event-core": "^0.7.
|
|
46
|
-
"@wooksjs/event-http": "^0.7.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
45
|
+
"@wooksjs/event-core": "^0.7.15",
|
|
46
|
+
"@wooksjs/event-http": "^0.7.15",
|
|
47
|
+
"wooks": "^0.7.15",
|
|
48
|
+
"@wooksjs/http-body": "^0.7.15"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@prostojs/logger": "^0.4.3",
|
|
52
|
-
"@wooksjs/event-core": "^0.7.
|
|
53
|
-
"
|
|
54
|
-
"@wooksjs/event-http": "^0.7.
|
|
55
|
-
"
|
|
52
|
+
"@wooksjs/event-core": "^0.7.15",
|
|
53
|
+
"wooks": "^0.7.15",
|
|
54
|
+
"@wooksjs/event-http": "^0.7.15",
|
|
55
|
+
"@wooksjs/http-body": "^0.7.15"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
58
58
|
"@wooksjs/event-http": {
|