@open-mercato/core 0.10.1-develop.7815.1.94bb61610f → 0.10.1-develop.7821.1.e5115777cb
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/.turbo/turbo-build.log +1 -1
- package/dist/modules/communication_channels/workers/poll-channel.js +44 -16
- package/dist/modules/communication_channels/workers/poll-channel.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +1 -1
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +15 -0
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/generate/route.js +3 -1
- package/dist/modules/workflows/api/definitions/generate/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +15 -0
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/lib/agent-result-mapping.js +3 -14
- package/dist/modules/workflows/lib/agent-result-mapping.js.map +2 -2
- package/dist/modules/workflows/lib/definition-evaluation.js +16 -2
- package/dist/modules/workflows/lib/definition-evaluation.js.map +2 -2
- package/dist/modules/workflows/lib/mapping-target-validation.js +85 -0
- package/dist/modules/workflows/lib/mapping-target-validation.js.map +7 -0
- package/dist/modules/workflows/lib/parallel-handler.js +10 -8
- package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
- package/dist/modules/workflows/lib/safe-mapping-path.js +38 -0
- package/dist/modules/workflows/lib/safe-mapping-path.js.map +7 -0
- package/dist/modules/workflows/lib/step-handler.js +5 -16
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/communication_channels/workers/poll-channel.ts +34 -5
- package/src/modules/sales/commands/documents.ts +3 -1
- package/src/modules/workflows/api/definitions/[id]/route.ts +17 -0
- package/src/modules/workflows/api/definitions/generate/route.ts +3 -1
- package/src/modules/workflows/api/definitions/route.ts +15 -0
- package/src/modules/workflows/i18n/de.json +1 -0
- package/src/modules/workflows/i18n/en.json +1 -0
- package/src/modules/workflows/i18n/es.json +1 -0
- package/src/modules/workflows/i18n/ko.json +1 -0
- package/src/modules/workflows/i18n/pl.json +1 -0
- package/src/modules/workflows/lib/agent-result-mapping.ts +3 -16
- package/src/modules/workflows/lib/definition-evaluation.ts +19 -2
- package/src/modules/workflows/lib/mapping-target-validation.ts +113 -0
- package/src/modules/workflows/lib/parallel-handler.ts +13 -9
- package/src/modules/workflows/lib/safe-mapping-path.ts +44 -0
- package/src/modules/workflows/lib/step-handler.ts +5 -32
|
@@ -28,6 +28,12 @@ import {
|
|
|
28
28
|
} from '../openapi'
|
|
29
29
|
import { invalidateTriggerCache } from '../../lib/event-trigger-service'
|
|
30
30
|
import { normalizeDefinitionValidationIssues } from '../../lib/definition-error-body'
|
|
31
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
32
|
+
import {
|
|
33
|
+
mappingTargetIssuesForApi,
|
|
34
|
+
UNSAFE_MAPPING_TARGET_MESSAGE,
|
|
35
|
+
UNSAFE_MAPPING_TARGET_MESSAGE_KEY,
|
|
36
|
+
} from '../../lib/mapping-target-validation'
|
|
31
37
|
import { getAllCodeWorkflows } from '../../lib/code-registry'
|
|
32
38
|
import {
|
|
33
39
|
authorizeWorkflowGrantChange,
|
|
@@ -262,6 +268,15 @@ export async function POST(request: NextRequest) {
|
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
const input: CreateWorkflowDefinitionApiInput = validation.data
|
|
271
|
+
const { translate } = await resolveTranslations()
|
|
272
|
+
const mappingMessage = translate(UNSAFE_MAPPING_TARGET_MESSAGE_KEY, UNSAFE_MAPPING_TARGET_MESSAGE)
|
|
273
|
+
const mappingIssues = mappingTargetIssuesForApi(input.definition, ['definition'], mappingMessage)
|
|
274
|
+
if (mappingIssues.length > 0) {
|
|
275
|
+
return NextResponse.json(
|
|
276
|
+
{ error: 'Validation failed', details: mappingIssues },
|
|
277
|
+
{ status: 400 }
|
|
278
|
+
)
|
|
279
|
+
}
|
|
265
280
|
|
|
266
281
|
// New definitions default to strict interpolation (spec §3.6). The default
|
|
267
282
|
// lives on the create path only — a schema-level default would flip
|
|
@@ -2029,6 +2029,7 @@
|
|
|
2029
2029
|
"workflows.validation.timerDurationOrUntilRequired": "„Auf Timer warten“ erfordert entweder „Dauer“ oder „Warten bis“.",
|
|
2030
2030
|
"workflows.validation.timerDurationXorUntil": "„Auf Timer warten“ akzeptiert „Dauer“ oder „Warten bis“, nicht beides. Löschen Sie eines davon.",
|
|
2031
2031
|
"workflows.validation.uniqueId": "ID muss eindeutig sein",
|
|
2032
|
+
"workflows.validation.unsafeMappingTarget": "Zielpfade für Zuordnungen dürfen keine Segmente namens __proto__, constructor oder prototype enthalten",
|
|
2032
2033
|
"workflows.validation.untilMustBeFuture": "„Warten bis“ muss ein Datum und eine Uhrzeit in der Zukunft sein.",
|
|
2033
2034
|
"workflows.validation.workflowIdFormat": "Workflow-ID darf nur Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche enthalten",
|
|
2034
2035
|
"workflows.validation.workflowIdMaxLength": "Workflow-ID darf maximal 50 Zeichen lang sein",
|
|
@@ -2029,6 +2029,7 @@
|
|
|
2029
2029
|
"workflows.validation.timerDurationOrUntilRequired": "Wait For Timer requires either \"Duration\" or \"Wait Until\".",
|
|
2030
2030
|
"workflows.validation.timerDurationXorUntil": "Wait For Timer accepts \"Duration\" or \"Wait Until\", not both. Clear one of them.",
|
|
2031
2031
|
"workflows.validation.uniqueId": "ID must be unique",
|
|
2032
|
+
"workflows.validation.unsafeMappingTarget": "Mapping target paths cannot contain __proto__, constructor, or prototype segments",
|
|
2032
2033
|
"workflows.validation.untilMustBeFuture": "Wait Until must be a date and time in the future.",
|
|
2033
2034
|
"workflows.validation.workflowIdFormat": "Workflow ID must contain only lowercase letters, numbers, hyphens, and underscores",
|
|
2034
2035
|
"workflows.validation.workflowIdMaxLength": "Workflow ID must be 50 characters or less",
|
|
@@ -2029,6 +2029,7 @@
|
|
|
2029
2029
|
"workflows.validation.timerDurationOrUntilRequired": "Esperar temporizador requiere una Duración o Esperar hasta.",
|
|
2030
2030
|
"workflows.validation.timerDurationXorUntil": "Esperar temporizador acepta Duración o Esperar hasta, no ambos. Borra uno de ellos.",
|
|
2031
2031
|
"workflows.validation.uniqueId": "El ID debe ser unico",
|
|
2032
|
+
"workflows.validation.unsafeMappingTarget": "Las rutas de destino de asignación no pueden contener segmentos __proto__, constructor o prototype",
|
|
2032
2033
|
"workflows.validation.untilMustBeFuture": "Esperar hasta debe ser una fecha y hora futura.",
|
|
2033
2034
|
"workflows.validation.workflowIdFormat": "El ID del flujo de trabajo solo puede contener letras minusculas, numeros, guiones y guiones bajos",
|
|
2034
2035
|
"workflows.validation.workflowIdMaxLength": "El ID del flujo de trabajo debe tener 50 caracteres o menos",
|
|
@@ -2029,6 +2029,7 @@
|
|
|
2029
2029
|
"workflows.validation.timerDurationOrUntilRequired": "Wait For Timer requires either \"Duration\" or \"Wait Until\".",
|
|
2030
2030
|
"workflows.validation.timerDurationXorUntil": "Wait For Timer accepts \"Duration\" or \"Wait Until\", not both. Clear one of them.",
|
|
2031
2031
|
"workflows.validation.uniqueId": "ID는 고유해야 합니다",
|
|
2032
|
+
"workflows.validation.unsafeMappingTarget": "매핑 대상 경로에는 __proto__, constructor 또는 prototype 세그먼트를 포함할 수 없습니다",
|
|
2032
2033
|
"workflows.validation.untilMustBeFuture": "대기 종료 시각은 미래의 날짜와 시간이어야 합니다.",
|
|
2033
2034
|
"workflows.validation.workflowIdFormat": "워크플로우 ID는 소문자, 숫자, 하이픈, 밑줄만 포함해야 합니다",
|
|
2034
2035
|
"workflows.validation.workflowIdMaxLength": "워크플로우 ID는 50자 이하여야 합니다",
|
|
@@ -2029,6 +2029,7 @@
|
|
|
2029
2029
|
"workflows.validation.timerDurationOrUntilRequired": "Krok „Czekaj na timer” wymaga podania „Czasu trwania” lub „Czekaj do”.",
|
|
2030
2030
|
"workflows.validation.timerDurationXorUntil": "Krok „Czekaj na timer” przyjmuje „Czas trwania” albo „Czekaj do”, nie oba naraz. Wyczyść jedno z nich.",
|
|
2031
2031
|
"workflows.validation.uniqueId": "ID musi być unikalne",
|
|
2032
|
+
"workflows.validation.unsafeMappingTarget": "Ścieżki docelowe mapowania nie mogą zawierać segmentów __proto__, constructor ani prototype",
|
|
2032
2033
|
"workflows.validation.untilMustBeFuture": "„Czekaj do” musi być datą i godziną w przyszłości.",
|
|
2033
2034
|
"workflows.validation.workflowIdFormat": "ID przepływu może zawierać tylko małe litery, cyfry, myślniki i podkreślenia",
|
|
2034
2035
|
"workflows.validation.workflowIdMaxLength": "ID przepływu musi mieć 50 znaków lub mniej",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
17
|
+
import { safeGetNestedValue, safeSetNestedValue } from './safe-mapping-path'
|
|
17
18
|
|
|
18
19
|
const logger = createLogger('workflows').child({ component: 'agent-result-mapping' })
|
|
19
20
|
|
|
@@ -27,20 +28,6 @@ export type AgentResultEnvelope = {
|
|
|
27
28
|
artifacts?: unknown[]
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
function getNestedValue(obj: any, path: string): any {
|
|
31
|
-
return path.split('.').reduce((current, key) => current?.[key], obj)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function setNestedValue(obj: Record<string, any>, path: string, value: any): void {
|
|
35
|
-
const keys = path.split('.')
|
|
36
|
-
const lastKey = keys.pop()!
|
|
37
|
-
const target = keys.reduce<Record<string, any>>((current, key) => {
|
|
38
|
-
if (!(key in current)) current[key] = {}
|
|
39
|
-
return current[key]
|
|
40
|
-
}, obj)
|
|
41
|
-
target[lastKey] = value
|
|
42
|
-
}
|
|
43
|
-
|
|
44
31
|
function warnOnTemplateMapping(sourcePath: string): void {
|
|
45
32
|
if (/\{\{.*\}\}/.test(sourcePath)) {
|
|
46
33
|
logger.warn(
|
|
@@ -79,9 +66,9 @@ export function mapAgentResultToContext(
|
|
|
79
66
|
const result: Record<string, any> = {}
|
|
80
67
|
for (const [targetKey, sourcePath] of Object.entries(outputMapping)) {
|
|
81
68
|
warnOnTemplateMapping(sourcePath)
|
|
82
|
-
const value =
|
|
69
|
+
const value = safeGetNestedValue(source, sourcePath)
|
|
83
70
|
if (value !== undefined) {
|
|
84
|
-
|
|
71
|
+
safeSetNestedValue(result, targetKey, value)
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
74
|
return result
|
|
@@ -31,6 +31,12 @@ import {
|
|
|
31
31
|
} from './definition-error-body'
|
|
32
32
|
import { collectUnresolvedContextRefWarnings } from './expression-refs'
|
|
33
33
|
import { definitionToGraph, validateWorkflowGraph } from './graph-utils'
|
|
34
|
+
import {
|
|
35
|
+
collectUnsafeMappingTargetIssues,
|
|
36
|
+
mappingTargetIssuesForApi,
|
|
37
|
+
UNSAFE_MAPPING_TARGET_MESSAGE,
|
|
38
|
+
UNSAFE_MAPPING_TARGET_MESSAGE_KEY,
|
|
39
|
+
} from './mapping-target-validation'
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Problems-panel copy for an unresolved `{{context.*}}` reference. Shared so
|
|
@@ -75,6 +81,7 @@ export interface EvaluateWorkflowDefinitionOptions {
|
|
|
75
81
|
* those warnings are skipped (they are advisory and never blocking).
|
|
76
82
|
*/
|
|
77
83
|
ledger?: ContextLedger | null
|
|
84
|
+
translate?: WorkflowIssueTranslator
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
export interface WorkflowDefinitionEvaluation {
|
|
@@ -128,7 +135,14 @@ export function evaluateWorkflowDefinition(
|
|
|
128
135
|
const { nodes, edges } = definitionToGraph(definition, { autoLayout: false })
|
|
129
136
|
const graphErrors = validateWorkflowGraph(nodes, edges)
|
|
130
137
|
const parsed = workflowDefinitionDataSchema.safeParse(definition)
|
|
131
|
-
const
|
|
138
|
+
const mappingMessage = options.translate
|
|
139
|
+
? options.translate(UNSAFE_MAPPING_TARGET_MESSAGE_KEY, UNSAFE_MAPPING_TARGET_MESSAGE, {})
|
|
140
|
+
: UNSAFE_MAPPING_TARGET_MESSAGE
|
|
141
|
+
const mappingIssues = collectUnsafeMappingTargetIssues(definition, mappingMessage)
|
|
142
|
+
const zodIssues: ZodIssueLike[] = [
|
|
143
|
+
...(parsed.success ? [] : parsed.error.issues),
|
|
144
|
+
...mappingIssues,
|
|
145
|
+
]
|
|
132
146
|
const ledger = options.ledger ?? null
|
|
133
147
|
const configWarnings: ZodIssueLike[] = [
|
|
134
148
|
...collectActivityConfigWarnings(definition),
|
|
@@ -150,7 +164,10 @@ export function evaluateWorkflowDefinition(
|
|
|
150
164
|
valid: errors === 0,
|
|
151
165
|
errorCount: errors,
|
|
152
166
|
warningCount: warnings,
|
|
153
|
-
issues:
|
|
167
|
+
issues: [
|
|
168
|
+
...(parsed.success ? [] : normalizeDefinitionValidationIssues(parsed.error)),
|
|
169
|
+
...mappingTargetIssuesForApi(definition, [], mappingMessage),
|
|
170
|
+
],
|
|
154
171
|
problems,
|
|
155
172
|
}
|
|
156
173
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { WorkflowDefinitionData } from '../data/entities'
|
|
2
|
+
import type { DefinitionValidationIssue } from './definition-error-body'
|
|
3
|
+
import type { ZodIssueLike } from './collect-validation-issues'
|
|
4
|
+
import { isSafeMappingTargetPath } from './safe-mapping-path'
|
|
5
|
+
|
|
6
|
+
export const UNSAFE_MAPPING_TARGET_MESSAGE_KEY = 'workflows.validation.unsafeMappingTarget'
|
|
7
|
+
|
|
8
|
+
export const UNSAFE_MAPPING_TARGET_MESSAGE =
|
|
9
|
+
'Mapping target paths cannot contain __proto__, constructor, or prototype segments'
|
|
10
|
+
|
|
11
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
12
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function collectMappingIssues(
|
|
16
|
+
value: unknown,
|
|
17
|
+
path: Array<string | number>,
|
|
18
|
+
message: string,
|
|
19
|
+
): ZodIssueLike[] {
|
|
20
|
+
if (!isRecord(value)) return []
|
|
21
|
+
return Object.keys(value)
|
|
22
|
+
.filter((targetPath) => !isSafeMappingTargetPath(targetPath))
|
|
23
|
+
.map((targetPath) => ({
|
|
24
|
+
path: [...path, targetPath],
|
|
25
|
+
message,
|
|
26
|
+
}))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function collectActivityIssues(
|
|
30
|
+
activities: unknown,
|
|
31
|
+
path: Array<string | number>,
|
|
32
|
+
message: string,
|
|
33
|
+
): ZodIssueLike[] {
|
|
34
|
+
if (!Array.isArray(activities)) return []
|
|
35
|
+
return activities.flatMap((activity, activityIndex) => {
|
|
36
|
+
if (!isRecord(activity) || activity.activityType !== 'INVOKE_AGENT') return []
|
|
37
|
+
const config = isRecord(activity.config) ? activity.config : {}
|
|
38
|
+
return collectMappingIssues(config.outputMapping, [
|
|
39
|
+
...path,
|
|
40
|
+
activityIndex,
|
|
41
|
+
'config',
|
|
42
|
+
'outputMapping',
|
|
43
|
+
], message)
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function collectUnsafeMappingTargetIssues(
|
|
48
|
+
definition: WorkflowDefinitionData,
|
|
49
|
+
message = UNSAFE_MAPPING_TARGET_MESSAGE,
|
|
50
|
+
): ZodIssueLike[] {
|
|
51
|
+
const stepIssues = Array.isArray(definition.steps)
|
|
52
|
+
? definition.steps.flatMap((step, stepIndex) => {
|
|
53
|
+
if (!isRecord(step)) return []
|
|
54
|
+
const config = isRecord(step.config) ? step.config : {}
|
|
55
|
+
const subWorkflowConfig = isRecord(step.subWorkflowConfig) ? step.subWorkflowConfig : {}
|
|
56
|
+
const issues: ZodIssueLike[] = []
|
|
57
|
+
|
|
58
|
+
if (step.stepType === 'SUB_WORKFLOW') {
|
|
59
|
+
issues.push(
|
|
60
|
+
...collectMappingIssues(config.inputMapping, ['steps', stepIndex, 'config', 'inputMapping'], message),
|
|
61
|
+
...collectMappingIssues(config.outputMapping, ['steps', stepIndex, 'config', 'outputMapping'], message),
|
|
62
|
+
...collectMappingIssues(subWorkflowConfig.inputMapping, [
|
|
63
|
+
'steps',
|
|
64
|
+
stepIndex,
|
|
65
|
+
'subWorkflowConfig',
|
|
66
|
+
'inputMapping',
|
|
67
|
+
], message),
|
|
68
|
+
...collectMappingIssues(subWorkflowConfig.outputMapping, [
|
|
69
|
+
'steps',
|
|
70
|
+
stepIndex,
|
|
71
|
+
'subWorkflowConfig',
|
|
72
|
+
'outputMapping',
|
|
73
|
+
], message),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
if (step.stepType === 'PARALLEL_JOIN') {
|
|
77
|
+
issues.push(
|
|
78
|
+
...collectMappingIssues(config.outputMapping, ['steps', stepIndex, 'config', 'outputMapping'], message),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
issues.push(...collectActivityIssues(step.activities, ['steps', stepIndex, 'activities'], message))
|
|
82
|
+
return issues
|
|
83
|
+
})
|
|
84
|
+
: []
|
|
85
|
+
|
|
86
|
+
const transitionIssues = Array.isArray(definition.transitions)
|
|
87
|
+
? definition.transitions.flatMap((transition, transitionIndex) => {
|
|
88
|
+
if (!isRecord(transition)) return []
|
|
89
|
+
return collectActivityIssues(transition.activities, [
|
|
90
|
+
'transitions',
|
|
91
|
+
transitionIndex,
|
|
92
|
+
'activities',
|
|
93
|
+
], message)
|
|
94
|
+
})
|
|
95
|
+
: []
|
|
96
|
+
|
|
97
|
+
return [...stepIssues, ...transitionIssues]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function mappingTargetIssuesForApi(
|
|
101
|
+
definition: WorkflowDefinitionData,
|
|
102
|
+
prefix: Array<string | number> = [],
|
|
103
|
+
message = UNSAFE_MAPPING_TARGET_MESSAGE,
|
|
104
|
+
): DefinitionValidationIssue[] {
|
|
105
|
+
return collectUnsafeMappingTargetIssues(definition, message).map((issue) => ({
|
|
106
|
+
path: [
|
|
107
|
+
...prefix,
|
|
108
|
+
...issue.path.map((segment) => (typeof segment === 'number' ? segment : String(segment))),
|
|
109
|
+
],
|
|
110
|
+
code: 'custom',
|
|
111
|
+
message: issue.message,
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import * as stepHandler from './step-handler'
|
|
39
39
|
import { branchToken, mergeTokenContext, type ExecutionToken } from './execution-token'
|
|
40
40
|
import { WORKFLOW_ERROR_CONTEXT_KEY, buildErrorContextEntry } from './error-routing'
|
|
41
|
+
import { isSafeMappingTargetPath, safeGetNestedValue } from './safe-mapping-path'
|
|
41
42
|
|
|
42
43
|
export interface AdvanceBranchesResult {
|
|
43
44
|
outcome: 'joined' | 'waiting' | 'failed'
|
|
@@ -110,6 +111,17 @@ interface ParallelContext {
|
|
|
110
111
|
userId?: string
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
export function applyParallelJoinOutputMapping(
|
|
115
|
+
context: Record<string, unknown>,
|
|
116
|
+
outputMapping: Record<string, string>,
|
|
117
|
+
): void {
|
|
118
|
+
for (const [topKey, sourcePath] of Object.entries(outputMapping)) {
|
|
119
|
+
if (topKey === 'branches' || !isSafeMappingTargetPath(topKey)) continue
|
|
120
|
+
const value = safeGetNestedValue(context, sourcePath)
|
|
121
|
+
if (value !== undefined) context[topKey] = value
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
function normalizeWorkflowUserId(value: unknown): string | null {
|
|
114
126
|
if (typeof value !== 'string') return null
|
|
115
127
|
const trimmed = value.trim()
|
|
@@ -117,10 +129,6 @@ function normalizeWorkflowUserId(value: unknown): string | null {
|
|
|
117
129
|
return trimmed
|
|
118
130
|
}
|
|
119
131
|
|
|
120
|
-
function getNestedValue(obj: any, path: string): any {
|
|
121
|
-
return path.split('.').reduce((current, key) => (current == null ? undefined : current[key]), obj)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
132
|
/**
|
|
125
133
|
* Open a PARALLEL_FORK: create one ACTIVE branch per outgoing `auto` transition
|
|
126
134
|
* and mark the instance FORKED. Branch tokens start positioned ON the fork step
|
|
@@ -634,11 +642,7 @@ async function fireJoin(
|
|
|
634
642
|
// mapping cannot clobber the per-branch namespaces.
|
|
635
643
|
const outputMapping: Record<string, string> | undefined = joinStep?.config?.outputMapping
|
|
636
644
|
if (outputMapping) {
|
|
637
|
-
|
|
638
|
-
if (topKey === 'branches') continue
|
|
639
|
-
const value = getNestedValue(nextContext, sourcePath)
|
|
640
|
-
if (value !== undefined) nextContext[topKey] = value
|
|
641
|
-
}
|
|
645
|
+
applyParallelJoinOutputMapping(nextContext, outputMapping)
|
|
642
646
|
}
|
|
643
647
|
|
|
644
648
|
// Park the root token ON the JOIN step (not its successor) and let the
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const FORBIDDEN_MAPPING_PATH_SEGMENTS = new Set(['__proto__', 'constructor', 'prototype'])
|
|
2
|
+
|
|
3
|
+
export function isSafeMappingTargetPath(path: string): boolean {
|
|
4
|
+
return !path.split('.').some((segment) => FORBIDDEN_MAPPING_PATH_SEGMENTS.has(segment))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
8
|
+
return typeof value === 'object' && value !== null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function safeGetNestedValue(source: unknown, path: string): unknown {
|
|
12
|
+
return path.split('.').reduce<unknown>((current, segment) => {
|
|
13
|
+
if (!isObjectRecord(current) || !Object.hasOwn(current, segment)) return undefined
|
|
14
|
+
return current[segment]
|
|
15
|
+
}, source)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function cloneMappingContainer(value: unknown): Record<string, unknown> {
|
|
19
|
+
if (Array.isArray(value)) return [...value] as unknown as Record<string, unknown>
|
|
20
|
+
if (isObjectRecord(value)) return { ...value }
|
|
21
|
+
return {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function safeSetNestedValue(
|
|
25
|
+
target: Record<string, unknown>,
|
|
26
|
+
path: string,
|
|
27
|
+
value: unknown,
|
|
28
|
+
): boolean {
|
|
29
|
+
const segments = path.split('.')
|
|
30
|
+
if (!isSafeMappingTargetPath(path)) return false
|
|
31
|
+
|
|
32
|
+
const lastSegment = segments.pop()
|
|
33
|
+
if (lastSegment === undefined) return false
|
|
34
|
+
|
|
35
|
+
let current = target
|
|
36
|
+
for (const segment of segments) {
|
|
37
|
+
const existing = Object.hasOwn(current, segment) ? current[segment] : undefined
|
|
38
|
+
current[segment] = cloneMappingContainer(existing)
|
|
39
|
+
current = current[segment] as Record<string, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current[lastSegment] = value
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
@@ -54,6 +54,7 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
|
54
54
|
import { findWorkflowDefinition } from './find-definition'
|
|
55
55
|
import { validateAgainstPorts } from './port-contract'
|
|
56
56
|
import type { WorkflowIoContract } from '../data/validators'
|
|
57
|
+
import { safeGetNestedValue, safeSetNestedValue } from './safe-mapping-path'
|
|
57
58
|
|
|
58
59
|
const logger = createLogger('workflows')
|
|
59
60
|
|
|
@@ -1678,34 +1679,6 @@ async function logStepEvent(
|
|
|
1678
1679
|
return workflowEvent
|
|
1679
1680
|
}
|
|
1680
1681
|
|
|
1681
|
-
/**
|
|
1682
|
-
* Get nested value from object using dot notation
|
|
1683
|
-
*
|
|
1684
|
-
* @param obj - Source object
|
|
1685
|
-
* @param path - Dot-notation path (e.g., "user.email")
|
|
1686
|
-
* @returns Value at path or undefined
|
|
1687
|
-
*/
|
|
1688
|
-
function getNestedValue(obj: any, path: string): any {
|
|
1689
|
-
return path.split('.').reduce((current, key) => current?.[key], obj)
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
/**
|
|
1693
|
-
* Set nested value in object using dot notation
|
|
1694
|
-
*
|
|
1695
|
-
* @param obj - Target object
|
|
1696
|
-
* @param path - Dot-notation path (e.g., "user.email")
|
|
1697
|
-
* @param value - Value to set
|
|
1698
|
-
*/
|
|
1699
|
-
function setNestedValue(obj: any, path: string, value: any): void {
|
|
1700
|
-
const keys = path.split('.')
|
|
1701
|
-
const lastKey = keys.pop()!
|
|
1702
|
-
const target = keys.reduce((current, key) => {
|
|
1703
|
-
if (!(key in current)) current[key] = {}
|
|
1704
|
-
return current[key]
|
|
1705
|
-
}, obj)
|
|
1706
|
-
target[lastKey] = value
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
1682
|
/**
|
|
1710
1683
|
* Map data from source context using mapping configuration
|
|
1711
1684
|
*
|
|
@@ -1720,9 +1693,9 @@ function mapInputData(
|
|
|
1720
1693
|
const result: Record<string, any> = {}
|
|
1721
1694
|
|
|
1722
1695
|
for (const [targetKey, sourcePath] of Object.entries(mapping)) {
|
|
1723
|
-
const value =
|
|
1696
|
+
const value = safeGetNestedValue(sourceContext, sourcePath)
|
|
1724
1697
|
if (value !== undefined) {
|
|
1725
|
-
|
|
1698
|
+
safeSetNestedValue(result, targetKey, value)
|
|
1726
1699
|
}
|
|
1727
1700
|
}
|
|
1728
1701
|
|
|
@@ -1744,9 +1717,9 @@ function mapOutputData(
|
|
|
1744
1717
|
const result: Record<string, any> = {}
|
|
1745
1718
|
|
|
1746
1719
|
for (const [targetKey, sourcePath] of Object.entries(mapping)) {
|
|
1747
|
-
const value =
|
|
1720
|
+
const value = safeGetNestedValue(childContext, sourcePath)
|
|
1748
1721
|
if (value !== undefined) {
|
|
1749
|
-
|
|
1722
|
+
safeSetNestedValue(result, targetKey, value)
|
|
1750
1723
|
}
|
|
1751
1724
|
}
|
|
1752
1725
|
|