@oomol-lab/open-flow 0.1.0-beta.74 → 0.1.0-beta.75
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/README.md +1 -0
- package/dist/browser/{ContentIcon-CUQHjQIw.js → ContentIcon-FCpKiwjp.js} +2 -1
- package/dist/browser/{FlowCanvas-Dtf8_cQp.js → FlowCanvas-dzRBnlUT.js} +1 -1
- package/dist/browser/{createResourceDialog-DI9pL-Yr.js → createResourceDialog-BeYDdUl-.js} +2 -2
- package/dist/browser/{dialog-BYJGvItc.js → dialog-ZtpsFbl4.js} +284 -13
- package/dist/browser/{exportSvg-BOoy7-Db.js → exportSvg-DqXw1yf-.js} +2 -2
- package/dist/browser/{flowChanges-RL-H4CjD.js → flowChanges-b_5UobFj.js} +285 -44
- package/dist/browser/{flowWorkspace-BkDsETj0.js → flowWorkspace-ItojZbO4.js} +414 -53
- package/dist/browser/preview.css +181 -176
- package/dist/browser/preview.js +955 -182
- package/dist/browser/workbench.css +181 -176
- package/dist/browser/workbench.js +12 -6
- package/dist/browser/{workbenchSelect-BcWPWGzw.js → workbenchSelect-M441Lz0d.js} +1 -1
- package/dist/command/index.js +36 -36
- package/dist/common/authoring.d.ts +4 -4
- package/dist/common/authoringExamples.d.ts +3 -2
- package/dist/common/authoringSchema.d.ts +67 -4
- package/dist/common/control-api-conformance.js +58 -7
- package/dist/common/control-api.js +58 -7
- package/dist/common/control-requests.d.ts +3 -3
- package/dist/common/control-requests.js +234 -62
- package/dist/common/engine-contract.d.ts +1 -0
- package/dist/common/flow-authoring.js +56 -7
- package/dist/common/flow-change.d.ts +1 -1
- package/dist/common/flow-change.js +56 -7
- package/dist/common/flow-encoding.js +55 -7
- package/dist/common/flow-graph.d.ts +2 -1
- package/dist/common/flow-retry.d.ts +1 -1
- package/dist/common/flow-semantics.js +810 -577
- package/dist/common/http-request.d.ts +156 -0
- package/dist/common/http-request.js +5254 -0
- package/dist/common/mcp.d.ts +3 -3
- package/dist/common/mcp.js +58 -7
- package/dist/common/run-events.js +11 -9
- package/dist/common/runtime-contract.js +3 -0
- package/dist/common/scheduler.d.ts +1 -1
- package/dist/common/scheduler.js +70 -21
- package/dist/common/webhook-response.d.ts +0 -2
- package/dist/common/webhook-trigger.js +12 -11
- package/package.json +5 -1
- package/skills/open-flow/references/flow-authoring.md +1 -0
- package/skills/open-flow/references/flow-n8n-conversion.md +19 -15
- package/src/control/common/api.ts +1 -0
- package/src/control/common/authoring.ts +14 -3
- package/src/control/common/authoringExamples.ts +8 -0
- package/src/control/common/authoringSchema.ts +30 -24
- package/src/execution/common/engineContract.ts +4 -0
- package/src/execution/common/events.ts +3 -0
- package/src/execution/common/node-execution.ts +4 -2
- package/src/execution/common/scheduler.ts +1 -0
- package/src/flow/common/change.ts +3 -2
- package/src/flow/common/changeSchema.ts +12 -3
- package/src/flow/common/graph.ts +10 -4
- package/src/flow/common/retry.ts +1 -1
- package/src/flow/common/semantics.ts +60 -3
- package/src/flow/common/webhookResponse.ts +3 -7
- package/src/{openapi → http}/common/authBindings.ts +3 -3
- package/src/http/common/headers.ts +5 -0
- package/src/http/common/httpRequest.ts +251 -0
- package/src/trigger/common/webhook.ts +4 -3
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
authoringDefinitionSchemas as definitions,
|
|
10
10
|
} from '../../flow/common/changeSchema.ts'
|
|
11
11
|
import { retryConfigurationSchema, retryNodeTypes } from '../../flow/common/retry.ts'
|
|
12
|
+
import { httpRequestConfigSchema } from '../../http/common/httpRequest.ts'
|
|
12
13
|
import { authoringLlmConfiguration } from './authoringLlm.ts'
|
|
13
14
|
import { authoringOpenApiSchema } from './authoringOpenApi.ts'
|
|
14
15
|
|
|
@@ -31,6 +32,7 @@ export const authoringTypes = [
|
|
|
31
32
|
'llm',
|
|
32
33
|
'decision',
|
|
33
34
|
'openapi',
|
|
35
|
+
'http-request',
|
|
34
36
|
'condition',
|
|
35
37
|
'value',
|
|
36
38
|
'wait',
|
|
@@ -111,6 +113,7 @@ export const authoringConfigurations = {
|
|
|
111
113
|
'code': z.strictObject({ inputs: inputFields, outputs: outputFields, capabilities: authoringCapabilities.optional() }),
|
|
112
114
|
'connector': definitions.connector.omit({ outputMode: true }).strict(),
|
|
113
115
|
'openapi': authoringOpenApiSchema,
|
|
116
|
+
'http-request': httpRequestConfigSchema,
|
|
114
117
|
'agent': definitions.agent
|
|
115
118
|
.omit({ prompt: true, tools: true })
|
|
116
119
|
.extend({
|
|
@@ -296,15 +299,17 @@ export function authoringSchema(kind?: AuthoringType) {
|
|
|
296
299
|
? 'Configure static parameters with node.add.inputs or input.set. Definitions come from the selected Trigger key; execution output and variable sources are unavailable.'
|
|
297
300
|
: kind == 'llm'
|
|
298
301
|
? 'Configure model settings, message templates and history as defaults. Input bindings with the same names override these defaults; named business inputs can be bound directly. Template strings interpolate {{name}}.'
|
|
299
|
-
: kind == '
|
|
300
|
-
? '
|
|
301
|
-
: kind == '
|
|
302
|
-
? 'Select
|
|
303
|
-
: kind == '
|
|
304
|
-
? '
|
|
305
|
-
: kind == '
|
|
306
|
-
? '
|
|
307
|
-
:
|
|
302
|
+
: kind == 'http-request'
|
|
303
|
+
? 'Configure method, query/headers modes and names, bodyFormat, authentication, responseFormat and statusPolicy. Bind url, query.<name>/header.<name> or whole query/headers objects, body and auth.token (Basic: auth.username/auth.password). Credentials accept variables or upstream outputs only. Outputs: body, statusCode, headers. Authentication is replaced as a complete choice.'
|
|
304
|
+
: kind == 'openapi'
|
|
305
|
+
? 'Select an operation; the tool resolves its ports and authentication. Read authenticationChoices from node details. authentication is replaced as a complete choice. Bind advertised credentials through inputs.'
|
|
306
|
+
: kind == 'workflow'
|
|
307
|
+
? 'Select another workflow with config.flowId. A workflow cannot call itself, including in a disconnected node. Owned unpublished or unavailable targets may be saved as incomplete drafts. Use node.refresh to load the current published interface.'
|
|
308
|
+
: kind == 'agent'
|
|
309
|
+
? 'Tools accept Connector Actions or {kind: "workflow", flowId, name, description, approval, inputs}. Workflow parameter sources use model, input or value. The platform derives the interface. Refresh a workflow tool with node.refresh and tool: its name.'
|
|
310
|
+
: kind == 'code'
|
|
311
|
+
? 'Select workflows through config.capabilities: [{kind: "workflow", flowId, name}]. Call context.workflows.call(name, input) and await its business output. The platform derives input/output types. Refresh a selected workflow tool with node.refresh and tool: its name. Calls obey node and Run time limits.'
|
|
312
|
+
: 'Supply business configuration and input data. Fixed definitions are assembled by the tool. Existing constraints and defaults are preserved on partial updates.',
|
|
308
313
|
example: {
|
|
309
314
|
op: 'node.add',
|
|
310
315
|
as: 'step',
|
|
@@ -312,15 +317,16 @@ export function authoringSchema(kind?: AuthoringType) {
|
|
|
312
317
|
name: 'Step',
|
|
313
318
|
...((
|
|
314
319
|
{
|
|
315
|
-
start: { config: { inputDefs: {} } },
|
|
316
|
-
outputs: { config: { outputDefs: {} } },
|
|
317
|
-
workflow: { config: { flowId: '<Flow ID>' } },
|
|
318
|
-
connector: { config: { action: '<Action ID>', connectionId: '<Connection ID>' } },
|
|
319
|
-
poll: { config: { key: '<Trigger key>', schedule: [{ type: 'every', unit: 'minute', value: 5 }] } },
|
|
320
|
-
integration: { config: { key: '<Trigger key>' } },
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
320
|
+
'start': { config: { inputDefs: {} } },
|
|
321
|
+
'outputs': { config: { outputDefs: {} } },
|
|
322
|
+
'workflow': { config: { flowId: '<Flow ID>' } },
|
|
323
|
+
'connector': { config: { action: '<Action ID>', connectionId: '<Connection ID>' } },
|
|
324
|
+
'poll': { config: { key: '<Trigger key>', schedule: [{ type: 'every', unit: 'minute', value: 5 }] } },
|
|
325
|
+
'integration': { config: { key: '<Trigger key>' } },
|
|
326
|
+
'http-request': { config: {}, inputs: { url: { kind: 'value', value: 'https://example.com' } } },
|
|
327
|
+
'openapi': { config: { sourceUrl: 'https://example.com/openapi.json', path: '/items', method: 'get' } },
|
|
328
|
+
'code': { code: 'export default ({value}) => ({result:value})', inputs: { value: { kind: 'value', value: 'Hello' } } },
|
|
329
|
+
'condition': {
|
|
324
330
|
config: {
|
|
325
331
|
match: 'first',
|
|
326
332
|
branches: [
|
|
@@ -335,15 +341,15 @@ export function authoringSchema(kind?: AuthoringType) {
|
|
|
335
341
|
],
|
|
336
342
|
},
|
|
337
343
|
},
|
|
338
|
-
value: { config: { values: { threshold: 100, region: 'EU' } } },
|
|
339
|
-
webhook: { config: { method: 'POST', body: { orderId: { schema: { type: 'string' }, nullable: false } } } },
|
|
340
|
-
wait: { prompt: 'Continue processing this order?', inputs: { order: { kind: 'value', value: { orderId: 'A-1' } } } },
|
|
341
|
-
approval: { prompt: 'Approve this order?', inputs: { order: { kind: 'value', value: { orderId: 'A-1' } } } },
|
|
342
|
-
llm: {
|
|
344
|
+
'value': { config: { values: { threshold: 100, region: 'EU' } } },
|
|
345
|
+
'webhook': { config: { method: 'POST', body: { orderId: { schema: { type: 'string' }, nullable: false } } } },
|
|
346
|
+
'wait': { prompt: 'Continue processing this order?', inputs: { order: { kind: 'value', value: { orderId: 'A-1' } } } },
|
|
347
|
+
'approval': { prompt: 'Approve this order?', inputs: { order: { kind: 'value', value: { orderId: 'A-1' } } } },
|
|
348
|
+
'llm': {
|
|
343
349
|
config: { model: { model: 'deepseek-v4-flash' }, template: [{ role: 'user', content: 'Summarize {{order}}.' }] },
|
|
344
350
|
inputs: { order: { kind: 'value', value: { orderId: 'A-1', total: 42 } } },
|
|
345
351
|
},
|
|
346
|
-
agent: {
|
|
352
|
+
'agent': {
|
|
347
353
|
config: {
|
|
348
354
|
model: 'deepseek-v4-flash',
|
|
349
355
|
},
|
|
@@ -6,6 +6,7 @@ export interface EngineContract {
|
|
|
6
6
|
readonly waitAutoContinue?: boolean
|
|
7
7
|
readonly webhookResponses?: boolean
|
|
8
8
|
readonly workflowTools?: boolean
|
|
9
|
+
readonly httpRequests?: boolean
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const currentEngineContract = 'open-flow-engine/v8'
|
|
@@ -13,6 +14,7 @@ export const nodejsEngineContract = 'open-flow-engine/v8/nodejs-compat-v1'
|
|
|
13
14
|
|
|
14
15
|
const currentContract: EngineContract = {
|
|
15
16
|
workflowTools: true,
|
|
17
|
+
httpRequests: true,
|
|
16
18
|
waitAutoContinue: true,
|
|
17
19
|
webhookResponses: true,
|
|
18
20
|
platformExports: new Set(['engineContract', 'identity']),
|
|
@@ -64,12 +66,14 @@ export function findEngineContract(contract: string): EngineContract | undefined
|
|
|
64
66
|
return {
|
|
65
67
|
...(contract.endsWith('/nodejs-compat-v1') ? nodejsContract : currentContract),
|
|
66
68
|
workflowTools: false,
|
|
69
|
+
httpRequests: false,
|
|
67
70
|
platformSource: `export const engineContract = ${JSON.stringify(contract)}\nexport function identity(value) { return value }\n`,
|
|
68
71
|
}
|
|
69
72
|
if (contract == 'open-flow-engine/v6' || contract == 'open-flow-engine/v6/nodejs-compat-v1')
|
|
70
73
|
return {
|
|
71
74
|
...(contract == 'open-flow-engine/v6' ? currentContract : nodejsContract),
|
|
72
75
|
workflowTools: false,
|
|
76
|
+
httpRequests: false,
|
|
73
77
|
waitAutoContinue: false,
|
|
74
78
|
webhookResponses: false,
|
|
75
79
|
platformSource: `export const engineContract = ${JSON.stringify(contract)}
|
|
@@ -22,6 +22,7 @@ const nodeKinds = new Set([
|
|
|
22
22
|
'decision',
|
|
23
23
|
'javascript',
|
|
24
24
|
'openapi',
|
|
25
|
+
'http-request',
|
|
25
26
|
'llm',
|
|
26
27
|
'value',
|
|
27
28
|
'wait',
|
|
@@ -54,6 +55,7 @@ function nodeKind(
|
|
|
54
55
|
| 'decision'
|
|
55
56
|
| 'javascript'
|
|
56
57
|
| 'openapi'
|
|
58
|
+
| 'http-request'
|
|
57
59
|
| 'llm'
|
|
58
60
|
| 'value'
|
|
59
61
|
| 'wait'
|
|
@@ -73,6 +75,7 @@ function nodeKind(
|
|
|
73
75
|
| 'decision'
|
|
74
76
|
| 'javascript'
|
|
75
77
|
| 'openapi'
|
|
78
|
+
| 'http-request'
|
|
76
79
|
| 'llm'
|
|
77
80
|
| 'value'
|
|
78
81
|
| 'wait'
|
|
@@ -108,6 +108,7 @@ function nodeKind(
|
|
|
108
108
|
| 'decision'
|
|
109
109
|
| 'javascript'
|
|
110
110
|
| 'openapi'
|
|
111
|
+
| 'http-request'
|
|
111
112
|
| 'llm'
|
|
112
113
|
| 'value'
|
|
113
114
|
| 'wait'
|
|
@@ -319,7 +320,8 @@ export function executeNode(
|
|
|
319
320
|
const title = nodeTitle(node)
|
|
320
321
|
const projectedInputs = Object.fromEntries(
|
|
321
322
|
Object.entries(nodeInputs).filter(([handle]) => {
|
|
322
|
-
if (node.kind == 'task' && 'executor' in node.task && node.task
|
|
323
|
+
if (node.kind == 'task' && 'executor' in node.task && ['openapi', 'http-request'].includes(node.task.executor.kind) && handle.startsWith('auth.'))
|
|
324
|
+
return false
|
|
323
325
|
const mapping = nodeInputMappings(node)[handle]
|
|
324
326
|
return mapping?.kind != 'sources' || mapping.sources.every((source) => source.kind != 'binding')
|
|
325
327
|
}),
|
|
@@ -365,7 +367,7 @@ export function executeNode(
|
|
|
365
367
|
Object.entries(nodeInputs).filter(([handle]) => {
|
|
366
368
|
if (additional.has(handle)) return false
|
|
367
369
|
const task = 'executor' in node.task ? node.task : undefined
|
|
368
|
-
if (task
|
|
370
|
+
if (task == null || !['openapi', 'http-request'].includes(task.executor.kind)) return true
|
|
369
371
|
const mapping = node.inputs[handle]
|
|
370
372
|
return (
|
|
371
373
|
mapping?.kind != 'unset' &&
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { JsonValue, Port, PortDefinition } from '../../types/index.ts'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { assertHttpAuthBindings } from '../../http/common/authBindings.ts'
|
|
4
4
|
export { fixedInputValue, inputValue, inputValues } from './inputValue.ts'
|
|
5
5
|
import type { RetryConfiguration } from './retry.ts'
|
|
6
6
|
import type { WebhookMethod } from './webhookMethod.ts'
|
|
@@ -167,6 +167,7 @@ export interface ConditionNode extends GraphNodeBase {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
export type ManagedTaskExecutor =
|
|
170
|
+
| import('../../http/common/httpRequest.ts').HttpRequestExecutor
|
|
170
171
|
| import('../../workflow/common/workflow.ts').WorkflowExecutor
|
|
171
172
|
| import('../../decision/common/decision.ts').DecisionExecutor
|
|
172
173
|
| import('../../openapi/common/openapi.ts').OpenApiExecutor
|
|
@@ -1031,6 +1032,6 @@ export function applyFlowChanges(content: RevisionContent, operations: readonly
|
|
|
1031
1032
|
}
|
|
1032
1033
|
}
|
|
1033
1034
|
}
|
|
1034
|
-
|
|
1035
|
+
assertHttpAuthBindings(document)
|
|
1035
1036
|
return upgradeDecodedRevisionContent({ document, modelVersion: currentFlowModelVersion, modules })
|
|
1036
1037
|
}
|
|
@@ -3,7 +3,8 @@ import type { ChangeOperation, FlowDocument, JsonValue, RevisionContent } from '
|
|
|
3
3
|
import { dequal } from 'dequal/lite'
|
|
4
4
|
import { z } from 'zod'
|
|
5
5
|
import { limitDecisionTask } from '../../decision/common/decision.ts'
|
|
6
|
-
import {
|
|
6
|
+
import { assertHttpAuthBindings } from '../../http/common/authBindings.ts'
|
|
7
|
+
import { httpRequestExecutorSchema } from '../../http/common/httpRequest.ts'
|
|
7
8
|
import { checkJsonDepth } from './json.ts'
|
|
8
9
|
import { retryConfigurationSchema } from './retry.ts'
|
|
9
10
|
import { triggerScheduleSchema } from './triggerScheduleSchema.ts'
|
|
@@ -118,6 +119,7 @@ const managedFields = z.object({
|
|
|
118
119
|
.optional(),
|
|
119
120
|
}),
|
|
120
121
|
z.strictObject({ kind: z.literal('workflow'), flowId: text, revisionId: text }),
|
|
122
|
+
httpRequestExecutorSchema,
|
|
121
123
|
]),
|
|
122
124
|
})
|
|
123
125
|
|
|
@@ -591,7 +593,7 @@ export function decodeRevisionContent(value: unknown): RevisionContent {
|
|
|
591
593
|
checkJsonDepth(value)
|
|
592
594
|
const content = revision.parse(value) as RevisionContent
|
|
593
595
|
checkModelVersion(content)
|
|
594
|
-
|
|
596
|
+
assertHttpAuthBindings(content.document)
|
|
595
597
|
return content
|
|
596
598
|
}
|
|
597
599
|
|
|
@@ -601,7 +603,7 @@ export function decodeRevisionEnvelope(value: unknown): RevisionContent {
|
|
|
601
603
|
const candidate = revisionSource.modelVersion == 2 ? { ...revisionSource, document: upgradeLegacyDocument(revisionSource.document) } : value
|
|
602
604
|
const content = envelope.parse(candidate) as RevisionContent
|
|
603
605
|
checkModelVersion(content)
|
|
604
|
-
|
|
606
|
+
assertHttpAuthBindings(content.document)
|
|
605
607
|
return { modelVersion: content.modelVersion, document: content.document, modules: content.modules }
|
|
606
608
|
}
|
|
607
609
|
|
|
@@ -711,6 +713,13 @@ export function changeOperationsSchema(kind?: string): JsonValue {
|
|
|
711
713
|
}
|
|
712
714
|
|
|
713
715
|
function checkModelVersion(content: RevisionContent): void {
|
|
716
|
+
if (
|
|
717
|
+
content.modelVersion < 11 &&
|
|
718
|
+
Object.values(content.document.graph.nodes).some(
|
|
719
|
+
(candidateNode) => candidateNode.kind == 'task' && 'executor' in candidateNode.task && candidateNode.task.executor.kind == 'http-request',
|
|
720
|
+
)
|
|
721
|
+
)
|
|
722
|
+
throw new TypeError('HTTP Request requires Flow model version 11.')
|
|
714
723
|
if (
|
|
715
724
|
content.modelVersion < 11 &&
|
|
716
725
|
Object.values(content.document.graph.nodes).some(
|
package/src/flow/common/graph.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputPort } from './change.ts'
|
|
1
2
|
import type {
|
|
2
3
|
BindingSource,
|
|
3
4
|
FlowDocument,
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
} from './change.ts'
|
|
15
16
|
import type { Diagnostic } from './semantics.ts'
|
|
16
17
|
|
|
18
|
+
import { httpHeaderNameIssue } from '../../http/common/headers.ts'
|
|
17
19
|
import { missingTriggerConfig, resolveTriggerConfig } from '../../trigger/common/config.ts'
|
|
18
20
|
import { triggerOutputDefinitions, triggerOutputPorts, webhookSupportsBody } from '../../trigger/common/contract.ts'
|
|
19
21
|
import { isTriggerNode, portsByHandle, validVariableName } from './change.ts'
|
|
@@ -27,7 +29,6 @@ import {
|
|
|
27
29
|
webhookResponseInputs,
|
|
28
30
|
validWebhookStatus,
|
|
29
31
|
webhookHeaderValues,
|
|
30
|
-
webhookHeaderNameIssue,
|
|
31
32
|
webhookHeaderValuePort,
|
|
32
33
|
webhookHeadersPort,
|
|
33
34
|
webhookResponseAllowsBody,
|
|
@@ -92,7 +93,7 @@ export function webhookResponseDiagnostics(graph: Graph, triggerId: string, path
|
|
|
92
93
|
] as const) {
|
|
93
94
|
const names: string[] = []
|
|
94
95
|
for (const name of Object.keys(headers ?? {})) {
|
|
95
|
-
if (
|
|
96
|
+
if (httpHeaderNameIssue(name, names) != null)
|
|
96
97
|
diagnostics.push(graphDiagnostic('trigger.webhook-response-header-name', 'Use valid, unique HTTP header names.', location))
|
|
97
98
|
names.push(name)
|
|
98
99
|
}
|
|
@@ -615,9 +616,14 @@ export function checkInputSources(
|
|
|
615
616
|
}
|
|
616
617
|
|
|
617
618
|
/** Enumerate structurally available outputs while retaining their compatibility with one input. */
|
|
618
|
-
export function inputSourceCandidates(
|
|
619
|
+
export function inputSourceCandidates(
|
|
620
|
+
graph: Graph,
|
|
621
|
+
target: string,
|
|
622
|
+
handle: string,
|
|
623
|
+
inputPort?: InputPort,
|
|
624
|
+
): Readonly<Record<string, readonly InputSourceCandidate[]>> {
|
|
619
625
|
const targetNode = graph.nodes[target]
|
|
620
|
-
const input = targetNode == null ? undefined : nodeInputPorts(targetNode)[handle]
|
|
626
|
+
const input = inputPort ?? (targetNode == null ? undefined : nodeInputPorts(targetNode)[handle])
|
|
621
627
|
if (input == null) return {}
|
|
622
628
|
const analysis = graphPaths(graph)
|
|
623
629
|
return Object.fromEntries(
|
package/src/flow/common/retry.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface RetryConfiguration {
|
|
|
7
7
|
readonly intervalMs: number
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const retryNodeTypes = ['code', 'connector', 'openapi', 'llm', 'decision'] as const
|
|
10
|
+
export const retryNodeTypes = ['code', 'connector', 'openapi', 'http-request', 'llm', 'decision'] as const
|
|
11
11
|
export const nodeRetryLimits = { maxRetries: 5, minIntervalMs: 1000, maxIntervalMs: 60000, intervalStepMs: 1000 } as const
|
|
12
12
|
export const defaultNodeRetry: RetryConfiguration = { maxRetries: 2, intervalMs: 2000 }
|
|
13
13
|
export const retryConfigurationSchema = z.strictObject({
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { dequal } from 'dequal/lite'
|
|
1
2
|
import { decisionTaskIssues } from '../../decision/common/decision.ts'
|
|
3
|
+
import { httpRequestIssues, httpAuthHandles, httpRequestTask, httpRequestUrl } from '../../http/common/httpRequest.ts'
|
|
2
4
|
import { openApiIssues, authHandles } from '../../openapi/common/openapi.ts'
|
|
3
5
|
import { nodeWorkflowReferences } from '../../workflow/common/workflow.ts'
|
|
4
6
|
import { isTriggerNode } from './change.ts'
|
|
@@ -271,13 +273,57 @@ export async function validateFlow(revision: RevisionContent, engine: EngineCont
|
|
|
271
273
|
})
|
|
272
274
|
for (const message of openApiIssues(task))
|
|
273
275
|
checked.diagnostics.push({ code: 'openapi.config-invalid', column: 0, line: 1, message, path: `/document/graph/nodes/${nodeId}/task/executor` })
|
|
274
|
-
if (task.executor.kind == '
|
|
275
|
-
const
|
|
276
|
+
if (task.executor.kind == 'http-request') {
|
|
277
|
+
for (const issue of httpRequestIssues(task.executor))
|
|
278
|
+
checked.diagnostics.push({
|
|
279
|
+
code: 'http-request.config-invalid',
|
|
280
|
+
column: 0,
|
|
281
|
+
line: 1,
|
|
282
|
+
message: issue.message,
|
|
283
|
+
path: `/document/graph/nodes/${nodeId}/task/executor/${issue.field}`,
|
|
284
|
+
})
|
|
285
|
+
if (node.inputs.url?.kind == 'value') {
|
|
286
|
+
try {
|
|
287
|
+
httpRequestUrl(node.inputs.url.value)
|
|
288
|
+
} catch {
|
|
289
|
+
checked.diagnostics.push({
|
|
290
|
+
code: 'http-request.url-invalid',
|
|
291
|
+
column: 0,
|
|
292
|
+
line: 1,
|
|
293
|
+
message: 'Enter an HTTP(S) URL without embedded credentials.',
|
|
294
|
+
path: `/document/graph/nodes/${nodeId}/inputs/url`,
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (task.executor.bodyFormat != 'none' && (node.inputs.body == null || node.inputs.body.kind == 'unset'))
|
|
299
|
+
checked.diagnostics.push({
|
|
300
|
+
code: 'http-request.body-required',
|
|
301
|
+
column: 0,
|
|
302
|
+
line: 1,
|
|
303
|
+
message: 'Set or bind the request body; JSON null is an explicit value.',
|
|
304
|
+
path: `/document/graph/nodes/${nodeId}/inputs/body`,
|
|
305
|
+
})
|
|
306
|
+
const expected = httpRequestTask(task.executor)
|
|
307
|
+
|
|
308
|
+
if (
|
|
309
|
+
!dequal(httpPortContracts(task.inputs), httpPortContracts(expected.inputs)) ||
|
|
310
|
+
!dequal(httpPortContracts(task.outputs), httpPortContracts(expected.outputs))
|
|
311
|
+
)
|
|
312
|
+
checked.diagnostics.push({
|
|
313
|
+
code: 'http-request.ports-invalid',
|
|
314
|
+
column: 0,
|
|
315
|
+
line: 1,
|
|
316
|
+
message: 'HTTP Request ports must match its configuration.',
|
|
317
|
+
path: `/document/graph/nodes/${nodeId}/task`,
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
if (task.executor.kind == 'openapi' || task.executor.kind == 'http-request') {
|
|
321
|
+
const handles = task.executor.kind == 'openapi' ? authHandles(task.executor.auth) : httpAuthHandles(task.executor)
|
|
276
322
|
for (const handle of handles) {
|
|
277
323
|
const mapping = node.inputs[handle]
|
|
278
324
|
if (mapping?.kind != 'sources' || mapping.sources.length == 0)
|
|
279
325
|
checked.diagnostics.push({
|
|
280
|
-
code:
|
|
326
|
+
code: `${task.executor.kind}.auth-source`,
|
|
281
327
|
column: 0,
|
|
282
328
|
line: 1,
|
|
283
329
|
message: 'Authentication requires a deployment variable or upstream output.',
|
|
@@ -385,9 +431,20 @@ function runRevision(revision: RevisionContent, triggerId: string): RevisionCont
|
|
|
385
431
|
}
|
|
386
432
|
}
|
|
387
433
|
|
|
434
|
+
const httpPortContracts = (ports: import('./change.ts').ManagedTaskDefinition['inputs']) =>
|
|
435
|
+
ports
|
|
436
|
+
.filter((port) => 'handle' in port)
|
|
437
|
+
.map(({ handle, jsonSchema, nullable }) => ({ handle, jsonSchema, nullable }))
|
|
438
|
+
.toSorted((a, b) => a.handle.localeCompare(b.handle))
|
|
439
|
+
|
|
388
440
|
export async function prepareFlow(revision: RevisionContent, engineContract: string, triggerId?: string): Promise<PrepareFlowResult> {
|
|
389
441
|
const engine = findEngineContract(engineContract)
|
|
390
442
|
if (engine == null) return { kind: 'engine-unsupported' }
|
|
443
|
+
if (
|
|
444
|
+
!engine.httpRequests &&
|
|
445
|
+
Object.values(revision.document.graph.nodes).some((node) => node.kind == 'task' && 'executor' in node.task && node.task.executor.kind == 'http-request')
|
|
446
|
+
)
|
|
447
|
+
return { kind: 'engine-unsupported' }
|
|
391
448
|
if (!engine.workflowTools && Object.values(revision.document.graph.nodes).some((node) => nodeWorkflowReferences(node).some((ref) => ref.tool != null)))
|
|
392
449
|
return { kind: 'engine-unsupported' }
|
|
393
450
|
if (triggerId != null) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { InputPort, JsonValue, Port, PortDefinition } from './change.ts'
|
|
2
2
|
|
|
3
|
+
import { httpHeaderNameIssue } from '../../http/common/headers.ts'
|
|
4
|
+
|
|
3
5
|
export const webhookResponseTimeout = {
|
|
4
6
|
defaultMs: 30_000,
|
|
5
7
|
minMs: 1000,
|
|
@@ -30,12 +32,6 @@ export function workflowOutputsPort(outputs: readonly Port[]): PortDefinition {
|
|
|
30
32
|
} as const
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
/** Header names are case-insensitive; never silently combine duplicate business fields. */
|
|
34
|
-
export function webhookHeaderNameIssue(name: string, names: readonly string[]): 'invalid' | 'duplicate' | undefined {
|
|
35
|
-
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) return 'invalid'
|
|
36
|
-
if (names.some((candidate) => candidate.toLowerCase() === name.toLowerCase())) return 'duplicate'
|
|
37
|
-
}
|
|
38
|
-
|
|
39
35
|
/** Fixed inputs belong to the node, not author-supplied definitions. */
|
|
40
36
|
export const webhookResponseInputs: readonly InputPort[] = [
|
|
41
37
|
{ handle: 'body', jsonSchema: {}, nullable: true, value: '' },
|
|
@@ -60,7 +56,7 @@ export function webhookHeaderValues(source: unknown): Readonly<Record<string, st
|
|
|
60
56
|
const headers: Record<string, string> = {}
|
|
61
57
|
const names: string[] = []
|
|
62
58
|
for (const [name, value] of Object.entries(source)) {
|
|
63
|
-
if (
|
|
59
|
+
if (httpHeaderNameIssue(name, names) != null) return
|
|
64
60
|
if (typeof value !== 'string' && typeof value !== 'boolean' && !(typeof value === 'number' && Number.isFinite(value))) return
|
|
65
61
|
const text = String(value)
|
|
66
62
|
if ([...text].some((character) => character.charCodeAt(0) > 255 || /[\0\r\n]/.test(character))) return
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { FlowDocument } from '../../flow/common/change.ts'
|
|
2
2
|
|
|
3
3
|
/** Never allow credentials to enter a Revision as literal node inputs. */
|
|
4
|
-
export function
|
|
4
|
+
export function assertHttpAuthBindings(document: FlowDocument): void {
|
|
5
5
|
for (const node of Object.values(document.graph.nodes)) {
|
|
6
|
-
if (node.kind != 'task' || !('executor' in node.task) || node.task.executor.kind
|
|
6
|
+
if (node.kind != 'task' || !('executor' in node.task) || !['openapi', 'http-request'].includes(node.task.executor.kind)) continue
|
|
7
7
|
for (const [handle, mapping] of Object.entries(node.inputs)) {
|
|
8
8
|
if (handle.startsWith('auth.') && mapping.kind == 'value')
|
|
9
|
-
throw new TypeError('
|
|
9
|
+
throw new TypeError('HTTP authentication accepts deployment variables and upstream outputs only.')
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Header names are case-insensitive; never silently combine duplicate business fields. */
|
|
2
|
+
export function httpHeaderNameIssue(name: string, names: readonly string[]): 'invalid' | 'duplicate' | undefined {
|
|
3
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(name)) return 'invalid'
|
|
4
|
+
if (names.some((candidate) => candidate.toLowerCase() === name.toLowerCase())) return 'duplicate'
|
|
5
|
+
}
|