@renseiai/agentfactory 0.8.7 → 0.8.9
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/src/config/index.d.ts +1 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +1 -1
- package/dist/src/config/repository-config.d.ts +37 -0
- package/dist/src/config/repository-config.d.ts.map +1 -1
- package/dist/src/config/repository-config.js +47 -0
- package/dist/src/config/repository-config.test.js +140 -1
- package/dist/src/governor/decision-engine.d.ts +3 -0
- package/dist/src/governor/decision-engine.d.ts.map +1 -1
- package/dist/src/governor/decision-engine.js +11 -0
- package/dist/src/governor/decision-engine.test.js +33 -0
- package/dist/src/governor/event-types.d.ts +18 -1
- package/dist/src/governor/event-types.d.ts.map +1 -1
- package/dist/src/governor/event-types.js +4 -0
- package/dist/src/governor/governor-types.d.ts +1 -1
- package/dist/src/governor/governor-types.d.ts.map +1 -1
- package/dist/src/governor/governor.d.ts +17 -1
- package/dist/src/governor/governor.d.ts.map +1 -1
- package/dist/src/governor/governor.js +112 -1
- package/dist/src/governor/governor.test.js +155 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
- package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.js +243 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
- package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
- package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
- package/dist/src/merge-queue/index.d.ts +18 -0
- package/dist/src/merge-queue/index.d.ts.map +1 -0
- package/dist/src/merge-queue/index.js +28 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
- package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
- package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
- package/dist/src/merge-queue/types.d.ts +48 -0
- package/dist/src/merge-queue/types.d.ts.map +1 -0
- package/dist/src/merge-queue/types.js +8 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
- package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.js +235 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
- package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
- package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
- package/dist/src/orchestrator/context-manager.d.ts +72 -0
- package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.js +120 -0
- package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
- package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
- package/dist/src/orchestrator/context-manager.test.js +137 -0
- package/dist/src/orchestrator/index.d.ts +8 -2
- package/dist/src/orchestrator/index.d.ts.map +1 -1
- package/dist/src/orchestrator/index.js +8 -1
- package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
- package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.d.ts +12 -0
- package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
- package/dist/src/orchestrator/orchestrator.js +282 -2
- package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
- package/dist/src/orchestrator/parse-work-result.js +6 -0
- package/dist/src/orchestrator/parse-work-result.test.js +19 -0
- package/dist/src/orchestrator/state-recovery.d.ts +21 -2
- package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
- package/dist/src/orchestrator/state-recovery.js +54 -2
- package/dist/src/orchestrator/state-recovery.test.js +106 -2
- package/dist/src/orchestrator/state-types.d.ts +62 -0
- package/dist/src/orchestrator/state-types.d.ts.map +1 -1
- package/dist/src/orchestrator/state-types.js +5 -1
- package/dist/src/orchestrator/summary-builder.d.ts +47 -0
- package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.js +240 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
- package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
- package/dist/src/orchestrator/summary-builder.test.js +236 -0
- package/dist/src/orchestrator/types.d.ts +2 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -1
- package/dist/src/orchestrator/work-types.d.ts +1 -1
- package/dist/src/orchestrator/work-types.d.ts.map +1 -1
- package/dist/src/providers/index.d.ts +64 -1
- package/dist/src/providers/index.d.ts.map +1 -1
- package/dist/src/providers/index.js +132 -1
- package/dist/src/providers/index.test.js +340 -2
- package/dist/src/routing/index.d.ts +7 -0
- package/dist/src/routing/index.d.ts.map +1 -0
- package/dist/src/routing/index.js +6 -0
- package/dist/src/routing/observation-recorder.d.ts +19 -0
- package/dist/src/routing/observation-recorder.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.js +73 -0
- package/dist/src/routing/observation-recorder.test.d.ts +2 -0
- package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
- package/dist/src/routing/observation-recorder.test.js +322 -0
- package/dist/src/routing/observation-store.d.ts +40 -0
- package/dist/src/routing/observation-store.d.ts.map +1 -0
- package/dist/src/routing/observation-store.js +1 -0
- package/dist/src/routing/observation-store.test.d.ts +2 -0
- package/dist/src/routing/observation-store.test.d.ts.map +1 -0
- package/dist/src/routing/observation-store.test.js +138 -0
- package/dist/src/routing/posterior-store.d.ts +12 -0
- package/dist/src/routing/posterior-store.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.js +13 -0
- package/dist/src/routing/posterior-store.test.d.ts +2 -0
- package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
- package/dist/src/routing/posterior-store.test.js +37 -0
- package/dist/src/routing/reward.d.ts +16 -0
- package/dist/src/routing/reward.d.ts.map +1 -0
- package/dist/src/routing/reward.js +29 -0
- package/dist/src/routing/reward.test.d.ts +2 -0
- package/dist/src/routing/reward.test.d.ts.map +1 -0
- package/dist/src/routing/reward.test.js +210 -0
- package/dist/src/routing/routing-engine.d.ts +20 -0
- package/dist/src/routing/routing-engine.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.js +113 -0
- package/dist/src/routing/routing-engine.test.d.ts +2 -0
- package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
- package/dist/src/routing/routing-engine.test.js +310 -0
- package/dist/src/routing/types.d.ts +157 -0
- package/dist/src/routing/types.d.ts.map +1 -0
- package/dist/src/routing/types.js +68 -0
- package/dist/src/routing/types.test.d.ts +2 -0
- package/dist/src/routing/types.test.d.ts.map +1 -0
- package/dist/src/routing/types.test.js +184 -0
- package/dist/src/templates/registry.test.js +2 -2
- package/dist/src/templates/types.d.ts +5 -0
- package/dist/src/templates/types.d.ts.map +1 -1
- package/dist/src/templates/types.js +3 -0
- package/dist/src/workflow/agent-cancellation.d.ts +37 -0
- package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.js +41 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
- package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
- package/dist/src/workflow/agent-cancellation.test.js +86 -0
- package/dist/src/workflow/branching-router.d.ts +38 -0
- package/dist/src/workflow/branching-router.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.js +52 -0
- package/dist/src/workflow/branching-router.test.d.ts +2 -0
- package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
- package/dist/src/workflow/branching-router.test.js +209 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
- package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.js +46 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
- package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
- package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
- package/dist/src/workflow/duration.d.ts +28 -0
- package/dist/src/workflow/duration.d.ts.map +1 -0
- package/dist/src/workflow/duration.js +57 -0
- package/dist/src/workflow/duration.test.d.ts +2 -0
- package/dist/src/workflow/duration.test.d.ts.map +1 -0
- package/dist/src/workflow/duration.test.js +74 -0
- package/dist/src/workflow/expression/ast.d.ts +53 -0
- package/dist/src/workflow/expression/ast.d.ts.map +1 -0
- package/dist/src/workflow/expression/ast.js +8 -0
- package/dist/src/workflow/expression/context.d.ts +40 -0
- package/dist/src/workflow/expression/context.d.ts.map +1 -0
- package/dist/src/workflow/expression/context.js +37 -0
- package/dist/src/workflow/expression/evaluator.d.ts +28 -0
- package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.js +165 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
- package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/evaluator.test.js +792 -0
- package/dist/src/workflow/expression/expression.test.d.ts +2 -0
- package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
- package/dist/src/workflow/expression/expression.test.js +516 -0
- package/dist/src/workflow/expression/helpers.d.ts +21 -0
- package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
- package/dist/src/workflow/expression/helpers.js +56 -0
- package/dist/src/workflow/expression/index.d.ts +55 -0
- package/dist/src/workflow/expression/index.d.ts.map +1 -0
- package/dist/src/workflow/expression/index.js +71 -0
- package/dist/src/workflow/expression/lexer.d.ts +37 -0
- package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
- package/dist/src/workflow/expression/lexer.js +166 -0
- package/dist/src/workflow/expression/parser.d.ts +23 -0
- package/dist/src/workflow/expression/parser.d.ts.map +1 -0
- package/dist/src/workflow/expression/parser.js +181 -0
- package/dist/src/workflow/gate-state.d.ts +115 -0
- package/dist/src/workflow/gate-state.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.js +185 -0
- package/dist/src/workflow/gate-state.test.d.ts +2 -0
- package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
- package/dist/src/workflow/gate-state.test.js +251 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
- package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.js +243 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
- package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
- package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
- package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.js +216 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/signal-gate.test.js +199 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
- package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.js +162 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
- package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
- package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
- package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.js +381 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/timer-gate.test.js +211 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
- package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.js +216 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
- package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
- package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
- package/dist/src/workflow/index.d.ts +31 -3
- package/dist/src/workflow/index.d.ts.map +1 -1
- package/dist/src/workflow/index.js +20 -1
- package/dist/src/workflow/parallelism-executor.d.ts +25 -0
- package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.js +53 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
- package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-executor.test.js +191 -0
- package/dist/src/workflow/parallelism-types.d.ts +80 -0
- package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
- package/dist/src/workflow/parallelism-types.js +8 -0
- package/dist/src/workflow/phase-context-injector.d.ts +29 -0
- package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.js +43 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
- package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-context-injector.test.js +123 -0
- package/dist/src/workflow/phase-output-collector.d.ts +39 -0
- package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.js +141 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
- package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
- package/dist/src/workflow/phase-output-collector.test.js +179 -0
- package/dist/src/workflow/retry-resolver.d.ts +51 -0
- package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.js +70 -0
- package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
- package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
- package/dist/src/workflow/retry-resolver.test.js +149 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
- package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
- package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
- package/dist/src/workflow/strategies/index.d.ts +4 -0
- package/dist/src/workflow/strategies/index.d.ts.map +1 -0
- package/dist/src/workflow/strategies/index.js +3 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
- package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.js +92 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
- package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
- package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
- package/dist/src/workflow/transition-engine.d.ts +3 -1
- package/dist/src/workflow/transition-engine.d.ts.map +1 -1
- package/dist/src/workflow/transition-engine.js +26 -7
- package/dist/src/workflow/transition-engine.test.js +215 -11
- package/dist/src/workflow/workflow-registry.d.ts +46 -1
- package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
- package/dist/src/workflow/workflow-registry.js +74 -0
- package/dist/src/workflow/workflow-registry.test.js +54 -0
- package/dist/src/workflow/workflow-types.d.ts +330 -12
- package/dist/src/workflow/workflow-types.d.ts.map +1 -1
- package/dist/src/workflow/workflow-types.js +100 -5
- package/dist/src/workflow/workflow-types.test.js +293 -2
- package/package.json +2 -2
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Gate Executor
|
|
3
|
+
*
|
|
4
|
+
* Pure logic for webhook gate evaluation, token generation, and callback URL
|
|
5
|
+
* management. Webhook gates pause workflow execution until an external HTTP
|
|
6
|
+
* callback is received, enabling integration with external approval systems,
|
|
7
|
+
* CI/CD pipelines, and other services.
|
|
8
|
+
*
|
|
9
|
+
* This module handles the evaluation and state management logic only.
|
|
10
|
+
* The actual HTTP endpoint registration happens in the server layer (SUP-1299).
|
|
11
|
+
*/
|
|
12
|
+
import type { GateState } from '../gate-state.js';
|
|
13
|
+
import type { GateDefinition, WorkflowDefinition } from '../workflow-types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Trigger configuration for a webhook gate.
|
|
16
|
+
* Defines the base endpoint path for webhook callbacks.
|
|
17
|
+
*/
|
|
18
|
+
export interface WebhookGateTrigger {
|
|
19
|
+
/** Base path for webhook callbacks (e.g., "/api/gates") */
|
|
20
|
+
endpoint: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Result of evaluating a webhook gate's current status.
|
|
24
|
+
*/
|
|
25
|
+
export interface WebhookGateResult {
|
|
26
|
+
/** Whether the gate condition has been satisfied */
|
|
27
|
+
satisfied: boolean;
|
|
28
|
+
/** The callback URL for an active webhook gate */
|
|
29
|
+
callbackUrl?: string;
|
|
30
|
+
/** Whether the gate has timed out */
|
|
31
|
+
timedOut?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Activation data for a webhook gate, including the authentication token,
|
|
35
|
+
* callback URL, and optional expiration timestamp.
|
|
36
|
+
*/
|
|
37
|
+
export interface WebhookGateActivation {
|
|
38
|
+
/** Cryptographically random token for webhook authentication */
|
|
39
|
+
token: string;
|
|
40
|
+
/** Full callback URL including token query parameter */
|
|
41
|
+
callbackUrl: string;
|
|
42
|
+
/** When the gate activation expires, epoch ms */
|
|
43
|
+
expiresAt?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate a cryptographically random token for webhook authentication.
|
|
47
|
+
*
|
|
48
|
+
* Uses Node.js `crypto.randomBytes()` to generate a 32-byte hex token.
|
|
49
|
+
* This token is stored in gate state and verified on callback receipt
|
|
50
|
+
* to ensure only authorized callers can satisfy the gate.
|
|
51
|
+
*
|
|
52
|
+
* @returns A 64-character hex string token
|
|
53
|
+
*/
|
|
54
|
+
export declare function generateWebhookToken(): string;
|
|
55
|
+
/**
|
|
56
|
+
* Build the callback URL for an activated webhook gate.
|
|
57
|
+
*
|
|
58
|
+
* The URL follows the format:
|
|
59
|
+
* `{baseUrl}/api/gates/{issueId}/{gateName}?token={token}`
|
|
60
|
+
*
|
|
61
|
+
* @param baseUrl - The base URL of the server (e.g., "https://api.example.com")
|
|
62
|
+
* @param issueId - The issue identifier this gate is associated with
|
|
63
|
+
* @param gateName - The unique gate name from the gate definition
|
|
64
|
+
* @param token - The authentication token for this gate activation
|
|
65
|
+
* @returns The fully-qualified callback URL
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildCallbackUrl(baseUrl: string, issueId: string, gateName: string, token: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Validate that a webhook callback's token matches the expected token.
|
|
70
|
+
*
|
|
71
|
+
* Uses timing-safe comparison via `crypto.timingSafeEqual` to prevent
|
|
72
|
+
* timing attacks that could leak token information through response times.
|
|
73
|
+
*
|
|
74
|
+
* @param token - The token received in the webhook callback
|
|
75
|
+
* @param expectedToken - The expected token stored in gate state
|
|
76
|
+
* @returns `true` if the tokens match, `false` otherwise
|
|
77
|
+
*/
|
|
78
|
+
export declare function validateWebhookCallback(token: string, expectedToken: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Pure function to evaluate a webhook gate's current status based on its
|
|
81
|
+
* definition and persisted state.
|
|
82
|
+
*
|
|
83
|
+
* Evaluation logic:
|
|
84
|
+
* - If gate state is `satisfied` -> return `{ satisfied: true }`
|
|
85
|
+
* - If gate state is `timed-out` -> return `{ satisfied: false, timedOut: true }`
|
|
86
|
+
* - If gate state is `active` and has a timeout deadline that has passed ->
|
|
87
|
+
* return `{ satisfied: false, timedOut: true }`
|
|
88
|
+
* - If gate state is `active` -> return `{ satisfied: false, callbackUrl }`
|
|
89
|
+
* where callbackUrl is reconstructed from state's signalSource if available
|
|
90
|
+
* - If gate state is `null` (not yet activated) -> return `{ satisfied: false }`
|
|
91
|
+
*
|
|
92
|
+
* @param gate - The gate definition from the workflow
|
|
93
|
+
* @param gateState - The persisted gate state, or null if the gate has not been activated
|
|
94
|
+
* @returns The evaluated webhook gate result
|
|
95
|
+
*/
|
|
96
|
+
export declare function evaluateWebhookGate(gate: GateDefinition, gateState: GateState | null): WebhookGateResult;
|
|
97
|
+
/**
|
|
98
|
+
* Type guard to check if a gate trigger configuration is a webhook trigger.
|
|
99
|
+
*
|
|
100
|
+
* A valid webhook trigger must have an `endpoint` property of type string.
|
|
101
|
+
*
|
|
102
|
+
* @param trigger - The trigger configuration to check
|
|
103
|
+
* @returns `true` if the trigger is a WebhookGateTrigger
|
|
104
|
+
*/
|
|
105
|
+
export declare function isWebhookGateTrigger(trigger: Record<string, unknown>): trigger is Record<string, unknown> & WebhookGateTrigger;
|
|
106
|
+
/**
|
|
107
|
+
* Get all webhook gates that apply to a given workflow phase.
|
|
108
|
+
*
|
|
109
|
+
* Filters the workflow's gate definitions to return only those that:
|
|
110
|
+
* 1. Have type `webhook`
|
|
111
|
+
* 2. Either have no `appliesTo` restriction, or include the specified phase
|
|
112
|
+
*
|
|
113
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
114
|
+
* @param phase - The phase name to filter gates for
|
|
115
|
+
* @returns Array of gate definitions that are webhook type and apply to the phase
|
|
116
|
+
*/
|
|
117
|
+
export declare function getApplicableWebhookGates(workflow: WorkflowDefinition, phase: string): GateDefinition[];
|
|
118
|
+
/**
|
|
119
|
+
* Creates the activation data for a webhook gate including a cryptographically
|
|
120
|
+
* random token, callback URL, and optional expiration timestamp.
|
|
121
|
+
*
|
|
122
|
+
* This function generates all the data needed to activate a webhook gate,
|
|
123
|
+
* but does not persist any state. The caller is responsible for storing the
|
|
124
|
+
* activation data (token in gate state, URL communicated to external system).
|
|
125
|
+
*
|
|
126
|
+
* @param issueId - The issue identifier this gate is associated with
|
|
127
|
+
* @param gateDef - The gate definition from the workflow
|
|
128
|
+
* @param baseUrl - The base URL of the server for building callback URLs
|
|
129
|
+
* @returns The webhook gate activation data
|
|
130
|
+
*/
|
|
131
|
+
export declare function createWebhookGateActivation(issueId: string, gateDef: GateDefinition, baseUrl: string): WebhookGateActivation;
|
|
132
|
+
//# sourceMappingURL=webhook-gate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-gate.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/webhook-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAEjD,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAa9E;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,SAAS,EAAE,OAAO,CAAA;IAClB,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,MAAM,CAMR;AAMD;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAeT;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,SAAS,GAAG,IAAI,GAC1B,iBAAiB,CAoCnB;AAMD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,OAAO,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,kBAAkB,CAOzD;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,kBAAkB,EAC5B,KAAK,EAAE,MAAM,GACZ,cAAc,EAAE,CAiBlB;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,EACvB,OAAO,EAAE,MAAM,GACd,qBAAqB,CAuBvB"}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Gate Executor
|
|
3
|
+
*
|
|
4
|
+
* Pure logic for webhook gate evaluation, token generation, and callback URL
|
|
5
|
+
* management. Webhook gates pause workflow execution until an external HTTP
|
|
6
|
+
* callback is received, enabling integration with external approval systems,
|
|
7
|
+
* CI/CD pipelines, and other services.
|
|
8
|
+
*
|
|
9
|
+
* This module handles the evaluation and state management logic only.
|
|
10
|
+
* The actual HTTP endpoint registration happens in the server layer (SUP-1299).
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
13
|
+
import { parseDuration } from '../gate-state.js';
|
|
14
|
+
const log = {
|
|
15
|
+
info: (msg, data) => console.log(`[webhook-gate] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
16
|
+
warn: (msg, data) => console.warn(`[webhook-gate] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
17
|
+
error: (msg, data) => console.error(`[webhook-gate] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
18
|
+
debug: (_msg, _data) => { },
|
|
19
|
+
};
|
|
20
|
+
// ============================================
|
|
21
|
+
// Token Generation
|
|
22
|
+
// ============================================
|
|
23
|
+
/**
|
|
24
|
+
* Generate a cryptographically random token for webhook authentication.
|
|
25
|
+
*
|
|
26
|
+
* Uses Node.js `crypto.randomBytes()` to generate a 32-byte hex token.
|
|
27
|
+
* This token is stored in gate state and verified on callback receipt
|
|
28
|
+
* to ensure only authorized callers can satisfy the gate.
|
|
29
|
+
*
|
|
30
|
+
* @returns A 64-character hex string token
|
|
31
|
+
*/
|
|
32
|
+
export function generateWebhookToken() {
|
|
33
|
+
return randomBytes(32).toString('hex');
|
|
34
|
+
}
|
|
35
|
+
// ============================================
|
|
36
|
+
// Callback URL
|
|
37
|
+
// ============================================
|
|
38
|
+
/**
|
|
39
|
+
* Build the callback URL for an activated webhook gate.
|
|
40
|
+
*
|
|
41
|
+
* The URL follows the format:
|
|
42
|
+
* `{baseUrl}/api/gates/{issueId}/{gateName}?token={token}`
|
|
43
|
+
*
|
|
44
|
+
* @param baseUrl - The base URL of the server (e.g., "https://api.example.com")
|
|
45
|
+
* @param issueId - The issue identifier this gate is associated with
|
|
46
|
+
* @param gateName - The unique gate name from the gate definition
|
|
47
|
+
* @param token - The authentication token for this gate activation
|
|
48
|
+
* @returns The fully-qualified callback URL
|
|
49
|
+
*/
|
|
50
|
+
export function buildCallbackUrl(baseUrl, issueId, gateName, token) {
|
|
51
|
+
const normalizedBase = baseUrl.replace(/\/+$/, '');
|
|
52
|
+
const encodedIssueId = encodeURIComponent(issueId);
|
|
53
|
+
const encodedGateName = encodeURIComponent(gateName);
|
|
54
|
+
const encodedToken = encodeURIComponent(token);
|
|
55
|
+
return `${normalizedBase}/api/gates/${encodedIssueId}/${encodedGateName}?token=${encodedToken}`;
|
|
56
|
+
}
|
|
57
|
+
// ============================================
|
|
58
|
+
// Token Validation
|
|
59
|
+
// ============================================
|
|
60
|
+
/**
|
|
61
|
+
* Validate that a webhook callback's token matches the expected token.
|
|
62
|
+
*
|
|
63
|
+
* Uses timing-safe comparison via `crypto.timingSafeEqual` to prevent
|
|
64
|
+
* timing attacks that could leak token information through response times.
|
|
65
|
+
*
|
|
66
|
+
* @param token - The token received in the webhook callback
|
|
67
|
+
* @param expectedToken - The expected token stored in gate state
|
|
68
|
+
* @returns `true` if the tokens match, `false` otherwise
|
|
69
|
+
*/
|
|
70
|
+
export function validateWebhookCallback(token, expectedToken) {
|
|
71
|
+
if (!token || !expectedToken) {
|
|
72
|
+
log.warn('Token validation failed: empty token provided');
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const tokenBuffer = Buffer.from(token, 'utf8');
|
|
76
|
+
const expectedBuffer = Buffer.from(expectedToken, 'utf8');
|
|
77
|
+
// timingSafeEqual requires buffers of equal length
|
|
78
|
+
if (tokenBuffer.length !== expectedBuffer.length) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return timingSafeEqual(tokenBuffer, expectedBuffer);
|
|
82
|
+
}
|
|
83
|
+
// ============================================
|
|
84
|
+
// Gate Evaluation
|
|
85
|
+
// ============================================
|
|
86
|
+
/**
|
|
87
|
+
* Pure function to evaluate a webhook gate's current status based on its
|
|
88
|
+
* definition and persisted state.
|
|
89
|
+
*
|
|
90
|
+
* Evaluation logic:
|
|
91
|
+
* - If gate state is `satisfied` -> return `{ satisfied: true }`
|
|
92
|
+
* - If gate state is `timed-out` -> return `{ satisfied: false, timedOut: true }`
|
|
93
|
+
* - If gate state is `active` and has a timeout deadline that has passed ->
|
|
94
|
+
* return `{ satisfied: false, timedOut: true }`
|
|
95
|
+
* - If gate state is `active` -> return `{ satisfied: false, callbackUrl }`
|
|
96
|
+
* where callbackUrl is reconstructed from state's signalSource if available
|
|
97
|
+
* - If gate state is `null` (not yet activated) -> return `{ satisfied: false }`
|
|
98
|
+
*
|
|
99
|
+
* @param gate - The gate definition from the workflow
|
|
100
|
+
* @param gateState - The persisted gate state, or null if the gate has not been activated
|
|
101
|
+
* @returns The evaluated webhook gate result
|
|
102
|
+
*/
|
|
103
|
+
export function evaluateWebhookGate(gate, gateState) {
|
|
104
|
+
if (!gateState) {
|
|
105
|
+
log.debug('Webhook gate not yet activated', { gateName: gate.name });
|
|
106
|
+
return { satisfied: false };
|
|
107
|
+
}
|
|
108
|
+
if (gateState.status === 'satisfied') {
|
|
109
|
+
log.debug('Webhook gate satisfied', { gateName: gate.name });
|
|
110
|
+
return { satisfied: true };
|
|
111
|
+
}
|
|
112
|
+
if (gateState.status === 'timed-out') {
|
|
113
|
+
log.debug('Webhook gate timed out', { gateName: gate.name });
|
|
114
|
+
return { satisfied: false, timedOut: true };
|
|
115
|
+
}
|
|
116
|
+
if (gateState.status === 'active') {
|
|
117
|
+
// Check if the gate has exceeded its timeout deadline
|
|
118
|
+
if (gateState.timeoutDeadline && Date.now() > gateState.timeoutDeadline) {
|
|
119
|
+
log.info('Webhook gate timeout deadline exceeded', {
|
|
120
|
+
gateName: gate.name,
|
|
121
|
+
issueId: gateState.issueId,
|
|
122
|
+
timeoutDeadline: gateState.timeoutDeadline,
|
|
123
|
+
});
|
|
124
|
+
return { satisfied: false, timedOut: true };
|
|
125
|
+
}
|
|
126
|
+
// Gate is active and waiting for callback
|
|
127
|
+
return {
|
|
128
|
+
satisfied: false,
|
|
129
|
+
callbackUrl: gateState.signalSource,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// Pending or unknown status
|
|
133
|
+
return { satisfied: false };
|
|
134
|
+
}
|
|
135
|
+
// ============================================
|
|
136
|
+
// Type Guards
|
|
137
|
+
// ============================================
|
|
138
|
+
/**
|
|
139
|
+
* Type guard to check if a gate trigger configuration is a webhook trigger.
|
|
140
|
+
*
|
|
141
|
+
* A valid webhook trigger must have an `endpoint` property of type string.
|
|
142
|
+
*
|
|
143
|
+
* @param trigger - The trigger configuration to check
|
|
144
|
+
* @returns `true` if the trigger is a WebhookGateTrigger
|
|
145
|
+
*/
|
|
146
|
+
export function isWebhookGateTrigger(trigger) {
|
|
147
|
+
return (typeof trigger === 'object' &&
|
|
148
|
+
trigger !== null &&
|
|
149
|
+
typeof trigger.endpoint === 'string' &&
|
|
150
|
+
trigger.endpoint.length > 0);
|
|
151
|
+
}
|
|
152
|
+
// ============================================
|
|
153
|
+
// Gate Filtering
|
|
154
|
+
// ============================================
|
|
155
|
+
/**
|
|
156
|
+
* Get all webhook gates that apply to a given workflow phase.
|
|
157
|
+
*
|
|
158
|
+
* Filters the workflow's gate definitions to return only those that:
|
|
159
|
+
* 1. Have type `webhook`
|
|
160
|
+
* 2. Either have no `appliesTo` restriction, or include the specified phase
|
|
161
|
+
*
|
|
162
|
+
* @param workflow - The workflow definition containing gate configurations
|
|
163
|
+
* @param phase - The phase name to filter gates for
|
|
164
|
+
* @returns Array of gate definitions that are webhook type and apply to the phase
|
|
165
|
+
*/
|
|
166
|
+
export function getApplicableWebhookGates(workflow, phase) {
|
|
167
|
+
if (!workflow.gates || workflow.gates.length === 0) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
return workflow.gates.filter((gate) => {
|
|
171
|
+
if (gate.type !== 'webhook') {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
// If no appliesTo is specified, the gate applies to all phases
|
|
175
|
+
if (!gate.appliesTo || gate.appliesTo.length === 0) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return gate.appliesTo.includes(phase);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
// ============================================
|
|
182
|
+
// Gate Activation
|
|
183
|
+
// ============================================
|
|
184
|
+
/**
|
|
185
|
+
* Creates the activation data for a webhook gate including a cryptographically
|
|
186
|
+
* random token, callback URL, and optional expiration timestamp.
|
|
187
|
+
*
|
|
188
|
+
* This function generates all the data needed to activate a webhook gate,
|
|
189
|
+
* but does not persist any state. The caller is responsible for storing the
|
|
190
|
+
* activation data (token in gate state, URL communicated to external system).
|
|
191
|
+
*
|
|
192
|
+
* @param issueId - The issue identifier this gate is associated with
|
|
193
|
+
* @param gateDef - The gate definition from the workflow
|
|
194
|
+
* @param baseUrl - The base URL of the server for building callback URLs
|
|
195
|
+
* @returns The webhook gate activation data
|
|
196
|
+
*/
|
|
197
|
+
export function createWebhookGateActivation(issueId, gateDef, baseUrl) {
|
|
198
|
+
const token = generateWebhookToken();
|
|
199
|
+
const callbackUrl = buildCallbackUrl(baseUrl, issueId, gateDef.name, token);
|
|
200
|
+
const activation = {
|
|
201
|
+
token,
|
|
202
|
+
callbackUrl,
|
|
203
|
+
};
|
|
204
|
+
// Compute expiration from gate timeout if configured
|
|
205
|
+
if (gateDef.timeout) {
|
|
206
|
+
const durationMs = parseDuration(gateDef.timeout.duration);
|
|
207
|
+
activation.expiresAt = Date.now() + durationMs;
|
|
208
|
+
}
|
|
209
|
+
log.info('Webhook gate activation created', {
|
|
210
|
+
issueId,
|
|
211
|
+
gateName: gateDef.name,
|
|
212
|
+
callbackUrl,
|
|
213
|
+
expiresAt: activation.expiresAt,
|
|
214
|
+
});
|
|
215
|
+
return activation;
|
|
216
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-gate.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/webhook-gate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { generateWebhookToken, buildCallbackUrl, validateWebhookCallback, evaluateWebhookGate, createWebhookGateActivation, } from './webhook-gate.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function makeGateDefinition(overrides = {}) {
|
|
7
|
+
return {
|
|
8
|
+
name: 'test-webhook',
|
|
9
|
+
type: 'webhook',
|
|
10
|
+
trigger: { endpoint: '/api/gates' },
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function makeGateState(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
issueId: 'issue-1',
|
|
17
|
+
gateName: 'test-webhook',
|
|
18
|
+
gateType: 'webhook',
|
|
19
|
+
status: 'active',
|
|
20
|
+
activatedAt: Date.now(),
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// generateWebhookToken
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
describe('generateWebhookToken', () => {
|
|
28
|
+
it('returns a 64-character hex string', () => {
|
|
29
|
+
const token = generateWebhookToken();
|
|
30
|
+
expect(token).toHaveLength(64);
|
|
31
|
+
expect(token).toMatch(/^[a-f0-9]{64}$/);
|
|
32
|
+
});
|
|
33
|
+
it('generates unique tokens', () => {
|
|
34
|
+
const token1 = generateWebhookToken();
|
|
35
|
+
const token2 = generateWebhookToken();
|
|
36
|
+
expect(token1).not.toBe(token2);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// buildCallbackUrl
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
describe('buildCallbackUrl', () => {
|
|
43
|
+
it('builds correct URL format', () => {
|
|
44
|
+
const url = buildCallbackUrl('https://api.example.com', 'issue-1', 'my-gate', 'abc123');
|
|
45
|
+
expect(url).toBe('https://api.example.com/api/gates/issue-1/my-gate?token=abc123');
|
|
46
|
+
});
|
|
47
|
+
it('URI-encodes special characters in issueId', () => {
|
|
48
|
+
const url = buildCallbackUrl('https://api.example.com', 'issue/1', 'gate', 'token');
|
|
49
|
+
expect(url).toContain('issue%2F1');
|
|
50
|
+
});
|
|
51
|
+
it('URI-encodes special characters in gateName', () => {
|
|
52
|
+
const url = buildCallbackUrl('https://api.example.com', 'issue-1', 'my gate', 'token');
|
|
53
|
+
expect(url).toContain('my%20gate');
|
|
54
|
+
});
|
|
55
|
+
it('normalizes trailing slashes on base URL', () => {
|
|
56
|
+
const url = buildCallbackUrl('https://api.example.com/', 'issue-1', 'gate', 'token');
|
|
57
|
+
expect(url).toBe('https://api.example.com/api/gates/issue-1/gate?token=token');
|
|
58
|
+
});
|
|
59
|
+
it('normalizes multiple trailing slashes', () => {
|
|
60
|
+
const url = buildCallbackUrl('https://api.example.com///', 'issue-1', 'gate', 'token');
|
|
61
|
+
expect(url).toBe('https://api.example.com/api/gates/issue-1/gate?token=token');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// validateWebhookCallback
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
describe('validateWebhookCallback', () => {
|
|
68
|
+
it('returns true for matching tokens', () => {
|
|
69
|
+
const token = 'test-webhook-token-value';
|
|
70
|
+
expect(validateWebhookCallback(token, token)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
it('returns false for mismatched tokens', () => {
|
|
73
|
+
expect(validateWebhookCallback('token-a', 'token-b')).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
it('returns false for empty token', () => {
|
|
76
|
+
expect(validateWebhookCallback('', 'expected')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it('returns false for empty expected token', () => {
|
|
79
|
+
expect(validateWebhookCallback('token', '')).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it('returns false for different length tokens', () => {
|
|
82
|
+
expect(validateWebhookCallback('short', 'a-much-longer-token')).toBe(false);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// evaluateWebhookGate
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
describe('evaluateWebhookGate', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
vi.useRealTimers();
|
|
95
|
+
});
|
|
96
|
+
it('returns not satisfied for null state', () => {
|
|
97
|
+
const gate = makeGateDefinition();
|
|
98
|
+
const result = evaluateWebhookGate(gate, null);
|
|
99
|
+
expect(result.satisfied).toBe(false);
|
|
100
|
+
expect(result.callbackUrl).toBeUndefined();
|
|
101
|
+
expect(result.timedOut).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
it('returns satisfied for satisfied state', () => {
|
|
104
|
+
const gate = makeGateDefinition();
|
|
105
|
+
const state = makeGateState({ status: 'satisfied', satisfiedAt: Date.now() });
|
|
106
|
+
const result = evaluateWebhookGate(gate, state);
|
|
107
|
+
expect(result.satisfied).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('returns timedOut for timed-out state', () => {
|
|
110
|
+
const gate = makeGateDefinition();
|
|
111
|
+
const state = makeGateState({ status: 'timed-out', timedOutAt: Date.now() });
|
|
112
|
+
const result = evaluateWebhookGate(gate, state);
|
|
113
|
+
expect(result.satisfied).toBe(false);
|
|
114
|
+
expect(result.timedOut).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('returns callbackUrl for active state', () => {
|
|
117
|
+
const gate = makeGateDefinition();
|
|
118
|
+
const state = makeGateState({
|
|
119
|
+
status: 'active',
|
|
120
|
+
signalSource: 'https://api.example.com/api/gates/issue-1/test-webhook?token=abc',
|
|
121
|
+
});
|
|
122
|
+
const result = evaluateWebhookGate(gate, state);
|
|
123
|
+
expect(result.satisfied).toBe(false);
|
|
124
|
+
expect(result.callbackUrl).toBe(state.signalSource);
|
|
125
|
+
});
|
|
126
|
+
it('returns timedOut when active gate has expired deadline', () => {
|
|
127
|
+
const gate = makeGateDefinition();
|
|
128
|
+
const state = makeGateState({
|
|
129
|
+
status: 'active',
|
|
130
|
+
timeoutDeadline: Date.now() - 1000, // deadline in the past
|
|
131
|
+
});
|
|
132
|
+
const result = evaluateWebhookGate(gate, state);
|
|
133
|
+
expect(result.satisfied).toBe(false);
|
|
134
|
+
expect(result.timedOut).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it('returns not timed out when active gate has future deadline', () => {
|
|
137
|
+
const gate = makeGateDefinition();
|
|
138
|
+
const state = makeGateState({
|
|
139
|
+
status: 'active',
|
|
140
|
+
timeoutDeadline: Date.now() + 60_000, // deadline in the future
|
|
141
|
+
});
|
|
142
|
+
const result = evaluateWebhookGate(gate, state);
|
|
143
|
+
expect(result.satisfied).toBe(false);
|
|
144
|
+
expect(result.timedOut).toBeUndefined();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
// createWebhookGateActivation
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
describe('createWebhookGateActivation', () => {
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
vi.useFakeTimers();
|
|
153
|
+
vi.setSystemTime(new Date('2025-06-01T12:00:00Z'));
|
|
154
|
+
});
|
|
155
|
+
afterEach(() => {
|
|
156
|
+
vi.useRealTimers();
|
|
157
|
+
});
|
|
158
|
+
it('generates a token', () => {
|
|
159
|
+
const gate = makeGateDefinition();
|
|
160
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
161
|
+
expect(activation.token).toHaveLength(64);
|
|
162
|
+
expect(activation.token).toMatch(/^[a-f0-9]{64}$/);
|
|
163
|
+
});
|
|
164
|
+
it('builds a callback URL with the generated token', () => {
|
|
165
|
+
const gate = makeGateDefinition({ name: 'review-gate' });
|
|
166
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
167
|
+
expect(activation.callbackUrl).toContain('https://api.example.com/api/gates/issue-1/review-gate');
|
|
168
|
+
expect(activation.callbackUrl).toContain(`token=${activation.token}`);
|
|
169
|
+
});
|
|
170
|
+
it('computes expiresAt from timeout duration', () => {
|
|
171
|
+
const gate = makeGateDefinition({
|
|
172
|
+
timeout: { duration: '4h', action: 'fail' },
|
|
173
|
+
});
|
|
174
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
175
|
+
expect(activation.expiresAt).toBe(Date.now() + 14_400_000);
|
|
176
|
+
});
|
|
177
|
+
it('does not set expiresAt when no timeout configured', () => {
|
|
178
|
+
const gate = makeGateDefinition();
|
|
179
|
+
const activation = createWebhookGateActivation('issue-1', gate, 'https://api.example.com');
|
|
180
|
+
expect(activation.expiresAt).toBeUndefined();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -4,11 +4,39 @@
|
|
|
4
4
|
* Declarative workflow graph definitions using YAML (v1.1 schema extension).
|
|
5
5
|
* Defines phases, transitions, escalation ladder, gates, and parallelism.
|
|
6
6
|
*/
|
|
7
|
-
export type { EscalationStrategy, PhaseDefinition, TransitionDefinition, EscalationLadderRung, EscalationConfig, GateDefinition, ParallelismGroupDefinition, WorkflowDefinition, } from './workflow-types.js';
|
|
8
|
-
export { PhaseDefinitionSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
7
|
+
export type { EscalationStrategy, PhaseDefinition, PhaseOutputDeclaration, PhaseInputDeclaration, TransitionDefinition, EscalationLadderRung, EscalationConfig, GateDefinition, ParallelismGroupDefinition, WorkflowDefinition, BranchingDefinition, TemplateRetryConfig, TemplateTimeoutConfig, } from './workflow-types.js';
|
|
8
|
+
export { PhaseDefinitionSchema, PhaseOutputDeclarationSchema, PhaseInputDeclarationSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, BranchingDefinitionSchema, TemplateRetryConfigSchema, TemplateTimeoutConfigSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
9
9
|
export { loadWorkflowDefinitionFile, getBuiltinWorkflowDir, getBuiltinWorkflowPath, } from './workflow-loader.js';
|
|
10
|
-
export type { WorkflowRegistryConfig } from './workflow-registry.js';
|
|
10
|
+
export type { WorkflowRegistryConfig, WorkflowStoreSource } from './workflow-registry.js';
|
|
11
11
|
export { WorkflowRegistry } from './workflow-registry.js';
|
|
12
12
|
export type { TransitionContext, TransitionResult } from './transition-engine.js';
|
|
13
13
|
export { evaluateTransitions } from './transition-engine.js';
|
|
14
|
+
export type { BranchingResult } from './branching-router.js';
|
|
15
|
+
export { evaluateBranching } from './branching-router.js';
|
|
16
|
+
export { parseDuration, DurationParseError } from './duration.js';
|
|
17
|
+
export type { ResolvedRetryConfig, ResolvedTimeoutConfig } from './retry-resolver.js';
|
|
18
|
+
export { resolveRetryConfig, resolveTimeoutConfig } from './retry-resolver.js';
|
|
19
|
+
export type { EvaluationContext } from './expression/index.js';
|
|
20
|
+
export { buildEvaluationContext, evaluateCondition } from './expression/index.js';
|
|
21
|
+
export type { GateState, GateStorage } from './gate-state.js';
|
|
22
|
+
export { InMemoryGateStorage, initGateStorage, activateGate, satisfyGate, timeoutGate, } from './gate-state.js';
|
|
23
|
+
export { parseDuration as parseGateDuration } from './gate-state.js';
|
|
24
|
+
export type { SignalGateTrigger, SignalGateResult } from './gates/signal-gate.js';
|
|
25
|
+
export { isSignalGateTrigger, evaluateSignalGate, getApplicableSignalGates, createImplicitHoldGate, createImplicitResumeGate, IMPLICIT_HOLD_GATE_NAME, IMPLICIT_RESUME_GATE_NAME, } from './gates/signal-gate.js';
|
|
26
|
+
export type { WebhookGateTrigger, WebhookGateResult, WebhookGateActivation } from './gates/webhook-gate.js';
|
|
27
|
+
export { generateWebhookToken, buildCallbackUrl, validateWebhookCallback, evaluateWebhookGate, isWebhookGateTrigger, getApplicableWebhookGates, createWebhookGateActivation, } from './gates/webhook-gate.js';
|
|
28
|
+
export type { TimeoutCheckResult, TimedOutGate, TimeoutResolution } from './gates/timeout-engine.js';
|
|
29
|
+
export { checkGateTimeout, checkAllGateTimeouts, resolveTimeoutAction, processGateTimeouts, } from './gates/timeout-engine.js';
|
|
30
|
+
export type { TimerGateTrigger, TimerGateResult } from './gates/timer-gate.js';
|
|
31
|
+
export { evaluateTimerGate, computeNextCronFireTime, isTimerGateTrigger, getApplicableTimerGates, parseCronField, parseCronExpression, } from './gates/timer-gate.js';
|
|
32
|
+
export type { GateEvaluationOptions, GateEvaluationResult } from './gates/gate-evaluator.js';
|
|
33
|
+
export { evaluateGatesForPhase, activateGatesForPhase, clearGatesForIssue, getApplicableGates, } from './gates/gate-evaluator.js';
|
|
34
|
+
export { PhaseOutputCollector } from './phase-output-collector.js';
|
|
35
|
+
export { PhaseContextInjector } from './phase-context-injector.js';
|
|
36
|
+
export type { ParallelTask, ParallelTaskResult, ParallelTaskError, ParallelismResult, ParallelismStrategy, ParallelismStrategyOptions, } from './parallelism-types.js';
|
|
37
|
+
export { ConcurrencySemaphore } from './concurrency-semaphore.js';
|
|
38
|
+
export { ParallelismExecutor } from './parallelism-executor.js';
|
|
39
|
+
export type { AgentCancellation } from './agent-cancellation.js';
|
|
40
|
+
export { InMemoryAgentCancellation } from './agent-cancellation.js';
|
|
41
|
+
export { FanOutStrategy, FanInStrategy, RaceStrategy } from './strategies/index.js';
|
|
14
42
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/workflow/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,oBAAoB,EACpB,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,0BAA0B,EAC1B,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/workflow/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,7 +4,26 @@
|
|
|
4
4
|
* Declarative workflow graph definitions using YAML (v1.1 schema extension).
|
|
5
5
|
* Defines phases, transitions, escalation ladder, gates, and parallelism.
|
|
6
6
|
*/
|
|
7
|
-
export { PhaseDefinitionSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
7
|
+
export { PhaseDefinitionSchema, PhaseOutputDeclarationSchema, PhaseInputDeclarationSchema, TransitionDefinitionSchema, EscalationLadderRungSchema, EscalationConfigSchema, GateDefinitionSchema, ParallelismGroupDefinitionSchema, WorkflowDefinitionSchema, BranchingDefinitionSchema, TemplateRetryConfigSchema, TemplateTimeoutConfigSchema, validateWorkflowDefinition, } from './workflow-types.js';
|
|
8
8
|
export { loadWorkflowDefinitionFile, getBuiltinWorkflowDir, getBuiltinWorkflowPath, } from './workflow-loader.js';
|
|
9
9
|
export { WorkflowRegistry } from './workflow-registry.js';
|
|
10
10
|
export { evaluateTransitions } from './transition-engine.js';
|
|
11
|
+
export { evaluateBranching } from './branching-router.js';
|
|
12
|
+
// Duration parser
|
|
13
|
+
export { parseDuration, DurationParseError } from './duration.js';
|
|
14
|
+
export { resolveRetryConfig, resolveTimeoutConfig } from './retry-resolver.js';
|
|
15
|
+
export { buildEvaluationContext, evaluateCondition } from './expression/index.js';
|
|
16
|
+
export { InMemoryGateStorage, initGateStorage, activateGate, satisfyGate, timeoutGate, } from './gate-state.js';
|
|
17
|
+
export { parseDuration as parseGateDuration } from './gate-state.js';
|
|
18
|
+
export { isSignalGateTrigger, evaluateSignalGate, getApplicableSignalGates, createImplicitHoldGate, createImplicitResumeGate, IMPLICIT_HOLD_GATE_NAME, IMPLICIT_RESUME_GATE_NAME, } from './gates/signal-gate.js';
|
|
19
|
+
export { generateWebhookToken, buildCallbackUrl, validateWebhookCallback, evaluateWebhookGate, isWebhookGateTrigger, getApplicableWebhookGates, createWebhookGateActivation, } from './gates/webhook-gate.js';
|
|
20
|
+
export { checkGateTimeout, checkAllGateTimeouts, resolveTimeoutAction, processGateTimeouts, } from './gates/timeout-engine.js';
|
|
21
|
+
export { evaluateTimerGate, computeNextCronFireTime, isTimerGateTrigger, getApplicableTimerGates, parseCronField, parseCronExpression, } from './gates/timer-gate.js';
|
|
22
|
+
export { evaluateGatesForPhase, activateGatesForPhase, clearGatesForIssue, getApplicableGates, } from './gates/gate-evaluator.js';
|
|
23
|
+
// Phase output/input handling
|
|
24
|
+
export { PhaseOutputCollector } from './phase-output-collector.js';
|
|
25
|
+
export { PhaseContextInjector } from './phase-context-injector.js';
|
|
26
|
+
export { ConcurrencySemaphore } from './concurrency-semaphore.js';
|
|
27
|
+
export { ParallelismExecutor } from './parallelism-executor.js';
|
|
28
|
+
export { InMemoryAgentCancellation } from './agent-cancellation.js';
|
|
29
|
+
export { FanOutStrategy, FanInStrategy, RaceStrategy } from './strategies/index.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParallelismExecutor
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates parallel task execution using strategy pattern.
|
|
5
|
+
* Each parallelism group in the workflow definition is executed
|
|
6
|
+
* through its configured strategy with concurrency limiting.
|
|
7
|
+
*/
|
|
8
|
+
import type { ParallelismGroupDefinition } from './workflow-types.js';
|
|
9
|
+
import type { ParallelTask, ParallelTaskResult, ParallelismResult, ParallelismStrategy } from './parallelism-types.js';
|
|
10
|
+
export declare class ParallelismExecutor {
|
|
11
|
+
private readonly strategies;
|
|
12
|
+
/**
|
|
13
|
+
* Register a strategy implementation for a strategy name.
|
|
14
|
+
*/
|
|
15
|
+
registerStrategy(name: string, strategy: ParallelismStrategy): void;
|
|
16
|
+
/**
|
|
17
|
+
* Get a registered strategy by name.
|
|
18
|
+
*/
|
|
19
|
+
getStrategy(name: string): ParallelismStrategy | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* Execute a parallelism group with the configured strategy.
|
|
22
|
+
*/
|
|
23
|
+
execute(group: ParallelismGroupDefinition, tasks: ParallelTask[], dispatch: (task: ParallelTask) => Promise<ParallelTaskResult>): Promise<ParallelismResult>;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=parallelism-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parallelism-executor.d.ts","sourceRoot":"","sources":["../../../src/workflow/parallelism-executor.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAA;AACrE,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EAEpB,MAAM,wBAAwB,CAAA;AAG/B,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8C;IAEzE;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAInE;;OAEG;IACH,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI1D;;OAEG;IACG,OAAO,CACX,KAAK,EAAE,0BAA0B,EACjC,KAAK,EAAE,YAAY,EAAE,EACrB,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,kBAAkB,CAAC,GAC5D,OAAO,CAAC,iBAAiB,CAAC;CA8B9B"}
|