@oomol-lab/open-flow 0.1.0-beta.84 → 0.1.0-beta.86

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 (97) hide show
  1. package/dist/browser/{FlowCanvas-B2g7NceZ.js → FlowCanvas-CHIaMaBv.js} +112 -16
  2. package/dist/browser/{createResourceDialog-Bi4fUnbD.js → createResourceDialog-C0E5yPe6.js} +3 -3
  3. package/dist/browser/{dialog-BzoSxhXQ.js → dialog-b3hsdu8i.js} +291 -143
  4. package/dist/browser/{exportPng-DLdtJvg3.js → exportPng-DGHcejgy.js} +2 -2
  5. package/dist/browser/{flowChanges-C567g-Pc.js → flowChanges-BvSWezEb.js} +375 -83
  6. package/dist/browser/{flowWorkspace-DBk4A-Ld.js → flowWorkspace-BKnMRClO.js} +1619 -475
  7. package/dist/browser/fr-CNTSedYU.js +593 -0
  8. package/dist/browser/ja-CxQBHr2J.js +593 -0
  9. package/dist/browser/ko-D1Exiv1h.js +593 -0
  10. package/dist/browser/licenses.md +1 -1
  11. package/dist/browser/{markdownContent-DIuJThGK.js → markdownContent-BR9-1ikU.js} +1 -1
  12. package/dist/browser/preview.css +181 -181
  13. package/dist/browser/preview.js +2103 -636
  14. package/dist/browser/ru-BzLZv0B7.js +593 -0
  15. package/dist/browser/{tooltip-DGj5pudg.js → tooltip-DSBF3bRw.js} +55 -23
  16. package/dist/browser/workbench.css +181 -181
  17. package/dist/browser/workbench.js +86 -17
  18. package/dist/browser/{workbenchSelect-BIaEVTx6.js → workbenchSelect-Di1Bm_-d.js} +2 -2
  19. package/dist/browser/{wrapLayout-C4moMKgb.js → wrapLayout-B0AT8_-i.js} +1 -1
  20. package/dist/browser/zh-CN-Dg_xSGde.js +593 -0
  21. package/dist/browser/zh-TW-oxuLwsV0.js +593 -0
  22. package/dist/command/index.js +61 -57
  23. package/dist/command/licenses.md +28 -4
  24. package/dist/common/authoring.d.ts +5 -5
  25. package/dist/common/authoringExamples.d.ts +3 -2
  26. package/dist/common/authoringSchema.d.ts +41 -4
  27. package/dist/common/catalogCache.d.ts +46 -0
  28. package/dist/common/connector-action.js +13 -10
  29. package/dist/common/connector-proxy.d.ts +96 -0
  30. package/dist/common/connector-proxy.js +5209 -0
  31. package/dist/common/control-api-conformance.js +312 -46
  32. package/dist/common/control-api.d.ts +12 -1
  33. package/dist/common/control-api.js +303 -41
  34. package/dist/common/control-requests.d.ts +4 -3
  35. package/dist/common/control-requests.js +14111 -171
  36. package/dist/common/engine-contract.d.ts +3 -2
  37. package/dist/common/event-source.js +55 -23
  38. package/dist/common/flow-authoring-node.d.ts +2 -0
  39. package/dist/common/flow-authoring.js +6936 -6781
  40. package/dist/common/flow-change-schema.d.ts +1 -1
  41. package/dist/common/flow-change.d.ts +2 -2
  42. package/dist/common/flow-change.js +91 -36
  43. package/dist/common/flow-connectionUsage.d.ts +1 -1
  44. package/dist/common/flow-encoding.js +93 -38
  45. package/dist/common/flow-requestParameters.d.ts +34 -0
  46. package/dist/common/flow-semantics.js +9429 -9236
  47. package/dist/common/flow-state.js +55 -23
  48. package/dist/common/flow-workflowFile.d.ts +3 -1
  49. package/dist/common/http-request.d.ts +6 -11
  50. package/dist/common/http-request.js +192 -141
  51. package/dist/common/licenses.md +1 -1
  52. package/dist/common/mcp.d.ts +3 -3
  53. package/dist/common/mcp.js +303 -41
  54. package/dist/common/run-events.js +1 -0
  55. package/dist/common/runtime-contract.js +10 -4
  56. package/dist/common/runtime-nodejs-compat.js +55 -23
  57. package/dist/common/scheduler-checkpoint.d.ts +3 -3
  58. package/dist/common/scheduler-dependencies.d.ts +22 -28
  59. package/dist/common/scheduler-node-execution.d.ts +1 -0
  60. package/dist/common/scheduler.d.ts +8 -2
  61. package/dist/common/scheduler.js +274 -95
  62. package/dist/common/workflow.js +51 -19
  63. package/dist/common/workflowAuthoring.d.ts +411 -0
  64. package/dist/render/browser/licenses.md +1 -1
  65. package/dist/render/browser/runtime.js +289 -61
  66. package/dist/render/node/index.js +85 -30
  67. package/dist/render/node/licenses.md +1 -1
  68. package/package.json +1 -1
  69. package/skills/open-flow/references/flow-authoring.md +14 -13
  70. package/src/base/common/jsonSchema.ts +11 -0
  71. package/src/connector/common/actionSchema.ts +4 -11
  72. package/src/connector/common/proxy.ts +152 -0
  73. package/src/control/common/api.ts +99 -3
  74. package/src/control/common/authoring.ts +23 -9
  75. package/src/control/common/authoringExamples.ts +9 -0
  76. package/src/control/common/authoringSchema.ts +21 -12
  77. package/src/control/common/catalogCache.ts +145 -0
  78. package/src/control/common/conformance.ts +9 -3
  79. package/src/control/common/connectorQueries.ts +17 -2
  80. package/src/control/common/requests.ts +1 -0
  81. package/src/control/common/workflowAuthoring.ts +120 -0
  82. package/src/execution/common/checkpoint.ts +18 -12
  83. package/src/execution/common/dependencies.ts +132 -15
  84. package/src/execution/common/events.ts +3 -0
  85. package/src/execution/common/node-execution.ts +22 -15
  86. package/src/execution/common/scheduler.ts +20 -31
  87. package/src/flow/common/change.ts +2 -1
  88. package/src/flow/common/changeSchema.ts +11 -1
  89. package/src/flow/common/connectionUsage.ts +4 -2
  90. package/src/flow/common/errorHandling.ts +3 -1
  91. package/src/flow/common/nodeChanges.ts +35 -18
  92. package/src/flow/common/requestParameters.ts +120 -0
  93. package/src/flow/common/semantics.ts +51 -1
  94. package/src/flow/common/sourceField.ts +3 -2
  95. package/src/flow/common/workflowFile.ts +2 -2
  96. package/src/http/common/httpRequest.ts +28 -78
  97. package/src/runtime/common/engineContract.ts +6 -3
