@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.
Files changed (60) hide show
  1. package/README.md +1 -0
  2. package/dist/browser/{ContentIcon-CUQHjQIw.js → ContentIcon-FCpKiwjp.js} +2 -1
  3. package/dist/browser/{FlowCanvas-Dtf8_cQp.js → FlowCanvas-dzRBnlUT.js} +1 -1
  4. package/dist/browser/{createResourceDialog-DI9pL-Yr.js → createResourceDialog-BeYDdUl-.js} +2 -2
  5. package/dist/browser/{dialog-BYJGvItc.js → dialog-ZtpsFbl4.js} +284 -13
  6. package/dist/browser/{exportSvg-BOoy7-Db.js → exportSvg-DqXw1yf-.js} +2 -2
  7. package/dist/browser/{flowChanges-RL-H4CjD.js → flowChanges-b_5UobFj.js} +285 -44
  8. package/dist/browser/{flowWorkspace-BkDsETj0.js → flowWorkspace-ItojZbO4.js} +414 -53
  9. package/dist/browser/preview.css +181 -176
  10. package/dist/browser/preview.js +955 -182
  11. package/dist/browser/workbench.css +181 -176
  12. package/dist/browser/workbench.js +12 -6
  13. package/dist/browser/{workbenchSelect-BcWPWGzw.js → workbenchSelect-M441Lz0d.js} +1 -1
  14. package/dist/command/index.js +36 -36
  15. package/dist/common/authoring.d.ts +4 -4
  16. package/dist/common/authoringExamples.d.ts +3 -2
  17. package/dist/common/authoringSchema.d.ts +67 -4
  18. package/dist/common/control-api-conformance.js +58 -7
  19. package/dist/common/control-api.js +58 -7
  20. package/dist/common/control-requests.d.ts +3 -3
  21. package/dist/common/control-requests.js +234 -62
  22. package/dist/common/engine-contract.d.ts +1 -0
  23. package/dist/common/flow-authoring.js +56 -7
  24. package/dist/common/flow-change.d.ts +1 -1
  25. package/dist/common/flow-change.js +56 -7
  26. package/dist/common/flow-encoding.js +55 -7
  27. package/dist/common/flow-graph.d.ts +2 -1
  28. package/dist/common/flow-retry.d.ts +1 -1
  29. package/dist/common/flow-semantics.js +810 -577
  30. package/dist/common/http-request.d.ts +156 -0
  31. package/dist/common/http-request.js +5254 -0
  32. package/dist/common/mcp.d.ts +3 -3
  33. package/dist/common/mcp.js +58 -7
  34. package/dist/common/run-events.js +11 -9
  35. package/dist/common/runtime-contract.js +3 -0
  36. package/dist/common/scheduler.d.ts +1 -1
  37. package/dist/common/scheduler.js +70 -21
  38. package/dist/common/webhook-response.d.ts +0 -2
  39. package/dist/common/webhook-trigger.js +12 -11
  40. package/package.json +5 -1
  41. package/skills/open-flow/references/flow-authoring.md +1 -0
  42. package/skills/open-flow/references/flow-n8n-conversion.md +19 -15
  43. package/src/control/common/api.ts +1 -0
  44. package/src/control/common/authoring.ts +14 -3
  45. package/src/control/common/authoringExamples.ts +8 -0
  46. package/src/control/common/authoringSchema.ts +30 -24
  47. package/src/execution/common/engineContract.ts +4 -0
  48. package/src/execution/common/events.ts +3 -0
  49. package/src/execution/common/node-execution.ts +4 -2
  50. package/src/execution/common/scheduler.ts +1 -0
  51. package/src/flow/common/change.ts +3 -2
  52. package/src/flow/common/changeSchema.ts +12 -3
  53. package/src/flow/common/graph.ts +10 -4
  54. package/src/flow/common/retry.ts +1 -1
  55. package/src/flow/common/semantics.ts +60 -3
  56. package/src/flow/common/webhookResponse.ts +3 -7
  57. package/src/{openapi → http}/common/authBindings.ts +3 -3
  58. package/src/http/common/headers.ts +5 -0
  59. package/src/http/common/httpRequest.ts +251 -0
  60. 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 == 'openapi'
300
- ? '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.'
301
- : kind == 'workflow'
302
- ? '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.'
303
- : kind == 'agent'
304
- ? '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.'
305
- : kind == 'code'
306
- ? '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.'
307
- : 'Supply business configuration and input data. Fixed definitions are assembled by the tool. Existing constraints and defaults are preserved on partial updates.',
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
- openapi: { config: { sourceUrl: 'https://example.com/openapi.json', path: '/items', method: 'get' } },
322
- code: { code: 'export default ({value}) => ({result:value})', inputs: { value: { kind: 'value', value: 'Hello' } } },
323
- condition: {
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?.executor.kind == 'openapi' && handle.startsWith('auth.')) return false
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?.executor.kind != 'openapi') return true
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' &&
@@ -69,6 +69,7 @@ export type SchedulerEvent =
69
69
  | 'decision'
70
70
  | 'javascript'
71
71
  | 'openapi'
72
+ | 'http-request'
72
73
  | 'llm'
73
74
  | 'value'
74
75
  | 'wait'
@@ -1,6 +1,6 @@
1
1
  import type { JsonValue, Port, PortDefinition } from '../../types/index.ts'
2
2
 
3
- import { assertOpenApiAuthBindings } from '../../openapi/common/authBindings.ts'
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
- assertOpenApiAuthBindings(document)
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 { assertOpenApiAuthBindings } from '../../openapi/common/authBindings.ts'
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
- assertOpenApiAuthBindings(content.document)
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
- assertOpenApiAuthBindings(content.document)
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(
@@ -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 (webhookHeaderNameIssue(name, names) != null)
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(graph: Graph, target: string, handle: string): Readonly<Record<string, readonly InputSourceCandidate[]>> {
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(
@@ -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 == 'openapi') {
275
- const handles = authHandles(task.executor.auth)
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: 'openapi.auth-source',
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 (webhookHeaderNameIssue(name, names) != null) return
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 assertOpenApiAuthBindings(document: FlowDocument): void {
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 != 'openapi') continue
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('OpenAPI authentication accepts deployment variables and upstream outputs only.')
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
+ }