@wooksjs/event-wf 0.7.9 → 0.7.10
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 +38 -12
- package/dist/index.d.ts +21 -11
- package/dist/index.mjs +38 -12
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -120,7 +120,6 @@ const useWfFinished = (0, __wooksjs_event_core.defineWook)((ctx) => ({
|
|
|
120
120
|
|
|
121
121
|
//#endregion
|
|
122
122
|
//#region packages/event-wf/src/outlets/trigger.ts
|
|
123
|
-
const DEFAULT_CONSUME_TOKEN = { email: true };
|
|
124
123
|
/**
|
|
125
124
|
* Handle an HTTP request that starts or resumes a workflow.
|
|
126
125
|
*
|
|
@@ -168,15 +167,11 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
168
167
|
const wfid = body?.[wfidName] ?? queryParams.get(wfidName) ?? void 0;
|
|
169
168
|
const input = body?.input;
|
|
170
169
|
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
171
|
-
const shouldConsume = (outletName) => {
|
|
172
|
-
if (typeof tok.consume === "boolean") return tok.consume;
|
|
173
|
-
return (tok.consume ?? DEFAULT_CONSUME_TOKEN)[outletName] ?? false;
|
|
174
|
-
};
|
|
175
170
|
let output;
|
|
176
171
|
if (token) {
|
|
177
172
|
const strategy = resolveStrategy(wfid ?? "");
|
|
178
173
|
ctx.set(stateStrategyKey, strategy);
|
|
179
|
-
const state = await strategy.
|
|
174
|
+
const state = await strategy.consume(token);
|
|
180
175
|
if (!state) return {
|
|
181
176
|
error: "Invalid or expired workflow state",
|
|
182
177
|
status: 400
|
|
@@ -185,8 +180,6 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
185
180
|
const realStrategy = resolveStrategy(state.schemaId);
|
|
186
181
|
ctx.set(stateStrategyKey, realStrategy);
|
|
187
182
|
}
|
|
188
|
-
const outletName = state.meta?.outlet;
|
|
189
|
-
if (outletName && shouldConsume(outletName)) await strategy.consume(token);
|
|
190
183
|
output = await deps.resume(state, {
|
|
191
184
|
input,
|
|
192
185
|
eventContext: ctx
|
|
@@ -238,13 +231,14 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
238
231
|
meta: { outlet: outletReq.outlet }
|
|
239
232
|
};
|
|
240
233
|
const newToken = await strategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0);
|
|
241
|
-
|
|
234
|
+
const outOfBand = outletHandler.tokenDelivery === "out-of-band";
|
|
235
|
+
if (tokenWrite === "cookie" && !outOfBand) response.setCookie(tokenName, newToken, {
|
|
242
236
|
httpOnly: true,
|
|
243
237
|
sameSite: "Strict",
|
|
244
238
|
path: "/"
|
|
245
239
|
});
|
|
246
240
|
const result = await outletHandler.deliver(outletReq, newToken);
|
|
247
|
-
if (tokenWrite === "body" && result?.response && typeof result.response === "object") return {
|
|
241
|
+
if (tokenWrite === "body" && !outOfBand && result?.response && typeof result.response === "object") return {
|
|
248
242
|
...result.response,
|
|
249
243
|
[tokenName]: newToken
|
|
250
244
|
};
|
|
@@ -274,6 +268,7 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
274
268
|
function createHttpOutlet(opts) {
|
|
275
269
|
return {
|
|
276
270
|
name: "http",
|
|
271
|
+
tokenDelivery: "caller",
|
|
277
272
|
async deliver(request, _token) {
|
|
278
273
|
const body = opts?.transform ? opts.transform(request.payload, request.context) : typeof request.payload === "object" && request.payload !== null ? {
|
|
279
274
|
...request.payload,
|
|
@@ -302,6 +297,7 @@ function createHttpOutlet(opts) {
|
|
|
302
297
|
function createEmailOutlet(send) {
|
|
303
298
|
return {
|
|
304
299
|
name: "email",
|
|
300
|
+
tokenDelivery: "out-of-band",
|
|
305
301
|
async deliver(request, token) {
|
|
306
302
|
await send({
|
|
307
303
|
target: request.target ?? "",
|
|
@@ -339,7 +335,7 @@ function createOutletHandler(wfApp) {
|
|
|
339
335
|
}
|
|
340
336
|
|
|
341
337
|
//#endregion
|
|
342
|
-
//#region node_modules/.pnpm/@prostojs+wf@0.
|
|
338
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.2.0/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
343
339
|
/**
|
|
344
340
|
* Generic outlet request. Use for custom outlets.
|
|
345
341
|
*
|
|
@@ -391,6 +387,29 @@ function outletEmail(target, template, context) {
|
|
|
391
387
|
*
|
|
392
388
|
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
393
389
|
*
|
|
390
|
+
* ## Security warning — replay
|
|
391
|
+
*
|
|
392
|
+
* This strategy is STATELESS. It cannot enforce single-use semantics:
|
|
393
|
+
* `consume()` is a no-op alias for `retrieve()` because there is no
|
|
394
|
+
* server-side record to delete. Anyone who obtains a copy of the token
|
|
395
|
+
* (browser history, server logs, shoulder-surfing, intermediate proxy,
|
|
396
|
+
* shared device) can replay it until the TTL expires.
|
|
397
|
+
*
|
|
398
|
+
* Use `EncapsulatedStateStrategy` ONLY when BOTH of the following hold:
|
|
399
|
+
*
|
|
400
|
+
* 1. Every workflow step is idempotent — re-executing a step with the same
|
|
401
|
+
* input produces no harmful side effects (pure data collection,
|
|
402
|
+
* validation-only steps).
|
|
403
|
+
* 2. The flow is not security-sensitive — no credential changes, financial
|
|
404
|
+
* operations, account provisioning, permission grants, or any other
|
|
405
|
+
* privileged action.
|
|
406
|
+
*
|
|
407
|
+
* For auth flows (login, password reset, invite accept), financial
|
|
408
|
+
* operations, or anything with meaningful side effects, use
|
|
409
|
+
* `HandleStateStrategy` with a durable `WfStateStore`. `HandleStateStrategy`
|
|
410
|
+
* supports true single-use tokens via atomic `getAndDelete` at the store
|
|
411
|
+
* layer.
|
|
412
|
+
*
|
|
394
413
|
* @example
|
|
395
414
|
* const strategy = new EncapsulatedStateStrategy({
|
|
396
415
|
* secret: crypto.randomBytes(32),
|
|
@@ -433,7 +452,14 @@ var EncapsulatedStateStrategy = class {
|
|
|
433
452
|
async retrieve(token) {
|
|
434
453
|
return this.decrypt(token);
|
|
435
454
|
}
|
|
436
|
-
/**
|
|
455
|
+
/**
|
|
456
|
+
* Stateless — CANNOT invalidate the token. Returns identical result to
|
|
457
|
+
* `retrieve()`. See the class-level security warning.
|
|
458
|
+
*
|
|
459
|
+
* This method exists only to satisfy the `WfStateStrategy` contract.
|
|
460
|
+
* Callers that need true single-use semantics must use
|
|
461
|
+
* `HandleStateStrategy`.
|
|
462
|
+
*/
|
|
437
463
|
async consume(token) {
|
|
438
464
|
return this.decrypt(token);
|
|
439
465
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -92,26 +92,36 @@ interface WfOutletTokenConfig {
|
|
|
92
92
|
write?: 'body' | 'cookie';
|
|
93
93
|
/** Parameter name for state token (default: `'wfs'`) */
|
|
94
94
|
name?: string;
|
|
95
|
-
/**
|
|
96
|
-
* Token consumption mode per outlet. When `true`, the trigger calls
|
|
97
|
-
* `strategy.consume()` (single-use token) instead of `strategy.retrieve()`
|
|
98
|
-
* on resume. Defaults to `{ email: true }` — email magic links are consumed
|
|
99
|
-
* on first use, HTTP tokens are reusable.
|
|
100
|
-
*
|
|
101
|
-
* Can be a boolean (applies to all outlets) or a per-outlet-name map.
|
|
102
|
-
*/
|
|
103
|
-
consume?: boolean | Record<string, boolean>;
|
|
104
95
|
}
|
|
105
96
|
interface WfOutletTriggerConfig {
|
|
106
97
|
/** Whitelist of allowed workflow IDs. If empty, all are allowed. */
|
|
107
98
|
allow?: string[];
|
|
108
99
|
/** Blacklist of workflow IDs. Checked after allow. */
|
|
109
100
|
block?: string[];
|
|
110
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* State persistence strategy. Either a single strategy shared by all
|
|
103
|
+
* workflows, or a function that returns a strategy per workflow ID.
|
|
104
|
+
*
|
|
105
|
+
* **Constraint when using the function form.** The trigger resolves the
|
|
106
|
+
* strategy at resume time using the `wfid` from the request. If the resume
|
|
107
|
+
* request does not include `wfid` (e.g. cookie-only transport, token-only
|
|
108
|
+
* body), the trigger calls `config.state('')` — meaning:
|
|
109
|
+
*
|
|
110
|
+
* - EITHER all strategies returned by the function must share the same
|
|
111
|
+
* underlying storage (same Redis instance, same `WfStateStore`, same
|
|
112
|
+
* encryption key), so `consume`/`retrieve` operations work regardless of
|
|
113
|
+
* which strategy instance is picked;
|
|
114
|
+
* - OR every resume request must carry `wfid` so the correct strategy is
|
|
115
|
+
* always resolved.
|
|
116
|
+
*
|
|
117
|
+
* Violating this contract silently breaks single-use token invalidation:
|
|
118
|
+
* the `consume` call runs against the wrong strategy's storage, and the
|
|
119
|
+
* token remains live in the real strategy.
|
|
120
|
+
*/
|
|
111
121
|
state: WfStateStrategy | ((wfid: string) => WfStateStrategy);
|
|
112
122
|
/** Registered outlets */
|
|
113
123
|
outlets: WfOutlet[];
|
|
114
|
-
/** Token configuration (reading, writing, naming
|
|
124
|
+
/** Token configuration (reading, writing, naming) */
|
|
115
125
|
token?: WfOutletTokenConfig;
|
|
116
126
|
/** Parameter name for workflow ID (default: `'wfid'`) */
|
|
117
127
|
wfidName?: string;
|
package/dist/index.mjs
CHANGED
|
@@ -97,7 +97,6 @@ const useWfFinished = defineWook((ctx) => ({
|
|
|
97
97
|
|
|
98
98
|
//#endregion
|
|
99
99
|
//#region packages/event-wf/src/outlets/trigger.ts
|
|
100
|
-
const DEFAULT_CONSUME_TOKEN = { email: true };
|
|
101
100
|
/**
|
|
102
101
|
* Handle an HTTP request that starts or resumes a workflow.
|
|
103
102
|
*
|
|
@@ -145,15 +144,11 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
145
144
|
const wfid = body?.[wfidName] ?? queryParams.get(wfidName) ?? void 0;
|
|
146
145
|
const input = body?.input;
|
|
147
146
|
const resolveStrategy = (id) => typeof config.state === "function" ? config.state(id) : config.state;
|
|
148
|
-
const shouldConsume = (outletName) => {
|
|
149
|
-
if (typeof tok.consume === "boolean") return tok.consume;
|
|
150
|
-
return (tok.consume ?? DEFAULT_CONSUME_TOKEN)[outletName] ?? false;
|
|
151
|
-
};
|
|
152
147
|
let output;
|
|
153
148
|
if (token) {
|
|
154
149
|
const strategy = resolveStrategy(wfid ?? "");
|
|
155
150
|
ctx.set(stateStrategyKey, strategy);
|
|
156
|
-
const state = await strategy.
|
|
151
|
+
const state = await strategy.consume(token);
|
|
157
152
|
if (!state) return {
|
|
158
153
|
error: "Invalid or expired workflow state",
|
|
159
154
|
status: 400
|
|
@@ -162,8 +157,6 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
162
157
|
const realStrategy = resolveStrategy(state.schemaId);
|
|
163
158
|
ctx.set(stateStrategyKey, realStrategy);
|
|
164
159
|
}
|
|
165
|
-
const outletName = state.meta?.outlet;
|
|
166
|
-
if (outletName && shouldConsume(outletName)) await strategy.consume(token);
|
|
167
160
|
output = await deps.resume(state, {
|
|
168
161
|
input,
|
|
169
162
|
eventContext: ctx
|
|
@@ -215,13 +208,14 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
215
208
|
meta: { outlet: outletReq.outlet }
|
|
216
209
|
};
|
|
217
210
|
const newToken = await strategy.persist(stateWithMeta, output.expires ? { ttl: output.expires - Date.now() } : void 0);
|
|
218
|
-
|
|
211
|
+
const outOfBand = outletHandler.tokenDelivery === "out-of-band";
|
|
212
|
+
if (tokenWrite === "cookie" && !outOfBand) response.setCookie(tokenName, newToken, {
|
|
219
213
|
httpOnly: true,
|
|
220
214
|
sameSite: "Strict",
|
|
221
215
|
path: "/"
|
|
222
216
|
});
|
|
223
217
|
const result = await outletHandler.deliver(outletReq, newToken);
|
|
224
|
-
if (tokenWrite === "body" && result?.response && typeof result.response === "object") return {
|
|
218
|
+
if (tokenWrite === "body" && !outOfBand && result?.response && typeof result.response === "object") return {
|
|
225
219
|
...result.response,
|
|
226
220
|
[tokenName]: newToken
|
|
227
221
|
};
|
|
@@ -251,6 +245,7 @@ async function handleWfOutletRequest(config, deps) {
|
|
|
251
245
|
function createHttpOutlet(opts) {
|
|
252
246
|
return {
|
|
253
247
|
name: "http",
|
|
248
|
+
tokenDelivery: "caller",
|
|
254
249
|
async deliver(request, _token) {
|
|
255
250
|
const body = opts?.transform ? opts.transform(request.payload, request.context) : typeof request.payload === "object" && request.payload !== null ? {
|
|
256
251
|
...request.payload,
|
|
@@ -279,6 +274,7 @@ function createHttpOutlet(opts) {
|
|
|
279
274
|
function createEmailOutlet(send) {
|
|
280
275
|
return {
|
|
281
276
|
name: "email",
|
|
277
|
+
tokenDelivery: "out-of-band",
|
|
282
278
|
async deliver(request, token) {
|
|
283
279
|
await send({
|
|
284
280
|
target: request.target ?? "",
|
|
@@ -316,7 +312,7 @@ function createOutletHandler(wfApp) {
|
|
|
316
312
|
}
|
|
317
313
|
|
|
318
314
|
//#endregion
|
|
319
|
-
//#region node_modules/.pnpm/@prostojs+wf@0.
|
|
315
|
+
//#region node_modules/.pnpm/@prostojs+wf@0.2.0/node_modules/@prostojs/wf/dist/outlets/index.mjs
|
|
320
316
|
/**
|
|
321
317
|
* Generic outlet request. Use for custom outlets.
|
|
322
318
|
*
|
|
@@ -368,6 +364,29 @@ function outletEmail(target, template, context) {
|
|
|
368
364
|
*
|
|
369
365
|
* Token format: `base64url(iv[12] + authTag[16] + ciphertext)`
|
|
370
366
|
*
|
|
367
|
+
* ## Security warning — replay
|
|
368
|
+
*
|
|
369
|
+
* This strategy is STATELESS. It cannot enforce single-use semantics:
|
|
370
|
+
* `consume()` is a no-op alias for `retrieve()` because there is no
|
|
371
|
+
* server-side record to delete. Anyone who obtains a copy of the token
|
|
372
|
+
* (browser history, server logs, shoulder-surfing, intermediate proxy,
|
|
373
|
+
* shared device) can replay it until the TTL expires.
|
|
374
|
+
*
|
|
375
|
+
* Use `EncapsulatedStateStrategy` ONLY when BOTH of the following hold:
|
|
376
|
+
*
|
|
377
|
+
* 1. Every workflow step is idempotent — re-executing a step with the same
|
|
378
|
+
* input produces no harmful side effects (pure data collection,
|
|
379
|
+
* validation-only steps).
|
|
380
|
+
* 2. The flow is not security-sensitive — no credential changes, financial
|
|
381
|
+
* operations, account provisioning, permission grants, or any other
|
|
382
|
+
* privileged action.
|
|
383
|
+
*
|
|
384
|
+
* For auth flows (login, password reset, invite accept), financial
|
|
385
|
+
* operations, or anything with meaningful side effects, use
|
|
386
|
+
* `HandleStateStrategy` with a durable `WfStateStore`. `HandleStateStrategy`
|
|
387
|
+
* supports true single-use tokens via atomic `getAndDelete` at the store
|
|
388
|
+
* layer.
|
|
389
|
+
*
|
|
371
390
|
* @example
|
|
372
391
|
* const strategy = new EncapsulatedStateStrategy({
|
|
373
392
|
* secret: crypto.randomBytes(32),
|
|
@@ -410,7 +429,14 @@ var EncapsulatedStateStrategy = class {
|
|
|
410
429
|
async retrieve(token) {
|
|
411
430
|
return this.decrypt(token);
|
|
412
431
|
}
|
|
413
|
-
/**
|
|
432
|
+
/**
|
|
433
|
+
* Stateless — CANNOT invalidate the token. Returns identical result to
|
|
434
|
+
* `retrieve()`. See the class-level security warning.
|
|
435
|
+
*
|
|
436
|
+
* This method exists only to satisfy the `WfStateStrategy` contract.
|
|
437
|
+
* Callers that need true single-use semantics must use
|
|
438
|
+
* `HandleStateStrategy`.
|
|
439
|
+
*/
|
|
414
440
|
async consume(token) {
|
|
415
441
|
return this.decrypt(token);
|
|
416
442
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wooksjs/event-wf",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
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.
|
|
40
|
+
"@prostojs/wf": "^0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.9.3",
|
|
44
44
|
"vitest": "^3.2.4",
|
|
45
|
-
"@wooksjs/event-
|
|
46
|
-
"@wooksjs/event-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
45
|
+
"@wooksjs/event-http": "^0.7.10",
|
|
46
|
+
"@wooksjs/event-core": "^0.7.10",
|
|
47
|
+
"wooks": "^0.7.10",
|
|
48
|
+
"@wooksjs/http-body": "^0.7.10"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@prostojs/logger": "^0.4.3",
|
|
52
|
-
"@wooksjs/event-core": "^0.7.
|
|
53
|
-
"@wooksjs/event-http": "^0.7.
|
|
54
|
-
"@wooksjs/http-body": "^0.7.
|
|
55
|
-
"wooks": "^0.7.
|
|
52
|
+
"@wooksjs/event-core": "^0.7.10",
|
|
53
|
+
"@wooksjs/event-http": "^0.7.10",
|
|
54
|
+
"@wooksjs/http-body": "^0.7.10",
|
|
55
|
+
"wooks": "^0.7.10"
|
|
56
56
|
},
|
|
57
57
|
"peerDependenciesMeta": {
|
|
58
58
|
"@wooksjs/event-http": {
|