@@ -166,6 +166,7 @@ export interface ConditionNode extends GraphNodeBase {
166
166
  }
167
167
 
168
168
  export type ManagedTaskExecutor =
169
+ | import('../../connector/common/proxy.ts').ConnectorProxyExecutor
169
170
  | import('../../http/common/httpRequest.ts').HttpRequestExecutor
170
171
  | import('../../state/common/state.ts').SkipDuplicatesExecutor
171
172
  | WorkflowExecutor
@@ -398,7 +399,7 @@ export interface FlowDocument {
398
399
 
399
400
  export interface RevisionContent {
400
401
  readonly document: FlowDocument
401
- readonly modelVersion: 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | typeof currentFlowModelVersion
402
+ readonly modelVersion: 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | typeof currentFlowModelVersion
402
403
  readonly modules: Readonly<Record<string, CodeModule>>
403
404
  }
404
405
 
@@ -2,6 +2,7 @@ import type { ChangeOperation, FlowDocument, JsonValue, RevisionContent } from '
2
2
 
3
3
  import { dequal } from 'dequal/lite'
4
4
  import { z } from 'zod'
5
+ import { connectorProxyExecutorSchema } from '../../connector/common/proxy.ts'
5
6
  import { limitDecisionTask } from '../../decision/common/decision.ts'
6
7
  import { assertHttpAuthBindings } from '../../http/common/authBindings.ts'
7
8
  import { isHttpHost, maxHttpHosts } from '../../http/common/hosts.ts'
@@ -14,7 +15,7 @@ import { waitAutoContinueDraftSchema, waitAutoContinueSchema } from './wait.ts'
14
15
  import { webhookMethods } from './webhookMethod.ts'
15
16
  import { webhookResponseTimeout } from './webhookResponse.ts'
16
17
 
17
- export const currentFlowModelVersion = 13
18
+ export const currentFlowModelVersion = 14
18
19
  const text = z.string()
19
20
  const actionConnection = text.describe(
20
21
  'Account selection: omit while unconfigured; use an account ID to select it; use "" to explicitly defer configuration. Deferred Actions fail if called; deferred Agent tools are excluded from the callable tool set. Not supported by Triggers.',
@@ -131,6 +132,7 @@ const managedFields = z.object({
131
132
  z.strictObject({ kind: z.literal('workflow'), flowId: text, revisionId: text }),
132
133
  httpRequestExecutorSchema,
133
134
  skipDuplicatesExecutorSchema,
135
+ connectorProxyExecutorSchema,
134
136
  ]),
135
137
  })
136
138
 
@@ -321,6 +323,7 @@ const revision = z.object({
321
323
  z.literal(10),
322
324
  z.literal(11),
323
325
  z.literal(12),
326
+ z.literal(13),
324
327
  z.literal(currentFlowModelVersion),
325
328
  ]),
326
329
  document,
@@ -726,6 +729,13 @@ export function changeOperationsSchema(kind?: string): JsonValue {
726
729
  }
727
730
 
