Browser-Resident Desktop Agents (next)
Browser Resident Desktop Agents (DAs) are an experimental feature added to FDC3 in 2.2. Limited aspects of their design may change in future versions and they are exempted from the FDC3 Standard's normal versioning and deprecation policies in order to facilitate any necessary change.
This document specifies the required behavior for Browser-Resident Desktop Agents (DA). Such agents allow FDC3 applications running directly in a browser to participate in FDC3 interop by way of a getAgent()
function that is provided by the @finos/fdc3
NPM module and a standardized communication protocol. This approach is in contrast to "Preload DAs" which run on technology that allows the FDC3 interface to be injected (such as Electron, WebView2 or a browser-extension based implementation).
This specification only applies to apps running in a browser and therefore assumes use of JavaScript/TypeScript and HTML APIs. Implementations in other languages such as .NET are not covered.
Along with this specification, a new general connection strategy has been established for FDC3 compliant web-applications: FDC3 compliant apps SHOULD make use of getAgent()
function provided by the @finos/fdc3
NPM module to retrieve their FDC3 interface (an instance of an implementation of the DesktopAgent
interface). Apps that follow these guidelines will be able to interop through either Browser-Resident DAs or Preload DAs without the inclusion of code or libraries specific to a particular Desktop Agent vendor or implementation. We refer to this concept as Write Once Run Anywhere (WORA).
Prior to FDC3 2.2, only Preload Desktop Agents were supported.
This document covers the requirements for implementors of Browser-Resident Desktop Agents. The getAgent()
function that applications use to gain access to an FDC3 interface is provided by the @finos/fdc3
NPM module. Many behavioral details of getAgent()
are purposefully omitted from this document in order to reduce the required scope of understanding. Please refer to the getAgent() specification in the FDC3 Web Connection Protocol for information on how the client side operates or supported platforms for details of how to access the Desktop Agent API in an application.
When referencing "DA" in the subsequent sections of this document we will hereafter always mean a "Browser-Resident Desktop Agent" - code that runs in a browser page (iframe or window) and which conforms to this specification.
Launching apps
As a prerequisite for launching an app via FDC3 in the browser, a DA must first exist as running code in a browser window (See failover functions for an exception to this rule), although that code MAY also connect to or rely on remotely hosted services. We will refer to this window as the "DA Window".
As the DA typically acts as a launcher for applications, it will often be the case that the DA window is related to the application window(s) in that it may have created the application window with window.open()
or have created an iframe and loaded the application URL into it. Hence, the DA window may be referred to as a 'parent' (window or frame) of the application frame and the relationship may be used to implement communication between the frames.
It is possible to have multiple DA Windows. For instance, a DA may propagate itself into new windows. Communication and coordination between DA Windows is an implementation detail and is not covered by this specification.
When an app runs getAgent()
, it checks for the existence of window.parent
, window.opener
and window.parent.opener
(and will continue up the chain of parent frames, e.g. window.parent.parent
, window.parent.parent.opener
until the reference to the next parent is equal to the current one (e.g. window.parent.parent === window.parent
indicating that the frame does not have a parent). getAgent()
will then send a standardized WCP1Hello
message to each parent window or frame reference via postMessage
in order to discover a DA.
Hence, apps may be launched:
- By creating iframes in a DA Window
- By calling
window.open
from a DA Window - By creating iframes in a window that was opened from a DA Window
and the Desktop Agent application will be found in a 'parent' of the application frame.
Responding to app instance connections - Web Connection Protocol (WCP)
Browser Resident DAs MUST call window.addEventListener("message", ...)
to receive incoming connection requests from apps, in the form of "WCP1Hello"
messages defined in the Web Connection Protocol.
Upon receiving an incoming "WCP1Hello"
the Desktop Agent MUST either:
Respond with a
WCP2LoadUrl
message (as defined in the Web Connection Protocol).- This message indicates that
getAgent()
should create an iframe, load the provided URL (for an adaptor to the Desktop Agent) into it and then restart the connection process by sending"WCP1Hello"
to the iframe.
dangerThe Content-Security-Policy directives frame-src, child-src and default-src can prevent iframes injected into an application from loading content. Wherever these are used in app implementations, please advise app implementors to include domains from which the adaptor content is served.
- This message indicates that
Create a
MessageChannel
with two entangledMessagePort
instances that will be used for further communication with the application.- Before returning one of
MessagePort
instances, the DA MUST set up event listeners to receive and process a"WCP4ValidateAppIdentity"
message from the application. - To deliver the
MessagePort
, the DA MUST respond to the event'ssource
window by responding with aWCP3Handshake
message (as defined in the Web Connection Protocol) and appendport2
from theMessageChannel
to the message.
- Before returning one of
All further communication is conducted over the MessageChannel
. The Desktop Agent should consider the newly created port to be inactive until a "WCP4ValidateAppIdentity"
message is received via the MessagePort
and successfully processed.
Validating app identity
The first message received by the Desktop Agent on MessagePort
port1
from an application via a MessageChannel
it created MUST be "WCP4ValidateAppIdentity"
. Once received and successfully processed, the Desktop Agent should respond with a WCP5ValidateAppIdentityResponse
message containing an appId
, instanceId
and instanceUuid
to identify the app and this specific instance and then enable processing of Desktop Agent Communication Protocol messages for this application. If validation fails, it should instead respond with WCP5ValidateAppIdentityFailedResponse
and close the MessageChannel
.
App identity is validated and an appId
assigned by matching the application to an AppD record already known to the Desktop Agent. This is achieved by matching identityUrl
(supplied by the application via the getAgent()
implementation) to the details.url
field of the known App Directory records.
As web applications may vary their URL during use, or serve multiple applications from the same origin (differentiated by path, search params and/or hash), care must be taken in matching URLs to AppD records. Two URLs are sent to the Desktop Agent in the "WCP4ValidateAppIdentity"
message:
identityUrl
: the URL to match to the App Directory record.actualUrl
: the current URL of the application, which MUST be captured automatically by thegetAgent()
implementation.
Applications may specify the identityUrl
value as an argument to getAgent()
. If not specified, the getAgent()
implementation MUST use the current URL of the application. The Desktop Agent MUST validate that the origin of the identityUrl
is the same as the origin of both the actualUrl
and the WCPValidateAppIdentity
message sent over the MessagePort
. The Desktop Agent MUST then match the URL to that of applications known to the Desktop Agent.
The actualUrl
field may be used for logging and debug purposes by the Desktop Agent and it differing from the identityUrl
indicates that the application provided an override via getAgent()
.
Owing to the fact that the different parts of a URL (origin, path, search parameters, anchor) are used differently by web applications, matching of the identityUrl
to known application URLs can be more complex than a simple string match. To allow application developers control the requirements of matching, Desktop Agents SHOULD consider a URL to match if all elements (origin, path, search parameters, anchor) present in the App Directory record's URL are present in the identityUrl
. As multiple App Directory records may match a given identity URL, Desktop Agents SHOULD look for the best match that meets the requirements.
For example, given an identity URL url
, and an array of App Directory records appDRecords
, a Desktop Agent MAY implement matching as follows:
/** Return the AppD record whose URL best matches the input URL or `null` if no
* match is found. To be considered a match all elements of the AppD URL
* (origin, path, searchParams and hash) MUST be found in the input URL.
* The best match is the AppD URL that contains the most elements from the
* input URL.
*/
let matchUrlToAppD = (url, appDRecords) => {
//parse the URL
const inputUrl = new URL(url);
//fn to trim trailing / from paths
const trimTrailingSlash = (path) => {
if (path.endsWith("/")) {
const out = path.slice(0, -1);
//return null if it was just a /
return out === "" ? null : out;
} else {
return path;
}
};
const matchScores = [];
//score AppD records based on the match of their URL to the input URL
// if any component of the AppD record URL is missing from input URL score 0,
// otherwise count the number of input elements matched.
appDRecords.map((record) => {
//record must contain a URL
if (!record.details?.url) { return; }
//parse record URL
const parsedUrl = new URL(record.details.url);
//origin of URL must match record
if (parsedUrl.origin !== inputUrl.origin) { return; }
let score = 1;
//path must match if present in AppD record - Path of "/" is ignored
const appDPath = trimTrailingSlash(parsedUrl.pathname);
if (appDPath) {
if (trimTrailingSlash(inputUrl.pathname) != appDPath) {
return;
} else {
score++;
}
}
//hash must match if present in AppD record
if (parsedUrl.hash) {
if (inputUrl.hash != parsedUrl.hash) {
return;
} else {
score++;
}
}
if (!!parsedUrl.search) {
//search params present in AppD record must be present in URL
const recordSearchKeys = parsedUrl.searchParams.keys().toArray();
for (let i=0; i<recordSearchKeys.length; i++) {
const key = recordSearchKeys[i];
if (inputUrl.searchParams.get(key) !== parsedUrl.searchParams.get(key)) {
return;
}
}
//count matched input URL search params for scoring
inputUrl.searchParams.keys().forEach((key) => {
if (parsedUrl.searchParams.get(key) === inputUrl.searchParams.get(key)) {
score++;
}
});
}
//everything present in the record was found in the inputUrl
matchScores.push([score, record]);
});
//return the best scoring match (ignore ties)
matchScores.sort((a,b) => b[0]-a[0]);
return matchScores[0][1] ?? null;
};
Validating instance identity
If the "WCP4ValidateAppIdentity"
request message contains instanceId
and instanceUuid
fields then the window may represent an app instance that has navigated or refreshed and requires reconnecting with the previously assigned instanceId
. The DA SHOULD reissue the same instanceId if the instanceUuid
, appId
, WindowProxy
object and origin provided all match what is already on record and return details via the WCP5ValidateAppIdentityResponse
message.
If an instanceId
and instanceUuid
are not provided or do not pass the checks defined above, a new instanceId
and instanceUuid
should be generated by the Desktop Agent and returned in the WCP5ValidateAppIdentityResponse
message. All generated instanceId
and instanceUuid
values should be retained by the DA (for the session or other appropriate period of time), along with a reference to the WindowProxy
(event.source
from the initial message received), as these can later be used for comparison to help determine if a new connection request is coming from a previously connected window which may or may not represent an existing app instance. If a previously connected window is reconnecting, any existing MessageChannel
instance created for it can be cleaned up.
Details of the received instanceId
and instanceUuid
are stored by the getAgent()
implementation in SessionStorage within the application window and are automatically reused when reconnecting to the Desktop Agent. SessionStorage is used as it is scoped to a particular window (rather than origin, as is the case with LocalStorage), allowing separate storage for each app instance.
Apps launched via a call to window.open()
from a window or app instance on the same origin can appear to be the original window. This is because browsers may clone SessionStorage for newly opened windows. When a child window calls getAgent()
with the same appId
, instanceId
and instanceUuid
as the parent window, it will appear to the DA that a navigation event occurred on the parent window. DAs therefore MUST also compare the WindowProxy
object that is used to establish each connection to differentiate such cloned instances. If the WindowProxy objects do not match, then a new instanceId
and instanceUuid
MUST be assigned.
For more details on the connection process, please see the documentation for the Web Connection Protocol.
Disconnects
DAs are responsible for tracking when app windows close or navigate, which is necessary to provide accurate responses to the findIntent
, findIntentsByContext
& findInstances
API calls, and to correctly resolve raised intents.
The HTML Standard specifies an onclose event handler on MessagePort
which would provide an ideal event-based solution for tracking the closing of app windows. However, this event is not currently implemented in Chrome/Chromium due to security concerns (it reveals the garbage collection activity of the process holding the other end of the pipe, see comment on whatwg/html/issues/1766, see also proposals to restrict when MessagePort's onclose event can fire).
Checking whether an application has closed may be achieved by a number of approaches:
- By checking the
closed
property of WindowProxy objects that were received via thesource
property of the originalWCP1Hello
message, or any subsequent message over theMessageChannel
.closed
will be true if the window or frame was closed or destroyed, or the window or frame has navigated cross-domain.- However, it should be noted that the
closed
will befalse
if the window has navigated same-domain, but is no longer an FDC3 app or has become a different FDC3 app. Hence, checking theclosed
property will not catch all cases. - If an equivalent
WindowProxy
object (WindowProxy
objects can be compared with==
and will be equivalent if they represent the same window) is received from a different application the DA should consider the original application using thatWindowProxy
to have closed.
- However, it should be noted that the
- By receiving a
WCP6Goodbye
message from the application when it is closing. ThegetAgent()
implementation automates the sending of this message via the HTML Standard's Page Life Cycle API. Specifically, thegetAgent()
implementation MUST attempt to detect windows closing by listening for thepagehide
event and considering a window to be closed if the event'spersisted
property isfalse
.- Note that the
pagehide
event may not fire if the window's render thread crashes or is closed while 'frozen'.
- Note that the
- By polling the application for responses via the
heartbeatEvent
andheartbeatAcknowledgement
messages provided in the Desktop Agent Communication Protocol. These message may be used for both periodic and on-demand polling by DA implementations. On-demand polling could, for example, be used to check that all instances returned in a findIntent response or displayed in an intent resolver are still alive.- Desktop Agents MAY determine their own timeout, or support configuration, to be used for considering an application to have closed as this may be affected by the implementation details of app and DAs.
Finally, Desktop Agents SHOULD retain instance details for applications that have closed as they may appear to close during navigation events, or may navigate away and then navigate back. By retaining the instance data (instanceId
, instanceUuid
and WindowProxy
) the same instance identity can be maintained or reissued. There is no standard length of time that such details should be retained, hence, Desktop Agents MAY determine for themselves how long to retain instance details for closed instances.
Responding to app communications with Desktop Agent Communication Protocol (DACP)
After validating an application's identity and any instance identity to be reused, the Desktop Agent is ready to support communication with the application. This is achieved via the Desktop Agent Communication Protocol (DACP) over the MessageChannel
established in the previous steps.
Implementing DAs in hidden iframes
As described above, DA providers can leverage hidden iframes to establish a communication mechanism that is independent of a parent window or frame. This approach allows apps to reconnect to a DA even when the parent window or frame has closed, or to connect a DA they've started themselves via a failover
function.
The hidden iframe URL can be provided in two ways:
- By a parent window or frame - This allows DAs to handle communication via a hidden iframe that loads a known URL. The main benefit of this approach is that it can allow a system to continue to operate even if the parent window or frame is closed.
- By a
failover
function - When no parent DA can be found (such as when a tab is opened directly by an end user) then a failover function can create a hidden iframe and return a reference to it (aWindowProxy
) that is used to initiate communication via the WCP in the same way as we do with a parent window or frame. Alternatively, aDesktopAgent
implementation may be loaded directly and returned from thefailover
function, whichgetAgent()
will pass-through.
Channel Selector and Intent Resolver User Interfaces
Channel Selector and Intent Resolver user-interfaces are normally provided by Desktop Agents to applications. However, when running in a web browser, a DA may not have the ability to present a channel selector in a window that has been opened with window.open()
, and it may be challenging to display a secondary window over the application when needed (due to pop-up blocking and user preferences).
The getAgent()
implementation can facilitate the injection and management of iframes in an application window. An app may provide the optional channelSelector
and intentResolver
parameters to the getAgent()
to indicate whether or not they need these interfaces. For example, the apps may not raise intents. Some apps may also resolve intents internally by leveraging the Desktop Agent's findIntent
or findIntentsForContext
API functions. In these situations, the apps won't need a DA-provided interface. Once an app calls getAgent()
, the parameters that the app provides are forwarded onto the Desktop Agent in the WCP1Hello
connection message.
Desktop Agents MAY implement their own user interfaces for channel selection and intent resolution. The URL for each interface may be returned in the channelSelectorUrl
and intentResolverUrl
properties of the payload of the WCP3Handshake
message sent by the DA during the connection sequence. Alternatively, if the Desktop Agent is able to provide these user interfaces by other means (for example DAs that render applications in iframes within a window they control may use other iframes to render these UIs) or if they app indicated that it did not need them then channelSelectorUrl
and intentResolverUrl
MAY be set to false
. Finally, channelSelectorUrl
and intentResolverUrl
MAY be set to true
to indicate that getAgent()
should use the default reference implementations of these UIs provided via the https://fdc3.finsos.org website.
The Content-Security-Policy directives frame-src, child-src and default-src can prevent iframes injected into an application from loading content. Where these are used in app implementations, please advise app implementors to include domains from which the UI implementations are served (including fdc3.finos.org if you are working with the reference Intent Resolver and Channel Selector UIs).
User interface iframes are initially injected into the application window with CSS that prevents their display:
{
width: "0";
height: "0";
position: "fixed";
}
and are always displayed with position: "fixed"
so that they are not part of the document flow.
Implementations of the UIs may then indicate a limited set of CSS to apply to their frame in the initial Fdc3UserInterfaceHello
message (when the width and height will be removed if not explicitly set in that message), and later adjust that via Fdc3UserInterfaceRestyle
. See the Controlling injected User Interfaces section in the DACP specification for more details.
Communication between the DesktopAgentProxy
and the iframes it injects is achieved via a similar mechanism to that used for communication between an app and the Desktop Agent: a MessageChannel
is established between the app and iframe, via a postMessage
sent from the iframe (Fdc3UserInterfaceHello
) and responded to by the DesktopAgentProxy
in the app's window (Fdc3UserInterfaceHandshake
), with a MessagePort
from a MessageChannel
appended.
A further set of messages are provided for working with the injected user interfaces over their MessageChannel
as part of the DACP, these are: Fdc3UserInterfaceRestyle
, Fdc3UserInterfaceDrag
, Fdc3UserInterfaceChannels
, Fdc3UserInterfaceChannelSelected
, Fdc3UserInterfaceResolve
and Fdc3UserInterfaceResolveAction
.
See the Desktop Agent Communication Protocol (DACP) for more details.