From 1860489df98a65aa14405c6f2449c04e6ce618c3 Mon Sep 17 00:00:00 2001 From: Karissa Jacobsen Date: Tue, 14 Jul 2026 11:47:20 -0700 Subject: [PATCH] Update IAM SDK and detect iframe CSP blocking for IDV workflows Upgrades @docusign/iam-sdk to beta.8, renames maestro references to workflow builder, and adds detection for workflows that cannot be embedded (e.g. IDV). Uses ReportingObserver to catch CSP frame-ancestors violations and automatically opens blocked workflows in a new tab. Co-Authored-By: Claude Opus 4.6 (1M context) --- client/src/api/index.js | 4 + client/src/assets/text.json | 3 +- .../WorkflowTriggerResult.jsx | 2 +- .../components/TriggerForm/TriggerForm.jsx | 24 ++-- .../TriggerWorkflowForm.jsx | 117 +++++++++++++++--- .../TriggerWorkflowForm.module.css | 21 +++- package.json | 2 +- server/constants.js | 4 +- server/controllers/workflowsController.js | 16 +++ server/package-lock.json | 13 +- server/package.json | 2 +- server/routes/workflowsRouter.js | 1 + server/server.js | 2 +- server/services/workflowsService.js | 18 ++- 14 files changed, 173 insertions(+), 56 deletions(-) diff --git a/client/src/api/index.js b/client/src/api/index.js index dcf7ca7..8f9a889 100644 --- a/client/src/api/index.js +++ b/client/src/api/index.js @@ -88,5 +88,9 @@ export const api = Object.freeze({ return error.response; } }, + getWorkflowInstance: async (workflowId, instanceId) => { + const response = await instance.get(`/workflows/${workflowId}/instances/${instanceId}`); + return response; + }, }, }); diff --git a/client/src/assets/text.json b/client/src/assets/text.json index 1aab2ac..ecf0a07 100644 --- a/client/src/assets/text.json +++ b/client/src/assets/text.json @@ -93,7 +93,8 @@ "triggerNewWorkflow": "Trigger new workflow ->", "getStarted": "Get started", "moreInfo": "More info", - "backHome": "← Back to workflows" + "backHome": "← Back to workflows", + "done": "Done" }, "links": { "github": "https://github.com/docusign/sample-app-workflows-node", diff --git a/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx b/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx index 0fd7c83..ab38842 100644 --- a/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx +++ b/client/src/components/Popups/WorkflowTriggerResult/WorkflowTriggerResult.jsx @@ -22,7 +22,7 @@ const WorkflowTriggerResult = ({ workflowInstanceUrl }) => {

{textContent.popups.workflowTriggered.title}


- See Embedded workflow instance recommendations and restrictions for more information. + See Embedded workflow instance recommendations and restrictions for more information.

{textContent.buttons.continue} diff --git a/client/src/components/TriggerForm/TriggerForm.jsx b/client/src/components/TriggerForm/TriggerForm.jsx index 6ee63be..dac2811 100644 --- a/client/src/components/TriggerForm/TriggerForm.jsx +++ b/client/src/components/TriggerForm/TriggerForm.jsx @@ -103,27 +103,17 @@ const TriggerForm = ({ workflowId, templateType }) => { } const { data: triggeredWorkflow } = await api.workflows.triggerWorkflow(workflowId, templateType, body); - setWorkflowInstanceUrl(triggeredWorkflow.instanceUrl); - - if (triggeredWorkflow.instanceUrl !== undefined) { - navigate(`${ROUTE.TRIGGERFORM}/${workflowId}?type=${templateType}&triggerUrl=${encodeURIComponent(triggeredWorkflow.instanceUrl)}`) - } if (triggeredWorkflow === WorkflowTriggerResponse.TRIGGER_ISSUE) { - // Update workflowDefinitions. "...workflow" creates new workflow-object to avoid mutation in redux - const updatedWorkflowDefinitions = workflows.map(w => { - if (w.id !== workflowId) return { ...w }; - - return { - ...w, - instanceId: triggeredWorkflow.instanceId, - isTriggered: true, - }; - }); - - dispatch(updateWorkflowDefinitions(updatedWorkflowDefinitions)); setDataSending(false); dispatch(openPopupWindow()); + return; + } + + setWorkflowInstanceUrl(triggeredWorkflow.instanceUrl); + + if (triggeredWorkflow.instanceUrl !== undefined) { + navigate(`${ROUTE.TRIGGERFORM}/${workflowId}?type=${templateType}&triggerUrl=${encodeURIComponent(triggeredWorkflow.instanceUrl)}`) } }; diff --git a/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx b/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx index bc28658..1138169 100644 --- a/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx +++ b/client/src/pages/TriggerWorkflowForm/TriggerWorkflowForm.jsx @@ -1,4 +1,5 @@ -import { useParams, useLocation } from 'react-router-dom'; +import { useState, useRef, useEffect, useCallback } from 'react'; +import { useParams, useLocation, useNavigate } from 'react-router-dom'; import styles from './TriggerWorkflowForm.module.css'; import Header from '../../components/Header/Header.jsx'; import Footer from '../../components/Footer/Footer.jsx'; @@ -8,32 +9,107 @@ import WorkflowDescription from '../../components/WorkflowDescription/WorkflowDe import TriggerBehindTheScenes from '../../components/WorkflowDescription/BehindTheScenes/TriggerBehindTheScenes.jsx'; import TriggerForm from '../../components/TriggerForm/TriggerForm.jsx'; import { ROUTE } from '../../constants.js'; +import { buttons } from '../../assets/text.json'; const TriggerWorkflowForm = () => { const { workflowId } = useParams(); + const navigate = useNavigate(); + const [embeddingFailed, setEmbeddingFailed] = useState(false); + const hasOpenedNewTab = useRef(false); + const childWindowRef = useRef(null); const location = useLocation(); const searchParams = new URLSearchParams(location.search); const type = searchParams.get('type'); const triggerUrl = searchParams.get('triggerUrl'); - const triggerUrlPattern = /^https:\/\/(?!.*javascript)[^()]+$/i; - -function isValidTriggerUrl(url) { - try { - const decoded = decodeURIComponent(url); - const parsedUrl = new URL(decoded); - // Only allow https and the exact hostname - return ( - parsedUrl.protocol === 'https:' && - parsedUrl.hostname === 'apps-d.docusign.com' - ); - } catch { - return false; + function isValidTriggerUrl(url) { + try { + const decoded = decodeURIComponent(url); + const parsedUrl = new URL(decoded); + return ( + parsedUrl.protocol === 'https:' && + parsedUrl.hostname === 'apps-d.docusign.com' + ); + } catch { + return false; + } } -} - + + const handleIframeLoad = useCallback((e) => { + try { + const href = e.target.contentWindow.location.href; + if (href === 'about:blank') { + setEmbeddingFailed(true); + } + } catch { + // Cross-origin SecurityError — handled by ReportingObserver for CSP blocks + } + }, []); + + // Detect CSP frame-ancestors violations via ReportingObserver + useEffect(() => { + if (!triggerUrl) return; + + let observer; + if (typeof ReportingObserver !== 'undefined') { + observer = new ReportingObserver((reports) => { + const blocked = reports.some( + (report) => report.body && report.body.effectiveDirective === 'frame-ancestors' + ); + if (blocked) { + setEmbeddingFailed(true); + } + }, { types: ['csp-violation'], buffered: false }); + observer.observe(); + } + + return () => { + if (observer) observer.disconnect(); + }; + }, [triggerUrl]); + + // Open new tab when embedding fails + useEffect(() => { + if (embeddingFailed && triggerUrl && !hasOpenedNewTab.current) { + hasOpenedNewTab.current = true; + childWindowRef.current = window.open(triggerUrl, '_blank'); + } + }, [embeddingFailed, triggerUrl]); + if (triggerUrl !== null && isValidTriggerUrl(triggerUrl)) { + // Embedding failed — workflow opened in new tab + if (embeddingFailed) { + return ( +
+
+
+ } + backRoute={ROUTE.TRIGGER} + /> +

This workflow cannot be embedded and has been opened in a new tab.

+

Complete your tasks in the new tab, then click Done to return.

+
+ Open workflow in new tab + + +
+
+
+ ); + } + + // Iframe embedding return (
@@ -44,10 +120,11 @@ function isValidTriggerUrl(url) { backRoute={ROUTE.TRIGGER} /> -
- -
+ +