728
731
  function checkModelVersion(content: RevisionContent): void {
732
+ if (
733
+ content.modelVersion < 14 &&
734
+ Object.values(content.document.graph.nodes).some(
735
+ (candidateNode) => candidateNode.kind === 'task' && 'executor' in candidateNode.task && candidateNode.task.executor.kind === 'connector-proxy',
736
+ )
737
+ )
738
+ throw new TypeError('Custom API Request requires Flow model v14.')
729
739
  if (
730
740
  content.modelVersion < 13 &&
731
741
  Object.values(content.document.graph.nodes).some(
@@ -8,7 +8,7 @@ export interface ConnectionUsage {
8
8
  readonly nodeId: string
9
9
  readonly name: string
10
10
 
11
- readonly kind: 'connector' | 'agent' | 'trigger' | 'notification' | 'code'
11
+ readonly kind: 'connector' | 'connector-proxy' | 'agent' | 'trigger' | 'notification' | 'code'
12
12
  }
13
13
 
14
14
  export function connectionUsage(document: FlowDocument): readonly ConnectionUsage[] {
@@ -31,6 +31,8 @@ export function connectionUsage(document: FlowDocument): readonly ConnectionUsag
31
31
  })
32
32
  }
33
33
  if (task?.executor.kind == 'connector') add('connector', task.executor.action, task.executor.connectionId)
34
+ if (task?.executor.kind == 'connector-proxy')
35
+ uses.push({ kind: 'connector-proxy', providerId: task.executor.providerId, connectionId: task.executor.connectionId, nodeId, name })
34
36
  if (node.kind == 'task' && 'moduleId' in node.task) {
35
37
  for (const capability of node.task.capabilities ?? []) {
36
38
  if ('mode' in capability && capability.mode == 'independent') for (const action of capability.actions) add('code', action.action, action.connectionId)
@@ -74,7 +76,7 @@ export function clearNodeConnections(graph: FlowDocument['graph'], connectionId?
74
76
  const { connectionId: _, ...rest } = value
75
77
  return rest as T
76
78
  }
77
- if (executor.kind == 'connector') return [id, { ...node, task: { ...node.task, executor: clear(executor) } }]
79
+ if (executor.kind == 'connector' || executor.kind == 'connector-proxy') return [id, { ...node, task: { ...node.task, executor: clear(executor) } }]
78
80
  if (executor.kind == 'agent')
79
81
  return [
80
82
  id,
@@ -26,7 +26,9 @@ export const nodeErrorSchema = {
26
26
  } as const
27
27
 
28
28
  export function supportsNodeErrorHandling(node: GraphNode): boolean {
29
- return supportsNodeRetry(node) || (node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'skip-duplicates')
29
+ return (
30
+ supportsNodeRetry(node) || (node.kind === 'task' && 'executor' in node.task && ['skip-duplicates', 'connector-proxy'].includes(node.task.executor.kind))
31
+ )
30
32
  }
31
33
 
32
34
  export function nodeSystemOutputPorts(node: GraphNode): Readonly<Record<string, PortDefinition>> {
@@ -22,6 +22,7 @@ import type {
22
22
  } from './change.ts'
23
23
 
24
24
  import { dequal } from 'dequal/lite'
25
+ import { connectorProxyHasBody, connectorProxyTask } from '../../connector/common/proxy.ts'
25
26
  import { decisionTask } from '../../decision/common/decision.ts'
26
27
  import { isTriggerNode, applyFlowChanges, nextNodeName, normalizeNodeName } from './change.ts'
27
28
  import { codeSharedPermissionsEnabled } from './codePermissions.ts'
@@ -401,13 +402,37 @@ export function setInputValues(
401
402
  content: RevisionContent,
402
403
  nodeId: string,
403
404
  values: Readonly<Record<string, JsonValue | undefined>>,
405
+ ): readonly ChangeOperation[] | undefined {
406
+ return setInputMappings(content, nodeId, Object.fromEntries(Object.entries(values).map(([handle, value]) => [handle, fixedInputValue(value)])))
407
+ }
408
+
409
+ /** Input sources own whether a Provider request includes a body and its generated port. */
410
+ export function setInputMappings(
411
+ content: RevisionContent,
412
+ nodeId: string,
413
+ mappings: Readonly<Record<string, InputMapping | undefined>>,
404
414
  ): readonly ChangeOperation[] | undefined {
405
415
  const node = content.document.graph?.nodes[nodeId]
406
416
  if (node == null || isTriggerNode(node)) return
407
417
  const operations: ChangeOperation[] = []
408
- for (const [handle, value] of Object.entries(values)) {
418
+ const task = node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'connector-proxy' ? node.task : undefined
419
+ let bodyUndeclared = false
420
+ if (task?.executor.kind === 'connector-proxy' && Object.hasOwn(mappings, 'body')) {
421
+ const port = task.inputs.find((input): input is InputPort => 'handle' in input && input.handle === 'body')
422
+ const hasBody = connectorProxyHasBody(mappings.body, port?.value)
423
+ bodyUndeclared = !hasBody && task.executor.bodyFormat === 'none'
424
+ if (hasBody && task.executor.bodyFormat === 'none') {
425
+ operations.push({
426
+ kind: 'graph.node.task.set',
427
+ nodeId,
428
+ before: task,
429
+ value: connectorProxyTask({ ...task.executor, bodyFormat: 'json' }, task.name, task),
430
+ })
431
+ }
432
+ }
433
+ for (const [handle, value] of Object.entries(mappings)) {
409
434
  const before = nodeInputMappings(node)[handle]
410
- const next = fixedInputValue(value)
435
+ const next = handle === 'body' && bodyUndeclared ? undefined : value
411
436
  if (dequal(before, next)) continue
412
437
  operations.push({ before, handle, kind: 'graph.node.input.set', nodeId, value: next })
413
438
  }
@@ -418,13 +443,7 @@ export function setInputValues(
418
443
  export function resetInputValues(content: RevisionContent, nodeId: string, handles: readonly string[]): readonly ChangeOperation[] {
419
444
  const node = content.document.graph?.nodes[nodeId]
420
445
  if (node == null || isTriggerNode(node)) return []
421
- const mappings = nodeInputMappings(node)
422
- return cleanVariableBindings(
423
- content,
424
- [...new Set(handles)].flatMap((handle): ChangeOperation[] =>
425
- mappings[handle] == null ? [] : [{ kind: 'graph.node.input.set', nodeId, handle, before: mappings[handle], value: undefined }],
426
- ),
427
- )
446
+ return setInputMappings(content, nodeId, Object.fromEntries(handles.map((handle) => [handle, undefined]))) ?? []
428
447
  }
429
448
 
430
449
  export function setInputSources(
@@ -435,9 +454,7 @@ export function setInputSources(
435
454
  ): readonly ChangeOperation[] {
436
455
  const node = content.document.graph?.nodes[nodeId]
437
456
  if (node == null || isTriggerNode(node)) return []
438
- return cleanVariableBindings(content, [
439
- { kind: 'graph.node.input.set', nodeId, handle, before: nodeInputMappings(node)[handle], value: { kind: 'sources', sources } },
440
- ])
457
+ return setInputMappings(content, nodeId, { [handle]: { kind: 'sources', sources } }) ?? []
441
458
  }
442
459
 
443
460
  export function setInputVariable(
@@ -459,10 +476,7 @@ export function setInputVariable(
459
476
  return [{ before: current.target, bindingId: currentId, kind: 'binding.target.set', value: name }]
460
477
  }
461
478
  const next: InputMapping = { kind: 'sources', sources: [{ bindingId, kind: 'binding' }] }
462
- return [
463
- { binding: { kind: 'variable', target: name }, bindingId, kind: 'binding.create' },
464
- ...cleanVariableBindings(content, [{ before: mapping, handle, kind: 'graph.node.input.set', nodeId, value: next }]),
465
- ]
479
+ return [{ binding: { kind: 'variable', target: name }, bindingId, kind: 'binding.create' }, ...(setInputMappings(content, nodeId, { [handle]: next }) ?? [])]
466
480
  }
467
481
 
468
482
  export function cleanVariableBindings(content: RevisionContent, operations: readonly ChangeOperation[]): readonly ChangeOperation[] {
@@ -495,9 +509,9 @@ function bindingReferences(document: RevisionContent['document']): Map<string, n
495
509
 
496
510
  export function setConnectorConnection(content: RevisionContent, nodeId: string, connectionId: string | undefined): readonly ChangeOperation[] | undefined {
497
511
  const node = content.document.graph.nodes[nodeId]
498
- if (node?.kind != 'task' || !('executor' in node.task) || node.task.executor.kind != 'connector') return
512
+ if (node?.kind != 'task' || !('executor' in node.task) || (node.task.executor.kind != 'connector' && node.task.executor.kind != 'connector-proxy')) return
499
513
  const task = node.task
500
- if (task.executor.kind != 'connector') return
514
+ if (task.executor.kind != 'connector' && task.executor.kind != 'connector-proxy') return
501
515
  if (task.executor.connectionId == connectionId) return []
502
516
  const { connectionId: _, ...executor } = task.executor
503
517
  return [
@@ -542,6 +556,9 @@ export function setServiceConnections(
542
556
  if (capabilities != null) operations.push(...(setCodeActions(content, nodeId, capabilities) ?? []))
543
557
  } else if (task.executor.kind == 'connector') {
544
558
  if (assign(task.executor) !== task.executor) operations.push(...(setConnectorConnection(content, nodeId, connectionId) ?? []))
559
+ } else if (task.executor.kind === 'connector-proxy') {
560
+ if (task.executor.providerId === providerId && task.executor.connectionId === previousConnectionId)
561
+ operations.push(...(setConnectorConnection(content, nodeId, connectionId) ?? []))
545
562
  } else if (task.executor.kind == 'agent') {
546
563
  const executor = {
547
564
  ...task.executor,
@@ -0,0 +1,120 @@
1
+ import type { InputMapping, InputPort, JsonValue, TaskDefinitionBase, TaskExecutor } from './model.ts'
2
+
3
+ import { dequal } from 'dequal/lite'
4
+ import { z } from 'zod'
5
+ import { httpHeaderNameIssue } from '../../http/common/headers.ts'
6
+ import { remapInputMappings, renamedPort } from './portChanges.ts'
7
+
8
+ export const requestParameterSchema = z.strictObject({ mode: z.enum(['fields', 'object']).default('fields'), names: z.array(z.string()).default([]) })
9
+ export type RequestParameters = z.infer<typeof requestParameterSchema>
10
+ export interface RequestParameterConfig {
11
+ readonly query: RequestParameters
12
+ readonly headers: RequestParameters
13
+ }
14
+ export interface RequestTaskNode {
15
+ readonly inputs: Readonly<Record<string, InputMapping>>
16
+ readonly task: TaskDefinitionBase & { readonly executor?: TaskExecutor }
17
+ }
18
+
19
+ export function requestParameterHandle(group: 'query' | 'headers', name: string): string {
20
+ return `${group === 'query' ? 'query' : 'header'}.${name}`
21
+ }
22
+
23
+ export function requestParameterPorts(
24
+ config: RequestParameterConfig,
25
+ querySchema: JsonValue,
26
+ queryValueSchema: JsonValue,
27
+ headerSchema: JsonValue,
28
+ ): InputPort[] {
29
+ return (['query', 'headers'] as const).flatMap((group): InputPort[] =>
30
+ config[group].mode === 'object'
31
+ ? [{ handle: group, jsonSchema: group === 'query' ? querySchema : headerSchema, nullable: true }]
32
+ : config[group].names
33
+ .filter((name) => name.trim())
34
+ .map((name) => ({
35
+ handle: requestParameterHandle(group, name),
36
+ jsonSchema: group === 'query' ? queryValueSchema : { type: 'string' },
37
+ nullable: true,
38
+ })),
39
+ )
40
+ }
41
+
42
+ export function retainRequestPorts(ports: readonly InputPort[], old: TaskDefinitionBase['inputs'] = []): TaskDefinitionBase['inputs'] {
43
+ const remaining = new Map(ports.map((port) => [port.handle, port]))
44
+ return [
45
+ ...old.flatMap((item): TaskDefinitionBase['inputs'] => {
46
+ if ('group' in item) return [item]
47
+ const next = remaining.get(item.handle)
48
+ if (next == null) return []
49
+ remaining.delete(item.handle)
50
+ return [dequal(item.jsonSchema, next.jsonSchema) && item.nullable == next.nullable ? item : next]
51
+ }),
52
+ ...remaining.values(),
53
+ ]
54
+ }
55
+
56
+ export interface RequestParameterIssue {
57
+ readonly field: 'query' | 'headers'
58
+ readonly index: number
59
+ readonly code: 'parameterNamesInvalid' | 'headerNameInvalid'
60
+ readonly message: string
61
+ }
62
+
63
+ export function requestParameterIssues(config: RequestParameterConfig): RequestParameterIssue[] {
64
+ const issues: RequestParameterIssue[] = []
65
+ for (const group of ['query', 'headers'] as const) {
66
+ if (config[group].mode !== 'fields') continue
67
+ const seen = new Set<string>()
68
+ for (const [index, name] of config[group].names.entries()) {
69
+ const key = group === 'headers' ? name.toLowerCase() : name
70
+ if (!name.trim() || seen.has(key))
71
+ issues.push({ field: group, index, code: 'parameterNamesInvalid', message: 'Parameter names must be nonempty and unique.' })
72
+ seen.add(key)
73
+ if (group === 'headers' && httpHeaderNameIssue(name, []) === 'invalid')
74
+ issues.push({ field: group, index, code: 'headerNameInvalid', message: 'Invalid HTTP header name.' })
75
+ }
76
+ }
77
+ return issues
78
+ }
79
+
80
+ /** Only a named parameter rename carries a source; changing modes does not rename a port. */
81
+ export function requestInputMappings(
82
+ mappings: Readonly<Record<string, InputMapping>>,
83
+ previous: TaskDefinitionBase,
84
+ next: TaskDefinitionBase,
85
+ ): Record<string, InputMapping> {
86
+ const candidate = renamedPort(previous.inputs, next.inputs)
87
+ const rename = candidate != null && ['query.', 'header.'].some((prefix) => candidate.every((name) => name.startsWith(prefix))) ? candidate : undefined
88
+ return remapInputMappings(mappings, next.inputs, rename)
89
+ }
90
+
91
+ export function withRequestParameterFields<Node extends RequestTaskNode>(
92
+ node: Node,
93
+ group: 'query' | 'headers',
94
+ values: Record<string, JsonValue>,
95
+ previous: RequestParameters,
96
+ createTask: (parameters: RequestParameters) => Node['task'],
97
+ ): Node {
98
+ const task = createTask({ mode: 'fields', names: Object.keys(values) })
99
+ const inputs = requestInputMappings(node.inputs, node.task, task)
100
+ const rename = renamedPort(node.task.inputs, task.inputs)
101
+ for (const [name, value] of Object.entries(values)) {
102
+ const handle = requestParameterHandle(group, name)
103
+ const existing = previous.mode === 'fields' && (previous.names.includes(name) || rename?.[1] === handle)
104
+ if (inputs[handle] == null && !existing) inputs[handle] = { kind: 'value', value }
105
+ }
106
+ return { ...node, task, inputs }
107
+ }
108
+
109
+ export function withRequestParameterSource<Node extends RequestTaskNode>(
110
+ node: Node,
111
+ group: 'query' | 'headers',
112
+ mapping: InputMapping | undefined,
113
+ createTask: (parameters: RequestParameters) => Node['task'],
114
+ ): Node {
115
+ const task = createTask({ mode: 'object', names: [] })
116
+ const inputs = requestInputMappings(node.inputs, node.task, task)
117
+ if (mapping == null) delete inputs[group]
118
+ else inputs[group] = mapping
119
+ return { ...node, task, inputs }
120
+ }
@@ -1,5 +1,6 @@
1
1
  import { dequal } from 'dequal/lite'
2
2
  import { connectorActionContracts } from '../../connector/common/actionContract.ts'
3
+ import { connectorProxyHasBody, connectorProxyIssues, connectorProxyTask } from '../../connector/common/proxy.ts'
3
4
  import { decisionTaskIssues } from '../../decision/common/decision.ts'
4
5
  import { httpRequestIssues, httpAuthHandles, httpRequestTask, httpRequestUrl } from '../../http/common/httpRequest.ts'
5
6
  import { openApiIssues, authHandles } from '../../openapi/common/openapi.ts'
@@ -14,7 +15,17 @@ import { connectionUsage } from './connectionUsage.ts'
14
15
  export { matchesTriggerOutputs, normalizeTriggerOutputs, triggerOutputDefinitions, triggerOutputPorts } from '../../trigger/common/contract.ts'
15
16
  import type { EngineContract } from '../../runtime/common/engineContract.ts'
16
17
  import type { RuntimeProgram } from '../../runtime/common/runtime.ts'
17
- import type { ConnectorActionCapability, ConnectorCapability, Graph, InputMapping, JsonValue, Port, RevisionContent, SchemaMismatch } from './change.ts'
18
+ import type {
19
+ ConnectorActionCapability,
20
+ ConnectorCapability,
21
+ Graph,
22
+ InputMapping,
23
+ InputPort,
24
+ JsonValue,
25
+ Port,
26
+ RevisionContent,
27
+ SchemaMismatch,
28
+ } from './change.ts'
18
29
 
19
30
  import { findEngineContract } from '../../runtime/common/engineContract.ts'
20
31
  import { agentConfigIssues } from './agent.ts'
@@ -289,6 +300,38 @@ export async function validateFlow(revision: RevisionContent, engine: EngineCont
289
300
  path: `/document/graph/nodes/${nodeId}/task`,
290
301
  })
291
302
  }
303
+ if (task.executor.kind === 'connector-proxy') {
304
+ const endpoint = node.inputs.endpoint
305
+ if (endpoint?.kind === 'value' && (typeof endpoint.value !== 'string' || !endpoint.value.trim()))
306
+ checked.diagnostics.push({
307
+ code: 'connector-proxy.endpoint-invalid',
308
+ column: 0,
309
+ line: 0,
310
+ message: 'A nonempty endpoint is required.',
311
+ path: `/document/graph/nodes/${nodeId}/inputs/endpoint`,
312
+ })
313
+ const bodyPort = task.inputs.find((port): port is InputPort => 'handle' in port && port.handle === 'body')
314
+ for (const issue of connectorProxyIssues(task.executor, connectorProxyHasBody(node.inputs.body, bodyPort?.value)))
315
+ checked.diagnostics.push({
316
+ code: 'connector-proxy.config-invalid',
317
+ column: 0,
318
+ line: 0,
319
+ message: issue.message,
320
+ path: issue.field === 'bodyFormat' ? `/document/graph/nodes/${nodeId}/inputs/body` : `/document/graph/nodes/${nodeId}/task/executor/${issue.field}`,
321
+ })
322
+ const expected = connectorProxyTask(task.executor)
323
+ if (
324
+ !dequal(httpPortContracts(task.inputs), httpPortContracts(expected.inputs)) ||
325
+ !dequal(httpPortContracts(task.outputs), httpPortContracts(expected.outputs))
326
+ )
327
+ checked.diagnostics.push({
328
+ code: 'connector-proxy.ports-invalid',
329
+ column: 0,
330
+ line: 0,
331
+ message: 'Provider API request ports do not match their configuration.',
332
+ path: `/document/graph/nodes/${nodeId}/task`,
333
+ })
334
+ }
292
335
  if (task.executor.kind == 'http-request') {
293
336
  for (const issue of httpRequestIssues(task.executor))
294
337
  checked.diagnostics.push({
@@ -458,6 +501,13 @@ const httpPortContracts = (ports: import('./change.ts').ManagedTaskDefinition['i
458
501
  export async function prepareFlow(revision: RevisionContent, engineContract: string, triggerId?: string): Promise<PrepareFlowResult> {
459
502
  const engine = findEngineContract(engineContract)
460
503
  if (engine == null) return { kind: 'engine-unsupported' }
504
+ if (
505
+ !engine.connectorProxy &&
506
+ Object.values(revision.document.graph.nodes).some(
507
+ (node) => node.kind === 'task' && 'executor' in node.task && node.task.executor.kind === 'connector-proxy',
508
+ )
509
+ )
510
+ return { kind: 'engine-unsupported' }
461
511
  if (
462
512
  !engine.flowState &&
463
513
  Object.values(revision.document.graph.nodes).some(
@@ -1,5 +1,6 @@
1
- import type { NodeSource, PortDefinition } from './change.ts'
1
+ import type { JsonValue, NodeSource, PortDefinition } from './change.ts'
2
2
 
3
+ import { propertySchemaWithDefinitions } from '../../base/common/jsonSchema.ts'
3
4
  import { schemaObject } from './schema.ts'
4
5
 
5
6
  /** Only explicitly declared, first-level object properties are selectable. */
@@ -17,7 +18,7 @@ export function sourceFields(output: PortDefinition): readonly { readonly field:
17
18
  field,
18
19
  port: {
19
20
  ...(typeof description == 'string' ? { description } : {}),
20
- jsonSchema,
21
+ jsonSchema: propertySchemaWithDefinitions(schema, jsonSchema) as JsonValue,
21
22
  nullable: missingParent || !required.includes(field),
22
23
  },
23
24
  }
@@ -86,8 +86,8 @@ export function cleanWorkflowFile(value: unknown): { readonly file: WorkflowFile
86
86
  }
87
87
  }
88
88
 
89
- export function encodeWorkflowFile(file: WorkflowFile): string {
90
- return `${JSON.stringify(cleanWorkflowFile(file).file, null, 2)}\n`
89
+ export function encodeWorkflowFile(file: WorkflowFile, options: { readonly clean?: boolean } = {}): string {
90
+ return `${JSON.stringify(options.clean === false ? decodeWorkflowFile(file) : cleanWorkflowFile(file).file, null, 2)}\n`
91
91
  }
92
92
 
93
93
  export function workflowFileName(name: string): string {
@@ -1,17 +1,24 @@
1
- import type { InputMapping, InputPort, JsonValue, ManagedTaskDefinitionOf, TaskDefinitionBase, TaskExecutor } from '../../flow/common/model.ts'
1
+ import type { InputMapping, InputPort, JsonValue, ManagedTaskDefinitionOf, TaskDefinitionBase } from '../../flow/common/model.ts'
2
+ import type { RequestTaskNode } from '../../flow/common/requestParameters.ts'
2
3
 
3
- import { dequal } from 'dequal/lite'
4
4
  import { z } from 'zod'
5
- import { remapInputMappings, renamedPort } from '../../flow/common/portChanges.ts'
5
+ import {
6
+ requestParameterSchema,
7
+ requestParameterPorts,
8
+ requestParameterIssues,
9
+ requestInputMappings,
10
+ retainRequestPorts,
11
+ withRequestParameterFields,
12
+ withRequestParameterSource,
13
+ } from '../../flow/common/requestParameters.ts'
6
14
  import { httpHeaderNameIssue } from './headers.ts'
7
15
 
8
16
  export { httpHostAllowed, isHttpHost, maxHttpHosts } from './hosts.ts'
9
17
 
10
- const parameterSchema = z.strictObject({ mode: z.enum(['fields', 'object']).default('fields'), names: z.array(z.string()).default([]) })
11
18
  export const httpRequestConfigSchema = z.strictObject({
12
19
  method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']).default('GET'),
13
- query: parameterSchema.default({ mode: 'fields', names: [] }),
14
- headers: parameterSchema.default({ mode: 'fields', names: [] }),
20
+ query: requestParameterSchema.default({ mode: 'fields', names: [] }),
21
+ headers: requestParameterSchema.default({ mode: 'fields', names: [] }),
15
22
  bodyFormat: z.enum(['none', 'json', 'text', 'form']).default('none'),
16
23
  authentication: z
17
24
  .union([
@@ -36,36 +43,15 @@ export const httpQuerySchema = { type: 'object', additionalProperties: queryValu
36
43
  export function httpAuthHandles(config: HttpRequestConfig): string[] {
37
44
  return config.authentication.type == 'none' ? [] : config.authentication.type == 'basic' ? ['auth.username', 'auth.password'] : ['auth.token']
38
45
  }
39
- // Preserve metadata on ports whose contracts did not change.
40
- function retain(ports: readonly InputPort[], old: TaskDefinitionBase['inputs']): TaskDefinitionBase['inputs'] {
41
- const remaining = new Map(ports.map((port) => [port.handle, port]))
42
- return [
43
- ...old.flatMap((item): TaskDefinitionBase['inputs'] => {
44
- if ('group' in item) return [item]
45
- const next = remaining.get(item.handle)
46
- if (next == null) return []
47
- remaining.delete(item.handle)
48
- return [dequal(item.jsonSchema, next.jsonSchema) && item.nullable == next.nullable ? item : next]
49
- }),
50
- ...remaining.values(),
51
- ]
52
- }
53
46
  export function httpRequestTask(
54
47
  config: HttpRequestConfig = httpRequestConfigSchema.parse({}),
55
48
  name = 'HTTP Request',
56
49
  previous?: TaskDefinitionBase,
57
50
  ): ManagedTaskDefinitionOf<HttpRequestExecutor> {
58
- const inputs: InputPort[] = [{ handle: 'url', jsonSchema: { type: 'string' }, nullable: false }]
59
- for (const group of ['query', 'headers'] as const) {
60
- if (config[group].mode == 'object') inputs.push({ handle: group, jsonSchema: group == 'query' ? httpQuerySchema : httpHeaderSchema, nullable: true })
61
- else
62
- for (const parameterName of config[group].names.filter((value) => value.trim()))
63
- inputs.push({
64
- handle: `${group == 'query' ? 'query' : 'header'}.${parameterName}`,
65
- jsonSchema: group == 'query' ? queryValueSchema : { type: 'string' },
66
- nullable: true,
67
- })
68
- }
51
+ const inputs: InputPort[] = [
52
+ { handle: 'url', jsonSchema: { type: 'string' }, nullable: false },
53
+ ...requestParameterPorts(config, httpQuerySchema, queryValueSchema, httpHeaderSchema),
54
+ ]
69
55
  if (config.bodyFormat != 'none')
70
56
  inputs.push({
71
57
  handle: 'body',
@@ -82,8 +68,8 @@ export function httpRequestTask(
82
68
  return {
83
69
  name,
84
70
  executor: { ...config, kind: 'http-request' },
85
- inputs: retain(inputs, previous?.inputs ?? []),
86
- outputs: retain(outputs, previous?.outputs ?? []),
71
+ inputs: retainRequestPorts(inputs, previous?.inputs),
72
+ outputs: retainRequestPorts(outputs, previous?.outputs),
87
73
  }
88
74
  }
89
75
  export interface HttpRequestIssue {
@@ -96,18 +82,7 @@ export function httpRequestIssues(config: HttpRequestConfig): HttpRequestIssue[]
96
82
  const issues: HttpRequestIssue[] = []
97
83
  if (['GET', 'HEAD'].includes(config.method) && config.bodyFormat != 'none')
98
84
  issues.push({ field: 'bodyFormat', code: 'bodyUnsupported', message: 'GET and HEAD do not support a request body.' })
99
- for (const group of ['query', 'headers'] as const) {
100
- if (config[group].mode != 'fields') continue
101
- const seen = new Set<string>()
102
- for (const [index, name] of config[group].names.entries()) {
103
- const key = group == 'headers' ? name.toLowerCase() : name
104
- if (!name.trim() || seen.has(key))
105
- issues.push({ field: group, index, code: 'parameterNamesInvalid', message: 'Parameter names must be nonempty and unique.' })
106
- seen.add(key)
107
- if (group == 'headers' && httpHeaderNameIssue(name, []) === 'invalid')
108
- issues.push({ field: group, index, code: 'headerNameInvalid', message: 'Invalid HTTP header name.' })
109
- }
110
- }
85
+ issues.push(...requestParameterIssues(config))
111
86
  const auth = config.authentication
112
87
  if (auth.type != 'none') {
113
88
  const group = auth.type == 'apiKey' && auth.in == 'query' ? 'query' : 'headers'
@@ -218,49 +193,24 @@ export function parseHttpResponse(config: HttpRequestConfig, status: number, hea
218
193
  }
219
194
 
220
195
  /** Only a named parameter rename carries a value; changing source modes is not a rename. */
221
- export function httpRequestInputs(
222
- mappings: Readonly<Record<string, InputMapping>>,
223
- previous: TaskDefinitionBase,
224
- next: TaskDefinitionBase,
225
- ): Record<string, InputMapping> {
226
- const candidate = renamedPort(previous.inputs, next.inputs)
227
- const rename = candidate != null && ['query.', 'header.'].some((prefix) => candidate.every((name) => name.startsWith(prefix))) ? candidate : undefined
228
- return remapInputMappings(mappings, next.inputs, rename)
229
- }
196
+ export const httpRequestInputs = requestInputMappings
230
197
 
231
- /** A Task node as HTTP helpers read it; inline Tasks have no executor. */
232
- interface HttpTaskNode {
233
- readonly inputs: Readonly<Record<string, InputMapping>>
234
- readonly task: TaskDefinitionBase & { readonly executor?: TaskExecutor }
235
- }
236
-
237
- function httpRequestExecutor(node: HttpTaskNode): HttpRequestExecutor | undefined {
198
+ function httpRequestExecutor(node: RequestTaskNode): HttpRequestExecutor | undefined {
238
199
  const executor = node.task.executor
239
200
  return executor?.kind === 'http-request' ? (executor as HttpRequestExecutor) : undefined
240
201
  }
241
202
 
242
203
  /** The object editor assembles named inputs without changing unrelated request settings. */
243
- export function withHttpParameterFields<Node extends HttpTaskNode>(node: Node, group: 'query' | 'headers', values: Record<string, JsonValue>): Node {
204
+ export function withHttpParameterFields<Node extends RequestTaskNode>(node: Node, group: 'query' | 'headers', values: Record<string, JsonValue>): Node {
244
205
  const config = httpRequestExecutor(node)
245
206
  if (config == null) return node
246
- const task = httpRequestTask({ ...config, [group]: { mode: 'fields', names: Object.keys(values) } }, node.task.name, node.task)
247
- const inputs = httpRequestInputs(node.inputs, node.task, task)
248
- const rename = renamedPort(node.task.inputs, task.inputs)
249
- for (const [name, value] of Object.entries(values)) {
250
- const handle = `${group === 'query' ? 'query' : 'header'}.${name}`
251
- const existing = config[group].mode === 'fields' && (config[group].names.includes(name) || rename?.[1] === handle)
252
- if (inputs[handle] == null && !existing) inputs[handle] = { kind: 'value', value }
253
- }
254
- // Only the HTTP Task and its input mappings change, so the node keeps its own type.
255
- return { ...node, task, inputs } as Node
207
+ return withRequestParameterFields(node, group, values, config[group], (parameters) =>
208
+ httpRequestTask({ ...config, [group]: parameters }, node.task.name, node.task),
209
+ )
256
210
  }
257
211
 
258
- export function withHttpParameterSource<Node extends HttpTaskNode>(node: Node, group: 'query' | 'headers', mapping?: InputMapping): Node {
212
+ export function withHttpParameterSource<Node extends RequestTaskNode>(node: Node, group: 'query' | 'headers', mapping?: InputMapping): Node {
259
213
  const config = httpRequestExecutor(node)
260
214
  if (config == null) return node
261
- const task = httpRequestTask({ ...config, [group]: { mode: 'object', names: [] } }, node.task.name, node.task)
262
- const inputs = httpRequestInputs(node.inputs, node.task, task)
263
- if (mapping == null) delete inputs[group]
264
- else inputs[group] = mapping
265
- return { ...node, task, inputs } as Node
215
+ return withRequestParameterSource(node, group, mapping, (parameters) => httpRequestTask({ ...config, [group]: parameters }, node.task.name, node.task))
266
216
  }
@@ -10,10 +10,11 @@ export interface EngineContract {
10
10
  /** Code may call `context.fetch` for hosts declared by its `http` Capability. */
11
11
  readonly codeFetch?: boolean
12
12
  readonly flowState?: boolean
13
+ readonly connectorProxy?: boolean
13
14
  }
14
15
 
15
- /** v10 adds persistent Flow State to the published v9 Node compatibility and fetch contract. */
16
- export const currentEngineContract = 'open-flow-engine/v10'
16
+ /** v11 adds Provider API requests through Connector Proxy. */
17
+ export const currentEngineContract = 'open-flow-engine/v11'
17
18
  /** The published v8 Node compatibility contract, retained for existing Publications and Runs. */
18
19
  export const nodejsEngineContract = 'open-flow-engine/v8/nodejs-compat-v1'
19
20
 
@@ -65,10 +66,12 @@ const nodejsContract: EngineContract = {
65
66
  }
66
67
 
67
68
  const v9Contract: EngineContract = { ...nodejsContract, codeFetch: true, platformSource: platformSource('open-flow-engine/v9') }
68
- const currentContract: EngineContract = { ...v9Contract, flowState: true, platformSource: platformSource(currentEngineContract) }
69
+ const v10Contract: EngineContract = { ...v9Contract, flowState: true, platformSource: platformSource('open-flow-engine/v10') }
70
+ const currentContract: EngineContract = { ...v10Contract, connectorProxy: true, platformSource: platformSource(currentEngineContract) }
69
71
 
70
72
  export function findEngineContract(contract: string): EngineContract | undefined {
71
73
  if (contract == currentEngineContract) return currentContract
74
+ if (contract == 'open-flow-engine/v10') return v10Contract
72
75
  if (contract == 'open-flow-engine/v9') return v9Contract
73
76
  if (contract == 'open-flow-engine/v8') return v8Contract
74
77
  if (contract == nodejsEngineContract) return nodejsContract