UNPKG

@connectifi/agent-web

Version:

A simple web implementation of a connectifi agent

1 lines 188 kB
{"version":3,"sources":["../src/types.ts","../src/fab/index.css","../src/fab/components/view/logo.ts","../src/fab/components/common/html.ts","../src/fab/components/common/button/reconnect.ts","../src/fab/components/view/directory.ts","../src/fab/components/channel-picker/toolbar/button.ts","../src/fab/components/channel-picker/toolbar/index.ts","../src/fab/components/common/image.ts","../src/fab/components/channel-picker/channels.ts","../src/fab/components/channel-picker/index.ts","../src/fab/components/header/logo.ts","../src/fab/components/header/index.ts","../src/fab/components/resolver/header/dismiss.ts","../src/fab/components/resolver/header/logo.ts","../src/fab/components/common/title.ts","../src/fab/components/resolver/header/title.ts","../src/fab/components/resolver/header/index.ts","../src/fab/components/resolver/app-row/icon-container.ts","../src/fab/components/resolver/app-row/info/agent.ts","../src/fab/components/resolver/app-row/info/index.ts","../src/fab/components/resolver/app-row/title.ts","../src/fab/components/resolver/app-row/index.ts","../src/fab/components/resolver/group.ts","../src/fab/components/resolver/intent-row.ts","../src/fab/components/resolver/list.ts","../src/fab/components/resolver/index.ts","../src/fab/components/resolver-background.ts","../src/fab/components/toast/message.ts","../src/fab/components/toast/index.ts","../src/fab/components/container.ts","../src/fab/components/common/button/login.ts","../src/fab/window/logout.ts","../src/fab/authenticator.ts","../src/fab/components/common/button/open.ts","../src/fab/storage-access.ts","../src/fab/opener.ts","../src/fab/index.ts","../src/main.ts","../src/agent/channel.ts","../src/agent/api.ts","../src/agent/index.ts","../src/common/promise.ts","../../common/src/interop/subscriptions.ts","../src/agent/subscription.ts","../src/common/iframe.ts","../src/common/port.ts","../src/agent/iframe.ts","../src/agent/tunnel.ts","../../common/src/interop/envelope.ts","../src/agent/websocket.ts","../src/common/event-emitter.ts","../src/common/logger.ts","../../common/src/directories.ts","../src/agent/window-listener.ts","../src/agent/storage-access.ts","../src/common/app-id.ts","../src/agent/authentication.ts","../src/common/element.ts","../../common/src/types.ts"],"sourcesContent":["import type {\n DownEventData,\n DownEventTopic,\n DownResponseData,\n DownResponseTopic,\n} from '@/common/interop/down-messages';\nimport type { ConnectifiAppMetadata } from '@/common/types';\nimport type {\n AppMetadata,\n Context,\n IntentMetadata,\n DesktopAgent,\n AppIntent,\n} from '@finos/fdc3';\nimport { AuthTargetType } from './agent/authentication';\n\nexport interface DirectoryIntent extends IntentMetadata {\n contexts?: Array<string>;\n}\n\nexport enum ResolutionType {\n Intent = 'intent-resolver',\n Context = 'context-resolver',\n}\n\nexport type DownEventHandler<TTopic extends DownEventTopic> = (\n data: DownEventData[TTopic],\n) => void;\nexport type DownEventHandlers = {\n [K in keyof DownEventData]: DownEventHandler<K>;\n};\nexport type DownResponseHandler<TTopic extends DownResponseTopic> = (\n data: DownResponseData[TTopic],\n) => void;\nexport type DownResponseHandlerMap = {\n [K in keyof DownResponseData]: DownResponseHandler<K>;\n};\n\n/**\n * represents an FDC3 intent with a collection of related App Instances\n **/\n\nexport type AppIntentResult = {\n intent: IntentMetadata;\n apps: ConnectifiAppMetadata[];\n};\n\nexport interface IntentResolutionMessage {\n resolutionType: ResolutionType;\n context: Context;\n data: AppIntentResult | AppIntentResult[];\n bridgeData?: AppIntent | AppIntent[];\n resultPendingId?: string;\n}\n\nexport interface ResolveCallbackProps {\n selected: ConnectifiAppMetadata;\n intent: string;\n context: Context;\n bridge?: boolean;\n metadata?: any;\n}\n\nexport type ResolveCallback = (props: ResolveCallbackProps) => void;\n\nexport type CloseCallback = () => void;\n\nexport type AuthenticateConfig = {\n /**\n * Prompts the user to authenticate.\n *\n * By default, the current window will be redirected to login page.\n * If redirect style is new window instead, the login page will be shown in a new browser window.\n *\n * Returns a promise that resolves when the user has successfully authenticated.\n */\n authenticate: (redirectStyle?: RedirectStyle) => Promise<void>;\n getAuthenticationUrl: (targetType?: AuthTargetType) => string;\n} & DirectoryProps;\n\n/**\n * interface for any Agent\n * @deprecated\n */\nexport type FDC3Agent = {\n connect: () => void;\n} & Pick<AuthenticateConfig, 'authenticate'>;\n\nexport type RequestStorageAccessConfig = {\n /**\n * A button for users to request storage access.\n *\n * This button is embedded within an iframe on the interop domain, as modern browsers require user interaction\n * from the domain where third-party cookies reside. If a storage access request is initiated outside the interop\n * domain, the browser will deny it. Custom implementations should append this button to a DOM element within their UI.\n */\n button: HTMLIFrameElement;\n\n /**\n * Waits for the user to grant or deny storage access.\n *\n * This method should be called after the storage access button has been appended to the UI.\n *\n * Returns a promise that resolves to a boolean, indicating whether the user granted or denied storage access.\n */\n waitForStorageAccess: () => Promise<boolean>;\n\n /**\n * Requests user consent for third-party cookie usage.\n *\n * This is necessary only if a storage access request fails, suggesting that third-party cookies may be blocked.\n * By default, the current window is redirected to the consent page. If the redirect is set to new window,\n * the consent page will open in a new browser window.\n *\n * Returns a promise that resolves to a boolean, indicating whether the user granted or denied consent.\n */\n requestConsent: (redirectStyle?: RedirectStyle) => Promise<boolean>;\n};\n\nexport interface AgentHandlers {\n /**\n * fdc3 api is ready for use\n * @param fdc3\n * @returns\n */\n onFDC3Ready?: (fdc3: DesktopAgent) => void;\n /**\n * directory session is established\n * @param directory directory properties\n * @param username if directory requires signin, the username is passed as well\n * @returns\n */\n onSessionStarted?: (directory: DirectoryProps, username?: string) => void;\n /**\n * session error - timeout connecting to service, bad app id, etc.\n * @param errorMessage\n * @returns\n */\n onSessionError?: (errorMessage: string) => void;\n\n /**\n * signin/login is complete\n * - applicable ONLY to directories with strict interop strategy\n * @param username\n * @returns\n */\n onSignedIn?: () => void;\n /**\n * signout/logout is complete\n * - applicable ONLY to directories with strict interop strategy\n * @returns\n */\n onSignedOut?: () => void;\n\n /**\n * channel joined - useful for setting any channel indicators\n * @param channelId id of the channel that was joined\n * @returns\n */\n onChannelJoined?: (channelId: string) => void;\n /**\n * channel left - useful for resetting any channel indicators\n * @returns\n */\n onChannelLeft?: () => void;\n /**\n * interop service is connected (useful for clearing any disconnected state/indicators)\n * @param initialConnect indicates if this is the initial connection to the service\n * @returns\n */\n onConnected?: (initialConnect: boolean) => void;\n /**\n * interop service is disconnected, it will attempt to reconnect in \"nextConnect\" seconds.\n * useful for setting indicators/state like \"disconnected, reconnecting in ...\"\n * @param nextConnect next reconnect attempt in seconds\n * @returns\n */\n onDisconnected?: (nextConnect?: number) => void;\n /**\n * \"working\" or \"busy\" state of the agent\n * typically used for a \"spinner\" or \"loading\" indicator/effect\n * @param workInProgress\n * @returns\n */\n onWorkingChanged?: (workInProgress: boolean) => void;\n\n /**\n * Called when agent is requesting the user to authenticate.\n * @param directory the directory the user is authenticating against.\n */\n onSessionAuthRequired?: (directory: DirectoryProps) => void;\n\n /**\n * for implementing a custom intent resolver\n * @param message\n * @param callback\n * @param closeCallback\n * @returns\n */\n handleIntentResolution?: (\n message: IntentResolutionMessage,\n callback: ResolveCallback,\n closeCallback: CloseCallback,\n ) => void;\n\n /**\n * for overriding the default \"open\" implementation (window.open)\n * @param message\n * @returns\n */\n handleOpen?: (message: ConnectifiOpenMessage) => void;\n\n /**\n * for overriding the default FAB click operation\n * @param message\n * @returns\n */\n handleFABClicked?: (event: MouseEvent) => void;\n\n /**\n * Handles authenticating the user for directories that require authentication.\n * Applicable ONLY to directories with strict interop strategy.\n * @param directory directory properties\n * @returns promise that resolves when authentication of the user is successful.\n */\n handleAuthenticate?: (config: AuthenticateConfig) => Promise<void>;\n\n /**\n * Overrides the default behavior for requesting storage access.\n */\n handleRequestStorageAccess?: (\n config: RequestStorageAccessConfig,\n ) => Promise<void>;\n}\n\nexport interface AgentConfig extends AgentHandlers {\n props?: FabProps;\n headless?: boolean;\n logLevel?: LogLevel;\n bridgeGlobal?: boolean;\n // for overriding the default logging implementation (console.log)\n logger?: (...params: any) => void;\n}\n\nexport interface AgentConfigInfo {\n bridgeGlobal?: boolean;\n headless?: boolean;\n logLevel?: LogLevel;\n props?: FabProps;\n activeHandlers: AgentConfigHandlerInfo;\n}\n\nexport type AgentConfigHandlerInfo = { [P in keyof AgentHandlers]?: true };\n\nexport type LogLevel = 'debug' | 'info' | 'silent';\nexport interface Logger {\n info: (...params: any) => void;\n debug: (...params: any) => void;\n error: (...params: any) => void;\n}\n\nexport type ValidPositions = 'tl' | 'ml' | 'bl' | 'tr' | 'mr' | 'br';\n\nexport type RedirectStyle = 'sameWindow' | 'newWindow';\n\nexport interface FabProps {\n logoSrc?: string;\n position?: ValidPositions;\n loginStyle?: RedirectStyle;\n}\n\nexport interface DirectoryProps {\n name: string;\n interopStrategy: 'open' | 'openauth' | 'app' | 'strict';\n icon?: string;\n}\n\nexport type FabToastType =\n | 'success'\n | 'error'\n | 'login'\n | 'reconnect'\n | 'session'\n | 'timeout';\n\n/**\n * interface for UI component binding to an Agent\n */\nexport interface AgentGUI {\n agent: FDC3Agent | undefined;\n connected: boolean;\n logoSrc?: string;\n bind: (agent: FDC3Agent) => void;\n\n // allow override of the default click action\n handleClick?: (event: MouseEvent) => void;\n\n onConnected: (initialConnect: boolean) => void;\n onDisconnected: (nextConnect?: number) => void;\n onFDC3Ready: (fdc3: DesktopAgent) => void;\n onSessionStarted: (directory: DirectoryProps, username?: string) => void;\n onSessionError: (errorMessage: string) => void;\n onSignedOut: () => void;\n onWorkingChanged: (working: boolean) => void;\n onChannelJoined: (channelId: string) => void;\n onChannelLeft: () => void;\n handleIntentResolution: (\n message: IntentResolutionMessage,\n callback: ResolveCallback,\n closeCallback: CloseCallback,\n ) => void;\n}\n\nexport interface ConnectifiMessageData {\n target?: AppMetadata | string | undefined;\n context?: Context;\n intent?: string;\n id?: string;\n contextType?: string;\n channelId?: string;\n channel?: string;\n name?: string | AppMetadata;\n appId?: string;\n src?: string;\n title?: string;\n stateId?: string;\n url?: string;\n pendingId?: string;\n resultPendingId?: string;\n}\n\n/**\n * todo: change label \"fdc3Token\" to \"token\"\n */\nexport interface ConnectifiMessage {\n topic: string;\n appId: string;\n eventId: string;\n data:\n | ConnectifiMessageData\n | AppIntentResult\n | AppIntentResult[]\n | JoinChannelResult;\n resultPendingId?: string;\n context?: Context;\n}\n\nexport interface JoinChannelResult {\n channel: string;\n}\n\nexport interface TopicDataPair {\n topic: string;\n data?: ConnectifiMessageData;\n}\n\nexport interface AgentState {\n connected?: boolean;\n owner?: string | null;\n channel?: string | undefined;\n}\n\nexport enum ConnectionError {\n NoConnectionAvailable = 'NoConnectionAvailable',\n}\n\nexport interface ConnectifiOpenMessage {\n name?: string;\n appId?: string;\n url?: string;\n}\n","@keyframes throbber{0%{opacity:0}50%{opacity:.5}to{opacity:0}}@keyframes fadeInUp{0%{opacity:0;transform:translate3d(0,100%,0)}to{opacity:1;transform:translateZ(0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;transform:translate3d(0,100%,0)}}@keyframes zoomIn{0%{opacity:0;transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;transform:scale3d(.3,.3,.3)}to{opacity:0}}*{color:var(--text-color);font-family:Arial,Helvetica,sans-serif;line-height:1rem}img{height:1.2rem;width:1.2rem}button{align-items:center;background:transparent;border:none;border-radius:50%;display:flex;filter:var(--drop-shadow-sm);height:1.8rem;justify-content:center;margin:.2rem;padding:0;transition:var(--ease);width:1.8rem}button:hover{transform:scale(1.1)}button img{height:.7rem;width:.7rem}button:hover{transform:var(--grow)}.container{--text-color:#111;--shadow:#27272ac2;--drop-shadow:drop-shadow(5px 5px 10px var(--shadow));--drop-shadow-sm:drop-shadow(3px 3px 5px var(--shadow));--box-shadow:rgba(50,50,93,.2) 0px 13px 27px -5px,rgba(0,0,0,.25) 0px 8px 16px -8px;--border:1px solid var(--secondary-variant);--ease:all 0.2s ease-in-out;--grow:scale(1.1);--grow-sm:scale(1.04);--radius:0.25rem;--background:#fff;--primary:#000;--primary-variant:#27272a;--secondary:#aaa;--secondary-variant:#d0d4db;font-size:.8rem}.fab{align-items:center;display:flex;flex-direction:column;position:fixed;z-index:99999}.fab.bl,.fab.bl .picker,.fab.br,.fab.br .picker{flex-direction:column-reverse}.fab.br .inner,.fab.mr .inner,.fab.tr .inner{align-items:end}.fabHeader,.inner{display:flex;flex-direction:column;height:3.5rem;justify-content:center;width:3.5rem}.fabHeader{align-items:center;background-color:var(--secondary);border-radius:50%;filter:var(--drop-shadow);margin:0;padding:0;transition:background-color .25s linear,var(--ease)}.fabHeader:hover{transform:var(--grow-sm)}.fab.tl{left:1rem;top:2rem}.fab.ml{left:1rem;top:calc(50vh - 1.8rem)}.fab.bl{bottom:2rem;left:1rem}.fab.tr{right:1rem;top:2rem}.fab.mr{right:1rem;top:calc(50vh - 1.8rem)}.fab.br{bottom:1.75rem;right:1rem}.fabHeader img{border-radius:50%;height:3rem;width:3rem}.fabHeader .mask{background-color:#fff;border-radius:50%;height:100%;opacity:0;position:absolute;transition:transform 2s ease-in-out;width:100%}.fabHeader .indicator{background-color:gray;border:1px solid #fff;border-radius:50%;bottom:0;height:.8rem;position:absolute;right:0;width:.8rem}.fabHeader.connected .indicator{background-color:#90ee90}.fabHeader.busy .mask{animation:throbber 2s infinite}.fabHeader.connected{background-color:var(--primary)}.toast{align-items:center;background-color:var(--background);border:var(--border);border-radius:var(--radius);box-shadow:var(--box-shadow);display:flex;flex-flow:row;flex-wrap:nowrap;justify-content:left;margin-left:70px;margin-right:70px;max-width:25rem;min-width:12rem;opacity:.9;padding:.4rem;position:fixed;visibility:hidden}.toast.show{animation-duration:.3s;animation-name:fadeInUp;visibility:visible}.toast.hide{animation-duration:.26s;animation-name:fadeOutDown}.toast>img{height:1.5rem;margin-right:.4rem;width:1.5rem}.toast>button>img{height:1.2rem;width:1.2rem}.toast .title{font-weight:600}.toast .message{font-size:.7rem;margin-right:.5rem;overflow:hidden;text-overflow:ellipsis}.picker{align-items:center;color:var(--text-color);display:none;flex-direction:column;justify-content:center;padding:.2rem 0}.picker.show{display:flex}.picker button>img{filter:invert(1)}.picker.show button{animation-duration:.4s;animation-name:zoomIn;opacity:1;visibility:visible}.picker button{animation-duration:.5s;animation-name:zoomOut;opacity:0;visibility:hidden}.toolbar{align-items:center;display:flex;flex-flow:row}.toolbar>button{background-color:var(--primary-variant)}.toolbar .title{margin-left:.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:calc(100vw - 3rem)}.channels{display:none;flex-wrap:wrap;justify-content:center}.channels.show{display:flex}.resolver{background-color:var(--background);border:var(--border);border-radius:.6rem .6rem 0 0;box-shadow:var(--box-shadow);display:none;height:24rem;left:50%;margin-left:-15rem;margin-top:-12rem;opacity:0;position:fixed;top:50%;width:30rem;z-index:99999}.resolver.open{display:block;opacity:1;transition-duration:.3s;transition-property:opacity}.resolver .header{align-items:center;background-color:var(--primary-variant);border-top-left-radius:.6rem;border-top-right-radius:.6rem;display:flex;flex-direction:row;font-size:1rem;padding:.6rem 0 .3rem;width:100%}.resolver .header *{color:#fff}.resolver .header .logo img{height:1.5rem;margin:2px .5em 0;width:1.5rem}.resolver .header .title{flex-grow:1;overflow:hidden;white-space:wrap}.resolver .header .title:after{background:linear-gradient(90deg,transparent,transparent 85%,var(--primary-variant) 90%);border-top-right-radius:.6em;content:\"\";display:block;height:2em;left:0;pointer-events:none;position:absolute;top:0;width:100%}.resolver .header .title span{display:inline-block;margin:0 0 4px}.resolver .header .dismissContainer{align-items:start;display:flex}.resolver .header .dismiss{margin:0 .5em;z-index:1}.resolver .header .dismiss>div{background-color:transparent;border-radius:50%;cursor:pointer;height:1.2em;text-align:center;width:1.2em}.resolver .header .dismiss>div>span{position:relative;top:2px}.resolver .header .dismiss div:hover{background-color:#555}.resolver .list{height:20.5rem;overflow:scroll}.resolver .list .item{align-items:center;cursor:pointer;display:flex;flex-direction:row;flex-wrap:nowrap;height:2rem;overflow:hidden;padding:.3rem .3rem .3rem .6rem}.resolver .list .item:hover{background-color:#e7e7e7}.resolver .list .group{border-bottom:.5px solid #bbb;border-top:.5px solid #bbb;padding:.25rem .75rem}.resolver .list .item .icon-container{display:flex;flex-direction:row;flex-wrap:nowrap}.resolver .list .item img{background-repeat:no-repeat;background-size:contain;margin-right:.4rem}.resolver .list .item .title{flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resolver .list .item .info{display:flex;flex-direction:row;flex-wrap:nowrap;padding-right:.4rem}.resolver .list .item .info>div{background-repeat:no-repeat;background-size:contain;height:1.2rem;margin-left:.4rem;padding:.3em;text-align:center;width:1.2rem}.resolver .list .item .info .agent>div{margin-top:.1em}.resolver .list .item .info .more{background-color:#d3d3d3;height:1.2rem;padding-top:.5em;width:1.2rem}.resolver .list .item .info .more>span{border-color:#aaa;border-style:dashed;border-width:0 2px}.resolver .list .intentRow{background-color:#ddd;border-top:1px solid #bbb;padding-bottom:.6rem;padding-top:.6rem}.resolver .list .intentTitle{font-size:1rem;padding:0 .5em}.resolverBackground{background-color:#111;display:none;height:100%;left:0;opacity:.2;position:absolute;top:0;width:100%;z-index:9999}.resolverBackground.open{display:block}@media (max-width:600px){.resolver{margin-left:calc(-50vw - -1rem);width:calc(100vw - 2rem)}}@media (max-height:480px){.resolver{height:calc(100vh - 2rem);margin-top:calc(-50vh - -1rem)}.resolver .list{height:calc(100vh - 4.7rem)}}","/**\n * A composition of multiple HTML image elements that all should render the same current directory logo.\n */\nexport class LogoView {\n constructor(private images: HTMLImageElement[]) {}\n\n updateImageSource(imageSource: string) {\n this.images.forEach((image) => {\n image.src = imageSource;\n });\n }\n}\n","class HTMLElementDecorator<T extends keyof HTMLElementTagNameMap> {\n element: HTMLElementTagNameMap[T];\n\n constructor(type: T) {\n this.element = document.createElement(type);\n }\n}\n\nexport class HTMLDivDecorator extends HTMLElementDecorator<'div'> {\n constructor() {\n super('div');\n }\n}\n\nexport class HTMLButtonDecorator extends HTMLElementDecorator<'button'> {\n constructor() {\n super('button');\n }\n}\n\nexport class HTMLImageDecorator extends HTMLElementDecorator<'img'> {\n constructor() {\n super('img');\n }\n}\n\nexport function clearTitle(element: HTMLElement) {\n element.title = '';\n}\n","import { HTMLButtonDecorator } from '../html';\n\nexport class ReconnectButton extends HTMLButtonDecorator {\n containerElement = document.createElement('span');\n\n constructor(interopHost: string) {\n super();\n this.element.title = 'reconnect now';\n\n const iconUrl = `${interopHost}/connect.png`;\n const img = document.createElement('img');\n img.src = iconUrl;\n this.element.appendChild(img);\n }\n}\n","import type { DirectoryProps } from '../../../types';\n\nimport { ChannelPicker } from '../channel-picker';\nimport { LogoView } from './logo';\n\n/**\n * A composition of multiple HTML elements that renders details about the current directory.\n */\nexport class DirectoryView {\n constructor(private picker: ChannelPicker, private logo: LogoView) {}\n\n update(directory: DirectoryProps) {\n this.picker.updateInteropStrategy(directory.interopStrategy);\n\n if (directory.icon) {\n this.logo.updateImageSource(directory.icon);\n }\n }\n}\n","import { HTMLButtonDecorator } from '../../common/html';\n\nexport class ChannelPickerToolbarButton extends HTMLButtonDecorator {\n private icon = document.createElement('img');\n\n constructor() {\n super();\n this.element.appendChild(this.icon);\n }\n\n updateOwner(\n interopHost: string,\n interopStrategy: string,\n owner: string | undefined,\n ) {\n if (interopStrategy !== 'strict' && interopStrategy !== 'openauth') {\n this.element.style.display = 'none';\n return;\n }\n this.element.style.display = '';\n\n if (!owner) {\n this.element.setAttribute('title', 'please sign in');\n this.icon.src = `${interopHost}/signin.svg`;\n return;\n }\n this.element.setAttribute('title', `sign out of ${owner}`);\n this.icon.src = `${interopHost}/signout.svg`;\n }\n}\n","import { HTMLDivDecorator } from '../../common/html';\nimport { ChannelPickerToolbarButton } from './button';\n\nexport class ChannelPickerToolbar extends HTMLDivDecorator {\n button: ChannelPickerToolbarButton;\n\n constructor() {\n super();\n this.element.classList.add('toolbar');\n\n this.button = new ChannelPickerToolbarButton();\n this.element.appendChild(this.button.element);\n }\n}\n","export function createImage(src: string) {\n const image = new Image();\n image.src = src;\n return image;\n}\n\nexport function getWarningImageUrl(interopHost: string) {\n return `${interopHost}/warning.svg`;\n}\n\nexport function getErrorImageUrl(interopHost: string) {\n return `${interopHost}/error.svg`;\n}\n\nexport function getCheckImageUrl(interopHost: string) {\n return `${interopHost}/check.svg`;\n}\n\nexport function getUnlockImageUrl(interopHost: string) {\n return `${interopHost}/unlock.svg`;\n}\n\nexport function getLogoImageUrl(interopHost: string) {\n return `${interopHost}/connectifi-logo-white.png`;\n}\n\nexport function getChannelImageUrl(interopHost: string) {\n return `${interopHost}/channel.svg`;\n}\n\nconst imageCache: Map<string, HTMLImageElement> = new Map();\nexport function getImageInstance(src: string) {\n if (!imageCache.has(src)) {\n imageCache.set(src, createImage(src));\n }\n return imageCache.get(src) as HTMLImageElement;\n}\n","import type { Channel } from '@finos/fdc3';\n\nimport { HTMLDivDecorator } from '../common/html';\nimport { createImage } from '../common/image';\n\nfunction isYOffScreen(el?: Element, marginFromWindow = 0) {\n if (!el) return false;\n const rect = el.getBoundingClientRect();\n return (\n rect.y + rect.height - marginFromWindow < 0 ||\n rect.y > window.innerHeight - marginFromWindow\n );\n}\n\nexport class ChannelPickerChannelsContainer extends HTMLDivDecorator {\n constructor(\n private channelSrc: string,\n private onChannelPicked: (channel: string) => void,\n ) {\n super();\n this.element.classList.add('channels');\n\n this.addEventListeners();\n }\n\n private addEventListeners() {\n window.addEventListener('resize', this.handleResize.bind(this));\n }\n\n private handleResize() {\n const marginFromWindow = 45;\n const child = this.element.children.item(0);\n const childrenLength = this.element.children.length;\n\n if (!child) return;\n\n const childSize = child.getBoundingClientRect().height;\n const childStyle = getComputedStyle(child);\n const margin =\n parseFloat(childStyle.marginTop) + parseFloat(childStyle.marginBottom);\n const totalChildSize = childSize + margin;\n\n for (let i = 1; i < childrenLength + 1; i++) {\n const newWidth = totalChildSize * i + 'px';\n this.element.style.width = newWidth;\n\n if (\n !isYOffScreen(this.element.firstChild as Element, marginFromWindow) &&\n !isYOffScreen(this.element.lastChild as Element, marginFromWindow)\n ) {\n return;\n }\n }\n }\n\n private async pickChannel(channel: string, event?: Event) {\n event?.stopPropagation();\n this.onChannelPicked(channel);\n }\n\n private renderChannel(channel: Channel) {\n const id = `channel-item-${channel.id}`;\n const hasItem = this.element.querySelector(`#${id}`);\n\n //don't render if we've already done it (prevent duplicating DOM), or if this is the 'global' channel\n if (hasItem || channel.id === 'global') {\n return;\n }\n\n const button = document.createElement('button');\n const name = channel.displayMetadata?.name || channel.id;\n const color = channel.displayMetadata?.color || 'black';\n\n button.id = id;\n button.title = name + ' Channel';\n button.style.backgroundColor = color;\n\n button.addEventListener('click', (event: Event) => {\n this.pickChannel(channel.id as string, event);\n });\n\n button.appendChild(createImage(this.channelSrc));\n\n this.element.appendChild(button);\n }\n\n renderChannels(channels: Channel[]) {\n channels.forEach((channel) => this.renderChannel(channel));\n this.handleResize();\n }\n}\n","import { ChannelPickerToolbar } from './toolbar';\nimport { ChannelPickerChannelsContainer } from './channels';\nimport { HTMLDivDecorator } from '../common/html';\n\nexport class ChannelPicker extends HTMLDivDecorator {\n private animationDelay = -1;\n private isShowing = false;\n public canShow = false;\n\n toolbar: ChannelPickerToolbar;\n\n channels: ChannelPickerChannelsContainer;\n\n constructor(\n hubSrc: string,\n position: string,\n private onChannelPicked: (channel: string) => void,\n ) {\n super();\n this.element.classList.add('picker', position);\n\n this.toolbar = new ChannelPickerToolbar();\n this.element.appendChild(this.toolbar.element);\n\n this.channels = new ChannelPickerChannelsContainer(\n hubSrc,\n this.handleChannelPicked.bind(this),\n );\n this.element.appendChild(this.channels.element);\n\n this.stopRootClickPropagation();\n }\n\n private handleChannelPicked(channel: string) {\n this.onChannelPicked(channel);\n this.hide();\n }\n\n private stopRootClickPropagation() {\n this.element.addEventListener('click', (event: Event) => {\n event.stopPropagation();\n });\n }\n\n hide(): void {\n if (this.canShow) {\n this.isShowing = false;\n this.element.classList.remove('show');\n this.animationDelay = window.setTimeout(() => {\n this.channels.element.classList.remove('show');\n }, 500);\n }\n }\n\n show(): void {\n if (this.canShow) {\n window.clearTimeout(this.animationDelay);\n this.isShowing = true;\n this.channels.element.classList.add('show');\n this.element.classList.add('show');\n }\n }\n\n toggle(): void {\n if (this.isShowing) {\n return this.hide();\n }\n this.show();\n }\n\n updateInteropStrategy(interopStrategy: string | undefined) {\n if (!interopStrategy) return;\n this.element.classList.add(interopStrategy);\n }\n}\n","import { HTMLImageDecorator } from '../common/html';\n\nexport class HeaderLogo extends HTMLImageDecorator {\n constructor(logoSrc: string) {\n super();\n this.element.alt = 'connectifi';\n\n this.element.classList.add('logo');\n this.element.classList.add('loaded');\n this.element.classList.add('connected');\n\n this.element.src = logoSrc;\n }\n}\n","import { HTMLDivDecorator, clearTitle } from '../common/html';\nimport { HeaderLogo } from './logo';\n\nexport class Header extends HTMLDivDecorator {\n logo: HeaderLogo;\n indicator = document.createElement('div');\n mask = document.createElement('div');\n\n constructor(logoSrc: string) {\n super();\n\n this.element.classList.add('fabHeader');\n\n this.mask.classList.add('mask');\n this.element.appendChild(this.mask);\n\n this.indicator.classList.add('indicator');\n this.element.appendChild(this.indicator);\n\n this.logo = new HeaderLogo(logoSrc);\n this.element.appendChild(this.logo.element);\n }\n\n connected(directory: string) {\n this.element.classList.add('connected');\n this.element.title = `Connected to directory '${directory}'.`;\n }\n\n disconnected() {\n clearTitle(this.element);\n this.element.classList.remove('connected');\n }\n\n loading() {\n clearTitle(this.element);\n this.element.classList.add('busy');\n }\n\n loaded() {\n clearTitle(this.element);\n this.element.classList.remove('busy');\n }\n\n setBackgroundColor(color: string) {\n this.element.style.backgroundColor = color;\n }\n}\n","import { HTMLDivDecorator } from '../../common/html';\n\nexport class ResolverHeaderDismiss extends HTMLDivDecorator {\n constructor() {\n super();\n\n this.element.classList.add('dismissContainer');\n\n const dismiss = document.createElement('div');\n dismiss.classList.add('dismiss');\n\n const span = document.createElement('span');\n span.innerHTML = '&#215;';\n\n const container = document.createElement('div');\n container.appendChild(span);\n\n dismiss.appendChild(container);\n\n this.element.appendChild(dismiss);\n }\n}\n","import { HTMLDivDecorator } from '../../common/html';\n\nexport class ResolverHeaderLogo extends HTMLDivDecorator {\n icon = document.createElement('img');\n\n constructor(logoSrc: string) {\n super();\n\n this.element.classList.add('logo');\n\n this.icon.src = logoSrc;\n\n this.element.appendChild(this.icon);\n }\n}\n","import { HTMLDivDecorator } from './html';\n\nexport class Title extends HTMLDivDecorator {\n constructor() {\n super();\n this.element.classList.add('title');\n }\n\n updateDirectoryName(directoryName: string) {\n this.element.textContent = directoryName;\n this.element.title = `Connected to directory: '${directoryName}'`;\n }\n}\n","import { Title } from '../../common/title';\n\nexport class ResolverHeaderTitle extends Title {\n containerElement = document.createElement('span');\n\n constructor() {\n super();\n this.containerElement.innerHTML = '&#215;';\n this.element.appendChild(this.containerElement);\n }\n}\n","import { HTMLDivDecorator } from '../../common/html';\nimport { ResolverHeaderDismiss } from './dismiss';\nimport { ResolverHeaderLogo } from './logo';\nimport { ResolverHeaderTitle } from './title';\n\nexport class ResolverHeader extends HTMLDivDecorator {\n logo: ResolverHeaderLogo;\n dismiss = new ResolverHeaderDismiss();\n title = new ResolverHeaderTitle();\n\n constructor(logoSrc: string) {\n super();\n\n this.element.classList.add('header');\n\n this.logo = new ResolverHeaderLogo(logoSrc);\n this.element.appendChild(this.logo.element);\n this.element.appendChild(this.title.element);\n this.element.appendChild(this.dismiss.element);\n }\n}\n","import { HTMLDivDecorator } from '../../common/html';\n\n//resolve for different icon data formats between 1.x and 2.x\nconst getIconPath = (\n interopHost: string,\n icon: string | { src: string },\n): string => {\n if (icon) {\n //detect relative URL - more room for improvement here\n const url = typeof icon === 'string' ? icon : icon.src;\n if (url.toLowerCase().startsWith('http')) {\n return url;\n }\n\n return `${interopHost}/${url}`;\n }\n\n return '';\n};\n\nexport class ResolverAppRowIconContainer extends HTMLDivDecorator {\n constructor(\n interopHost: string,\n bridge: boolean,\n isSecure: boolean,\n icons?: { src: string }[],\n ) {\n super();\n\n this.element.classList.add('icon-container');\n\n if (bridge) return;\n\n const iconUrl = `${interopHost}/${isSecure ? 'lock' : 'warning'}.svg`;\n const lockIcon: HTMLElement = document.createElement('div');\n lockIcon.setAttribute('style', `background-image: url('${iconUrl}');`);\n lockIcon.classList.add('icon', 'mask');\n this.element.appendChild(lockIcon);\n\n const img = document.createElement('img');\n if (icons && icons.length > 0) {\n img.src = getIconPath(interopHost, icons[0]);\n }\n this.element.appendChild(img);\n }\n}\n","import { HTMLDivDecorator } from '../../../common/html';\n\nexport class ResolverAppRowAgentInfo extends HTMLDivDecorator {\n containerElement: HTMLDivElement = document.createElement('div');\n\n constructor(interopHost: string, type: string, device?: string) {\n super();\n\n const lowercaseType = type && type.toLocaleLowerCase();\n\n this.element.setAttribute('class', `agent ${lowercaseType}`);\n\n if (device) {\n this.element.setAttribute('title', `${type} (${device})`);\n } else {\n this.element.title = type;\n }\n\n this.element.setAttribute(\n 'style',\n `background-image: url('${interopHost}/${lowercaseType}.svg')`,\n );\n\n this.element.appendChild(this.containerElement);\n }\n}\n","import { HTMLDivDecorator } from '../../../common/html';\nimport { ResolverAppRowAgentInfo } from './agent';\n\nexport class ResolverAppRowInfo extends HTMLDivDecorator {\n constructor(\n interopHost: string,\n proximity: number,\n type: string,\n os?: string,\n device?: string,\n browser?: string,\n ) {\n super();\n\n this.element.classList.add('info');\n this.element.setAttribute('title', `proximity: ${proximity}`);\n\n if (type === 'directory') return;\n\n if (proximity > 1) {\n const agentNode = new ResolverAppRowAgentInfo(\n interopHost,\n os as string,\n device,\n );\n this.element.appendChild(agentNode.element);\n }\n\n if (proximity > 0) {\n const agentNode = new ResolverAppRowAgentInfo(\n interopHost,\n browser as string,\n );\n this.element.appendChild(agentNode.element);\n }\n }\n}\n","import { Title } from '../../common/title';\n\nexport class AppRowTitle extends Title {\n constructor(appTitle: string) {\n super();\n this.updateAppTitle(appTitle);\n }\n\n updateAppTitle(appTitle: string) {\n this.element.textContent = appTitle;\n this.element.setAttribute('title', `${appTitle}`);\n }\n}\n","import { ConnectifiAppMetadata } from '../../../../main';\nimport { HTMLDivDecorator } from '../../common/html';\nimport { ResolverAppRowIconContainer } from './icon-container';\nimport { ResolverAppRowInfo } from './info';\nimport { AppRowTitle } from './title';\n\nconst getAppTitle = (app: ConnectifiAppMetadata): string => {\n const title = app.title || app.name;\n const instTitle = app.instanceTitle;\n\n if (!instTitle) {\n return title || 'unknown';\n }\n\n if (title && !instTitle.startsWith(title)) {\n return `${title} - ${instTitle}`;\n }\n\n return instTitle;\n};\n\nexport class ResolverAppRow extends HTMLDivDecorator {\n iconContainer: ResolverAppRowIconContainer;\n title: AppRowTitle;\n info: ResolverAppRowInfo;\n\n constructor(\n interopHost: string,\n app: ConnectifiAppMetadata,\n bridge: boolean,\n ) {\n super();\n\n this.element.classList.add('item');\n\n this.iconContainer = new ResolverAppRowIconContainer(\n interopHost,\n bridge,\n app.isSecure,\n app.icons,\n );\n this.element.appendChild(this.iconContainer.element);\n\n const appTitle = getAppTitle(app);\n this.title = new AppRowTitle(appTitle);\n this.element.appendChild(this.title.element);\n\n this.info = new ResolverAppRowInfo(\n interopHost,\n app.proximity,\n app.type,\n app.os,\n app.device,\n app.browser,\n );\n this.element.appendChild(this.info.element);\n }\n}\n","import { IntentResultType } from '../../../main';\nimport { HTMLDivDecorator } from '../common/html';\n\nconst getAppTypeDisplayName = (intentResultType: IntentResultType) => {\n if (intentResultType === 'directory') {\n return 'Open New';\n }\n\n return 'Send To';\n};\n\nexport class ResolverGroup extends HTMLDivDecorator {\n constructor(bridge: boolean, type: IntentResultType) {\n super();\n\n this.element.classList.add('group');\n\n if (bridge) {\n this.element.textContent = 'Local Container';\n } else {\n this.element.textContent = getAppTypeDisplayName(type);\n }\n }\n}\n","import { HTMLDivDecorator } from '../common/html';\n\nexport class ResolverIntentRow extends HTMLDivDecorator {\n titleElement = document.createElement('div');\n\n constructor(title: string) {\n super();\n\n this.element.classList.add('intentRow');\n\n this.titleElement.className = 'intentTitle';\n this.titleElement.textContent = title;\n\n this.element.appendChild(this.titleElement);\n }\n}\n","import type { ConnectifiAppMetadata } from '@/common/types';\nimport type { AppIntentResult } from '@/common/interop/down-messages';\n\nimport { ResolverAppRow } from './app-row';\nimport { ResolverGroup } from './group';\nimport { ResolverIntentRow } from './intent-row';\nimport { HTMLDivDecorator } from '../common/html';\n\nconst sortAppsOfSameType = (\n a: ConnectifiAppMetadata,\n b: ConnectifiAppMetadata,\n) => {\n if (a.type !== 'window') {\n return 0;\n }\n\n if (a.proximity === b.proximity) {\n return b.lastUpdate! - a.lastUpdate!;\n }\n\n return a.proximity - b.proximity;\n};\n\n// for sorting/grouping apps by AppInstanceType\nexport const appsorter = (\n a: ConnectifiAppMetadata,\n b: ConnectifiAppMetadata,\n) => {\n if (a.type === b.type) {\n return sortAppsOfSameType(a, b);\n }\n\n if (a.type === 'directory') {\n return 1;\n }\n\n return -1;\n};\n\nexport class ResolverList extends HTMLDivDecorator {\n constructor() {\n super();\n this.element.classList.add('list');\n }\n\n clear() {\n this.element.innerHTML = '';\n }\n\n renderIntentRow({\n intent: { displayName, name } = { displayName: '', name: '' },\n }: AppIntentResult) {\n const intentRow = new ResolverIntentRow(displayName || name);\n this.element.appendChild(intentRow.element);\n }\n\n renderAppRows(\n interopHost: string,\n intentRes: AppIntentResult,\n onAppClicked: (\n app: ConnectifiAppMetadata,\n intentRes: AppIntentResult,\n bridge: boolean,\n ) => void,\n bridge: boolean = false,\n ) {\n const { apps } = intentRes;\n apps.sort(appsorter);\n\n let group: string = '';\n apps.forEach((app: ConnectifiAppMetadata) => {\n if (app.type !== group) {\n const groupRow = new ResolverGroup(bridge, app.type);\n this.element.appendChild(groupRow.element);\n group = app.type;\n }\n const row = new ResolverAppRow(interopHost, app, bridge);\n this.element.appendChild(row.element);\n\n row.element.addEventListener('click', (event) => {\n event.stopPropagation();\n onAppClicked(app, intentRes, bridge);\n });\n });\n }\n}\n","import { HTMLDivDecorator } from '../common/html';\nimport { ResolverHeader } from './header';\nimport { ResolverList } from './list';\n\nexport class Resolver extends HTMLDivDecorator {\n header: ResolverHeader;\n list = new ResolverList();\n isOpen = false;\n\n constructor(logoSrc: string) {\n super();\n\n this.element.classList.add('resolver');\n\n this.header = new ResolverHeader(logoSrc);\n this.element.appendChild(this.header.element);\n\n this.element.appendChild(this.list.element);\n\n this.stopClickPropagation();\n }\n\n private stopClickPropagation() {\n this.element.addEventListener('click', (event: Event) => {\n event.stopPropagation();\n });\n }\n\n open() {\n this.element.classList.add('open');\n this.isOpen = true;\n }\n\n close() {\n this.element.classList.remove('open');\n this.isOpen = false;\n }\n}\n","import { HTMLDivDecorator } from './common/html';\n\nexport class ResolverBackground extends HTMLDivDecorator {\n constructor() {\n super();\n this.element.classList.add('resolverBackground');\n }\n\n open() {\n this.element.classList.add('open');\n }\n\n close() {\n this.element.classList.remove('open');\n }\n}\n","import { HTMLDivDecorator } from '../common/html';\n\nexport class ToastMessage extends HTMLDivDecorator {\n constructor() {\n super();\n this.element.classList.add('message');\n }\n\n clear() {\n this.element.innerHTML = '';\n }\n\n setMessage(message: string) {\n this.element.innerText = message;\n }\n}\n","import { HTMLDivDecorator } from '../common/html';\nimport { Title } from '../common/title';\nimport { ToastMessage } from './message';\n\nexport interface ShowToastInput {\n title: string;\n message: string;\n image: HTMLImageElement;\n duration?: number;\n actionElement?: HTMLElement;\n}\n\nexport class Toast extends HTMLDivDecorator {\n title = new Title();\n\n private image: HTMLImageElement | undefined;\n private action: HTMLElement | undefined;\n private message = new ToastMessage();\n\n private animationDurationMs = 250;\n private durationTimer?: number;\n private animationTimer?: number;\n\n private hidePromise: Promise<void> | undefined;\n\n constructor() {\n super();\n\n this.element.classList.add('toast');\n\n const content = document.createElement('div');\n content.appendChild(this.title.element);\n content.appendChild(this.message.element);\n this.element.appendChild(content);\n }\n\n private clearAction() {\n if (!this.action) return;\n this.element.removeChild(this.action);\n this.action = undefined;\n }\n\n private setAction(action: HTMLElement | undefined) {\n if (!action) return;\n this.action = action;\n this.element.appendChild(action);\n }\n\n private clearImage() {\n if (!this.image) return;\n this.element.removeChild(this.image);\n this.image = undefined;\n }\n\n private setImage(image: HTMLImageElement) {\n this.image = image;\n this.element.insertBefore(image, this.element.firstChild);\n }\n\n private clear() {\n clearTimeout(this.durationTimer);\n this.element.classList.remove('success', 'show', 'hide', 'error');\n\n this.message.clear();\n this.clearAction();\n this.clearImage();\n }\n\n async hide() {\n if (this.hidePromise === undefined) {\n this.hidePromise = new Promise<void>((res) => {\n this.element.classList.add('hide');\n this.animationTimer = window.setTimeout(() => {\n this.element.classList.remove('hide', 'show');\n this.hidePromise = undefined;\n res();\n }, this.animationDurationMs);\n });\n }\n return this.hidePromise;\n }\n\n setDuration(duration: number) {\n this.durationTimer = window.setTimeout(() => this.hide(), duration);\n }\n\n async show({\n title,\n message,\n actionElement,\n duration = 3000,\n image,\n }: ShowToastInput) {\n if (this.element.classList.contains('show')) {\n await this.hide();\n }\n\n this.clear();\n\n this.title.element.innerText = title;\n this.message.setMessage(message);\n this.setAction(actionElement);\n this.setImage(image);\n\n this.element.classList.add('show');\n\n if (duration > 0) {\n this.setDuration(duration);\n }\n }\n\n update(message: string) {\n this.message.setMessage(message);\n }\n}\n","import { ChannelPicker } from './channel-picker';\nimport { Header } from './header';\nimport { Resolver } from './resolver';\nimport { ResolverBackground } from './resolver-background';\nimport { Toast } from './toast';\nimport { HTMLDivDecorator } from './common/html';\n\nexport class Container extends HTMLDivDecorator {\n header: Header;\n picker: ChannelPicker;\n toast: Toast;\n resolver: Resolver;\n resolverBackground = new ResolverBackground();\n\n constructor(\n logoSrc: string,\n hubSrc: string,\n position: string,\n onChannelPicked: (channel: string) => void,\n ) {\n super();\n\n this.element.classList.add('container');\n\n const inner = document.createElement('div');\n inner.classList.add('inner');\n\n this.header = new Header(logoSrc);\n inner.appendChild(this.header.element);\n\n this.toast = new Toast();\n inner.appendChild(this.toast.element);\n\n const fab = document.createElement('div');\n fab.classList.add('fab', position);\n\n fab.appendChild(inner);\n\n this.picker = new ChannelPicker(hubSrc, position, onChannelPicked);\n fab.appendChild(this.picker.element);\n\n this.element.appendChild(fab);\n\n this.resolver = new Resolver(logoSrc);\n this.element.appendChild(this.resolver.element);\n\n this.element.appendChild(this.resolverBackground.element);\n }\n}\n","import { HTMLButtonDecorator } from '../html';\n\nexport class LoginButton extends HTMLButtonDecorator {\n constructor(interopHost: string) {\n super();\n this.element.title = 'sign in';\n\n const iconUrl = `${interopHost}/signin.svg`;\n const img = document.createElement('img');\n img.src = iconUrl;\n this.element.appendChild(img);\n }\n}\n","import { RedirectStyle } from '../../types';\nimport { LoginButton } from '../components/common/button/login';\n\nexport class Logout {\n private window: Window | null = null;\n\n constructor(\n private interopHost: string,\n private style: RedirectStyle = 'sameWindow',\n ) {}\n\n createButton() {\n const button = new LoginButton(this.interopHost);\n const handleClick = (event: Event) => {\n event.stopPropagation();\n this.open();\n };\n button.element.addEventListener('click', handleClick);\n return button;\n }\n\n close() {\n this.window?.close();\n }\n\n open() {\n if (this.style === 'newWindow') {\n this.window = window.open(\n `${this.interopHost}/api/logout?target=interop`,\n '_blank',\n );\n\n return;\n }\n\n const url = window.location.href;\n\n window.location.href = `${\n this.interopHost\n }/api/logout?target=interop&interopUrl=${encodeURIComponent(url)}`;\n }\n}\n","import type { AuthenticateConfig, RedirectStyle } from '../types';\n\nimport { Toast } from './components/toast';\nimport { DirectoryView } from './components/view/directory';\nimport { getUnlockImageUrl, getImageInstance } from './components/common/image';\nimport { LoginButton } from './components/common/button/login';\n\nexport class ConnectifiAuthenticator {\n constructor(\n private readonly interopHost: string,\n private readonly toast: Pick<Toast, 'show'>,\n private readonly directoryView: Pick<DirectoryView, 'update'>,\n private readonly redirectStyle?: RedirectStyle,\n ) {}\n\n async authenticate(config: AuthenticateConfig) {\n const { name, authenticate } = config;\n\n const button = new LoginButton(this.interopHost);\n\n this.toast.show({\n title: 'Sign In',\n message: `Please sign in to '${name}'.`,\n actionElement: button.element,\n duration: -1,\n image: getImageInstance(getUnlockImageUrl(this.interopHost)),\n });\n this.directoryView.update(config);\n\n return new Promise<void>((res) =>\n button.element.addEventListener('click', async (ev) => {\n ev.preventDefault();\n await authenticate(this.redirectStyle);\n res();\n }),\n );\n }\n}\n","import { HTMLButtonDecorator } from '../html';\n\nexport class OpenButton extends HTMLButtonDecorator {\n constructor(interopHost: string) {\n super();\n this.element.title = 'Open';\n\n const iconUrl = `${interopHost}/signin.svg`;\n const img = document.createElement('img');\n img.src = iconUrl;\n this.element.appendChild(img);\n }\n}\n","import type { Toast } from './components/toast';\n\nimport { RedirectStyle, RequestStorageAccessConfig } from '../types';\nimport { OpenButton } from './components/common/button/open';\nimport { getImageInstance, getUnlockImageUrl } from './components/common/image';\n\n/**\n * Extends Storage Access implementation to add user interface for requesting storage access.\n */\nexport class StorageAccess {\n constructor(\n private interopUrl: string,\n private toast: Pick<Toast, 'show' | 'hide'>,\n private consentRedirectStyle: RedirectStyle = 'sameWindow',\n ) {}\n\n private createConsentButton() {\n return new OpenButton(t