@quiltt/vue 5.1.2

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.
@@ -0,0 +1,215 @@
1
+ /**
2
+ * QuilttConnector - Embeds the Quiltt Connector in an iframe
3
+ *
4
+ * This component renders the Quiltt Connector directly in your page,
5
+ * suitable for full-page or embedded connector experiences.
6
+ *
7
+ * @example
8
+ * ```vue
9
+ * <QuilttConnector
10
+ * :connector-id="connectorId"
11
+ * @exit-success="handleSuccess"
12
+ * />
13
+ * ```
14
+ */
15
+
16
+ import type { PropType } from 'vue'
17
+ import { computed, defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
18
+
19
+ import type { ConnectorSDKCallbackMetadata, ConnectorSDKEventType } from '@quiltt/core'
20
+
21
+ import { useQuilttSession } from '../composables/useQuilttSession'
22
+
23
+ export interface QuilttConnectorHandle {
24
+ handleOAuthCallback: (url: string) => void
25
+ }
26
+
27
+ export const QuilttConnector = defineComponent({
28
+ name: 'QuilttConnector',
29
+
30
+ props: {
31
+ /** Quiltt Connector ID */
32
+ connectorId: {
33
+ type: String,
34
+ required: true,
35
+ },
36
+ /** Existing connection ID for reconnection */
37
+ connectionId: {
38
+ type: String as PropType<string | undefined>,
39
+ default: undefined,
40
+ },
41
+ /** Pre-select a specific institution */
42
+ institution: {
43
+ type: String as PropType<string | undefined>,
44
+ default: undefined,
45
+ },
46
+ /** Deep link URL for OAuth callbacks (mobile apps) */
47
+ appLauncherUrl: {
48
+ type: String as PropType<string | undefined>,
49
+ default: undefined,
50
+ },
51
+ },
52
+
53
+ emits: {
54
+ /** Connector loaded */
55
+ load: (_metadata: ConnectorSDKCallbackMetadata) => true,
56
+ /** Connection successful */
57
+ 'exit-success': (_metadata: ConnectorSDKCallbackMetadata) => true,
58
+ /** User cancelled */
59
+ 'exit-abort': (_metadata: ConnectorSDKCallbackMetadata) => true,
60
+ /** Error occurred */
61
+ 'exit-error': (_metadata: ConnectorSDKCallbackMetadata) => true,
62
+ /** Any connector event */
63
+ event: (_type: ConnectorSDKEventType, _metadata: ConnectorSDKCallbackMetadata) => true,
64
+ /** OAuth URL requested (for native handling) */
65
+ navigate: (_url: string) => true,
66
+ },
67
+
68
+ setup(props, { emit, expose }) {
69
+ const iframeRef = ref<HTMLIFrameElement>()
70
+ const { session } = useQuilttSession()
71
+
72
+ const trustedQuilttHostSuffixes = ['quiltt.io', 'quiltt.dev', 'quiltt.app']
73
+
74
+ const isTrustedQuilttOrigin = (origin: string): boolean => {
75
+ try {
76
+ const originUrl = new URL(origin)
77
+ if (originUrl.protocol !== 'https:') {
78
+ return false
79
+ }
80
+ const hostname = originUrl.hostname.toLowerCase()
81
+ return trustedQuilttHostSuffixes.some(
82
+ (suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`)
83
+ )
84
+ } catch {
85
+ return false
86
+ }
87
+ }
88
+
89
+ // Connector origin for secure postMessage targeting
90
+ const connectorOrigin = computed(() => `https://${props.connectorId}.quiltt.app`)
91
+
92
+ // Build connector URL
93
+ const connectorUrl = computed(() => {
94
+ const url = new URL(connectorOrigin.value)
95
+
96
+ if (session.value?.token) {
97
+ url.searchParams.set('token', session.value.token)
98
+ }
99
+ if (props.connectionId) {
100
+ url.searchParams.set('connectionId', props.connectionId)
101
+ }
102
+ if (props.institution) {
103
+ url.searchParams.set('institution', props.institution)
104
+ }
105
+ if (props.appLauncherUrl) {
106
+ url.searchParams.set('app_launcher_url', props.appLauncherUrl)
107
+ }
108
+ if (typeof window !== 'undefined') {
109
+ url.searchParams.set('embed_location', window.location.href)
110
+ }
111
+ // Set mode for inline iframe embedding
112
+ url.searchParams.set('mode', 'INLINE')
113
+
114
+ return url.toString()
115
+ })
116
+
117
+ // Handle messages from the iframe
118
+ // The platform MessageBus sends: { source: 'quiltt', type: 'Load'|'ExitSuccess'|..., ...metadata }
119
+ const handleMessage = (event: MessageEvent) => {
120
+ if (!isTrustedQuilttOrigin(event.origin)) {
121
+ return
122
+ }
123
+
124
+ const data = event.data || {}
125
+ // Validate message is from Quiltt MessageBus
126
+ if (data.source !== 'quiltt' || !data.type) return
127
+
128
+ const { type, connectionId, profileId, connectorSession, url } = data
129
+
130
+ // Build metadata from message fields
131
+ const metadata: ConnectorSDKCallbackMetadata = {
132
+ connectorId: props.connectorId,
133
+ ...(profileId && { profileId }),
134
+ ...(connectionId && { connectionId }),
135
+ ...(connectorSession && { connectorSession }),
136
+ }
137
+
138
+ switch (type) {
139
+ case 'Load':
140
+ emit('event', 'Load' as ConnectorSDKEventType, metadata)
141
+ emit('load', metadata)
142
+ break
143
+ case 'ExitSuccess':
144
+ emit('event', 'ExitSuccess' as ConnectorSDKEventType, metadata)
145
+ emit('exit-success', metadata)
146
+ break
147
+ case 'ExitAbort':
148
+ emit('event', 'ExitAbort' as ConnectorSDKEventType, metadata)
149
+ emit('exit-abort', metadata)
150
+ break
151
+ case 'ExitError':
152
+ emit('event', 'ExitError' as ConnectorSDKEventType, metadata)
153
+ emit('exit-error', metadata)
154
+ break
155
+ case 'Navigate':
156
+ if (url) {
157
+ emit('navigate', url)
158
+ }
159
+ break
160
+ }
161
+ }
162
+
163
+ // Build OAuth callback message matching React Native SDK format
164
+ const buildOAuthCallbackMessage = (callbackUrl: string) => {
165
+ try {
166
+ const parsedUrl = new URL(callbackUrl)
167
+ const params: Record<string, string> = {}
168
+ parsedUrl.searchParams.forEach((value, key) => {
169
+ params[key] = value
170
+ })
171
+ return {
172
+ source: 'quiltt',
173
+ type: 'OAuthCallback',
174
+ data: { url: callbackUrl, params },
175
+ }
176
+ } catch {
177
+ return {
178
+ source: 'quiltt',
179
+ type: 'OAuthCallback',
180
+ data: { url: callbackUrl, params: {} },
181
+ }
182
+ }
183
+ }
184
+
185
+ const handleOAuthCallback = (url: string) => {
186
+ iframeRef.value?.contentWindow?.postMessage(
187
+ buildOAuthCallbackMessage(url),
188
+ connectorOrigin.value
189
+ )
190
+ }
191
+
192
+ expose({ handleOAuthCallback })
193
+
194
+ onMounted(() => {
195
+ window.addEventListener('message', handleMessage)
196
+ })
197
+
198
+ onUnmounted(() => {
199
+ window.removeEventListener('message', handleMessage)
200
+ })
201
+
202
+ return () =>
203
+ h('iframe', {
204
+ ref: iframeRef,
205
+ src: connectorUrl.value,
206
+ allow: 'publickey-credentials-get *',
207
+ class: 'quiltt-connector',
208
+ style: {
209
+ border: 'none',
210
+ width: '100%',
211
+ height: '100%',
212
+ },
213
+ })
214
+ },
215
+ })
@@ -0,0 +1,130 @@
1
+ /**
2
+ * QuilttContainer - Container component that renders Quiltt Connector inline
3
+ *
4
+ * Renders a container element where the Quiltt Connector will be displayed.
5
+ * The connector opens automatically when the component mounts.
6
+ *
7
+ * @example
8
+ * ```vue
9
+ * <QuilttContainer
10
+ * :connector-id="connectorId"
11
+ * @exit-success="handleSuccess"
12
+ * />
13
+ * ```
14
+ */
15
+
16
+ import { computed, defineComponent, h, onMounted, onUnmounted, type PropType, watch } from 'vue'
17
+
18
+ import type { ConnectorSDKCallbackMetadata, ConnectorSDKEventType } from '@quiltt/core'
19
+
20
+ import { useQuilttConnector } from '../composables/useQuilttConnector'
21
+ import { oauthRedirectUrlDeprecationWarning } from '../constants/deprecation-warnings'
22
+
23
+ export const QuilttContainer = defineComponent({
24
+ name: 'QuilttContainer',
25
+
26
+ props: {
27
+ /** Quiltt Connector ID */
28
+ connectorId: {
29
+ type: String,
30
+ required: true,
31
+ },
32
+ /** Existing connection ID for reconnection */
33
+ connectionId: {
34
+ type: String as PropType<string | undefined>,
35
+ default: undefined,
36
+ },
37
+ /** Pre-select a specific institution */
38
+ institution: {
39
+ type: String as PropType<string | undefined>,
40
+ default: undefined,
41
+ },
42
+ /** Deep link URL for OAuth callbacks (mobile apps) */
43
+ appLauncherUrl: {
44
+ type: String as PropType<string | undefined>,
45
+ default: undefined,
46
+ },
47
+ /**
48
+ * @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
49
+ * The OAuth redirect URL for mobile or embedded webview flows.
50
+ */
51
+ oauthRedirectUrl: {
52
+ type: String as PropType<string | undefined>,
53
+ default: undefined,
54
+ },
55
+ /** Render as a different element */
56
+ as: {
57
+ type: String,
58
+ default: 'div',
59
+ },
60
+ },
61
+
62
+ emits: {
63
+ /** Connector loaded */
64
+ load: (_metadata: ConnectorSDKCallbackMetadata) => true,
65
+ /** Connection successful */
66
+ 'exit-success': (_metadata: ConnectorSDKCallbackMetadata) => true,
67
+ /** User cancelled */
68
+ 'exit-abort': (_metadata: ConnectorSDKCallbackMetadata) => true,
69
+ /** Error occurred */
70
+ 'exit-error': (_metadata: ConnectorSDKCallbackMetadata) => true,
71
+ /** Connector exited (any reason) */
72
+ exit: (_type: ConnectorSDKEventType, _metadata: ConnectorSDKCallbackMetadata) => true,
73
+ /** Any connector event */
74
+ event: (_type: ConnectorSDKEventType, _metadata: ConnectorSDKCallbackMetadata) => true,
75
+ },
76
+
77
+ setup(props, { emit, slots }) {
78
+ watch(
79
+ () => props.oauthRedirectUrl,
80
+ (value) => {
81
+ if (value !== undefined) {
82
+ console.warn(oauthRedirectUrlDeprecationWarning)
83
+ }
84
+ },
85
+ { immediate: true }
86
+ )
87
+
88
+ const effectiveAppLauncherUri = computed(() => props.appLauncherUrl ?? props.oauthRedirectUrl)
89
+ let openTimeout: ReturnType<typeof setTimeout> | undefined
90
+
91
+ const { open } = useQuilttConnector(() => props.connectorId, {
92
+ connectionId: () => props.connectionId,
93
+ institution: () => props.institution,
94
+ appLauncherUrl: effectiveAppLauncherUri,
95
+ onEvent: (type, metadata) => emit('event', type, metadata),
96
+ onLoad: (metadata) => emit('load', metadata),
97
+ onExit: (type, metadata) => emit('exit', type, metadata),
98
+ onExitSuccess: (metadata) => emit('exit-success', metadata),
99
+ onExitAbort: (metadata) => emit('exit-abort', metadata),
100
+ onExitError: (metadata) => emit('exit-error', metadata),
101
+ })
102
+
103
+ onMounted(() => {
104
+ // Short delay to ensure SDK is loaded
105
+ openTimeout = setTimeout(() => {
106
+ open()
107
+ }, 100)
108
+ })
109
+
110
+ onUnmounted(() => {
111
+ if (openTimeout) {
112
+ clearTimeout(openTimeout)
113
+ openTimeout = undefined
114
+ }
115
+ })
116
+
117
+ return () =>
118
+ h(
119
+ props.as,
120
+ {
121
+ class: 'quiltt-container',
122
+ style: {
123
+ width: '100%',
124
+ height: '100%',
125
+ },
126
+ },
127
+ slots.default?.()
128
+ )
129
+ },
130
+ })
@@ -0,0 +1,3 @@
1
+ export { QuilttButton } from './QuilttButton'
2
+ export { QuilttConnector, type QuilttConnectorHandle } from './QuilttConnector'
3
+ export { QuilttContainer } from './QuilttContainer'
@@ -0,0 +1,7 @@
1
+ export * from './useQuilttConnector'
2
+ export * from './useQuilttInstitutions'
3
+ export * from './useQuilttResolvable'
4
+ export * from './useQuilttSession'
5
+ export * from './useQuilttSettings'
6
+ export * from './useSession'
7
+ export * from './useStorage'
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Quiltt Connector Composable
3
+ *
4
+ * Provides connector management functionality for Vue 3 applications.
5
+ * Loads the Quiltt Connector SDK and manages connector state.
6
+ *
7
+ * @example
8
+ * ```vue
9
+ * <script setup>
10
+ * import { useQuilttConnector } from '@quiltt/vue'
11
+ *
12
+ * const { open } = useQuilttConnector('conn_xxx', {
13
+ * onExitSuccess: (metadata) => {
14
+ * console.log('Connected:', metadata.connectionId)
15
+ * }
16
+ * })
17
+ * </script>
18
+ *
19
+ * <template>
20
+ * <button @click="open">Connect Bank Account</button>
21
+ * </template>
22
+ * ```
23
+ */
24
+
25
+ import type { MaybeRefOrGetter } from 'vue'
26
+ import { onMounted, onUnmounted, ref, toValue, watch } from 'vue'
27
+
28
+ import type {
29
+ ConnectorSDK,
30
+ ConnectorSDKCallbacks,
31
+ ConnectorSDKConnector,
32
+ Maybe,
33
+ QuilttJWT,
34
+ } from '@quiltt/core'
35
+ import { cdnBase } from '@quiltt/core'
36
+ import { extractVersionNumber } from '@quiltt/core/utils'
37
+
38
+ import { oauthRedirectUrlDeprecationWarning } from '../constants/deprecation-warnings'
39
+ import { getSDKAgent } from '../utils'
40
+ import { version } from '../version'
41
+ import { useQuilttSession } from './useQuilttSession'
42
+
43
+ declare const Quiltt: ConnectorSDK
44
+
45
+ export interface UseQuilttConnectorOptions extends ConnectorSDKCallbacks {
46
+ connectionId?: MaybeRefOrGetter<string | undefined>
47
+ institution?: MaybeRefOrGetter<string | undefined>
48
+ appLauncherUrl?: MaybeRefOrGetter<string | undefined>
49
+ /**
50
+ * @deprecated Use `appLauncherUrl` instead. This property will be removed in a future version.
51
+ * The OAuth redirect URL for mobile or embedded webview flows.
52
+ */
53
+ oauthRedirectUrl?: MaybeRefOrGetter<string | undefined>
54
+ nonce?: string
55
+ }
56
+
57
+ export interface UseQuilttConnectorReturn {
58
+ /** Open the connector modal */
59
+ open: () => void
60
+ }
61
+
62
+ /**
63
+ * Load the Quiltt SDK script
64
+ */
65
+ const loadScript = (src: string, nonce?: string): Promise<void> => {
66
+ return new Promise((resolve, reject) => {
67
+ // Check if already loaded
68
+ if (typeof Quiltt !== 'undefined') {
69
+ resolve()
70
+ return
71
+ }
72
+
73
+ // Check if script is already in DOM
74
+ const existing = document.querySelector(`script[src^="${src.split('?')[0]}"]`)
75
+ if (existing) {
76
+ existing.addEventListener('load', () => resolve())
77
+ existing.addEventListener('error', () => reject(new Error('Failed to load Quiltt SDK')))
78
+ return
79
+ }
80
+
81
+ const script = document.createElement('script')
82
+ script.src = src
83
+ script.async = true
84
+ if (nonce) script.nonce = nonce
85
+ script.onload = () => resolve()
86
+ script.onerror = () => reject(new Error('Failed to load Quiltt SDK'))
87
+ document.head.appendChild(script)
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Composable for managing Quiltt Connector
93
+ *
94
+ * Loads the Quiltt SDK script and provides methods to open/manage connectors.
95
+ * This composable can run without QuilttPlugin session context; when unavailable,
96
+ * it logs a warning and continues without authenticated session state.
97
+ */
98
+ export const useQuilttConnector = (
99
+ connectorId?: MaybeRefOrGetter<string | undefined>,
100
+ options?: UseQuilttConnectorOptions
101
+ ): UseQuilttConnectorReturn => {
102
+ const getConnectorId = (): string | undefined => toValue(connectorId)
103
+ const getConnectionId = (): string | undefined => toValue(options?.connectionId)
104
+ const getInstitution = (): string | undefined => toValue(options?.institution)
105
+ const getOauthRedirectUrl = (): string | undefined => toValue(options?.oauthRedirectUrl)
106
+ const getAppLauncherUri = (): string | undefined =>
107
+ toValue(options?.appLauncherUrl) ?? getOauthRedirectUrl()
108
+
109
+ const session = ref<Maybe<QuilttJWT | undefined>>()
110
+
111
+ try {
112
+ const quilttSession = useQuilttSession()
113
+ session.value = quilttSession.session.value
114
+
115
+ watch(
116
+ () => quilttSession.session.value,
117
+ (nextSession) => {
118
+ session.value = nextSession
119
+ },
120
+ { immediate: true }
121
+ )
122
+ } catch (error) {
123
+ console.warn(
124
+ '[Quiltt] useQuilttConnector: QuilttPlugin not found in the current app context. ' +
125
+ 'Continuing without session authentication.',
126
+ error
127
+ )
128
+ }
129
+
130
+ const connector = ref<ConnectorSDKConnector | undefined>()
131
+ const isLoaded = ref(false)
132
+ const isOpening = ref(false)
133
+ const isConnectorOpen = ref(false)
134
+
135
+ // Track previous values
136
+ let prevConnectionId = getConnectionId()
137
+ let prevConnectorId = getConnectorId()
138
+ let prevInstitution = getInstitution()
139
+ let prevAppLauncherUri = getAppLauncherUri()
140
+ let connectorCreated = false
141
+
142
+ // Load SDK script on mount
143
+ onMounted(async () => {
144
+ const sdkVersion = extractVersionNumber(version)
145
+ const userAgent = getSDKAgent(sdkVersion)
146
+ const scriptUrl = `${cdnBase}/v1/connector.js?agent=${encodeURIComponent(userAgent)}`
147
+
148
+ try {
149
+ await loadScript(scriptUrl, options?.nonce)
150
+ isLoaded.value = true
151
+ } catch (error) {
152
+ console.error('[Quiltt] Failed to load SDK:', error)
153
+ }
154
+ })
155
+
156
+ // Update authentication when session changes
157
+ watch(
158
+ () => session.value?.token,
159
+ (token) => {
160
+ if (typeof Quiltt !== 'undefined') {
161
+ Quiltt.authenticate(token)
162
+ }
163
+ },
164
+ { immediate: true }
165
+ )
166
+
167
+ // Handle script loaded
168
+ watch(
169
+ isLoaded,
170
+ (loaded) => {
171
+ if (!loaded || typeof Quiltt === 'undefined') return
172
+
173
+ // Authenticate with current session
174
+ Quiltt.authenticate(session.value?.token)
175
+ },
176
+ { immediate: true }
177
+ )
178
+
179
+ watch(
180
+ getOauthRedirectUrl,
181
+ (oauthRedirectUrl) => {
182
+ if (oauthRedirectUrl !== undefined) {
183
+ console.warn(oauthRedirectUrlDeprecationWarning)
184
+ }
185
+ },
186
+ { immediate: true }
187
+ )
188
+
189
+ // Create/update connector when needed
190
+ const updateConnector = () => {
191
+ const currentConnectorId = getConnectorId()
192
+ if (!isLoaded.value || typeof Quiltt === 'undefined' || !currentConnectorId) return
193
+
194
+ const currentConnectionId = getConnectionId()
195
+ const currentInstitution = getInstitution()
196
+ const currentAppLauncherUri = getAppLauncherUri()
197
+
198
+ // Check for changes
199
+ const connectionIdChanged = prevConnectionId !== currentConnectionId
200
+ const connectorIdChanged = prevConnectorId !== currentConnectorId
201
+ const institutionChanged = prevInstitution !== currentInstitution
202
+ const appLauncherUrlChanged = prevAppLauncherUri !== currentAppLauncherUri
203
+ const hasChanges =
204
+ connectionIdChanged ||
205
+ connectorIdChanged ||
206
+ institutionChanged ||
207
+ appLauncherUrlChanged ||
208
+ !connectorCreated
209
+
210
+ if (hasChanges) {
211
+ if (currentConnectionId) {
212
+ // Reconnect mode
213
+ connector.value = Quiltt.reconnect(currentConnectorId, {
214
+ connectionId: currentConnectionId,
215
+ appLauncherUrl: currentAppLauncherUri,
216
+ })
217
+ } else {
218
+ // Connect mode
219
+ connector.value = Quiltt.connect(currentConnectorId, {
220
+ institution: currentInstitution,
221
+ appLauncherUrl: currentAppLauncherUri,
222
+ })
223
+ }
224
+
225
+ connectorCreated = true
226
+ prevConnectionId = currentConnectionId
227
+ prevConnectorId = currentConnectorId
228
+ prevInstitution = currentInstitution
229
+ prevAppLauncherUri = currentAppLauncherUri
230
+ }
231
+ }
232
+
233
+ // Watch for changes that require connector update
234
+ watch(
235
+ [isLoaded, getConnectorId, getConnectionId, getInstitution, getAppLauncherUri],
236
+ updateConnector,
237
+ {
238
+ immediate: true,
239
+ }
240
+ )
241
+
242
+ // Register event handlers when connector changes
243
+ watch(
244
+ connector,
245
+ (newConnector, oldConnector) => {
246
+ // Cleanup old handlers
247
+ if (oldConnector) {
248
+ // Note: Quiltt SDK handles cleanup internally
249
+ }
250
+
251
+ if (!newConnector) return
252
+
253
+ // Register handlers
254
+ if (options?.onEvent) {
255
+ newConnector.onEvent(options.onEvent)
256
+ }
257
+ newConnector.onOpen((metadata) => {
258
+ isConnectorOpen.value = true
259
+ options?.onOpen?.(metadata)
260
+ })
261
+ if (options?.onLoad) {
262
+ newConnector.onLoad(options.onLoad)
263
+ }
264
+ newConnector.onExit((type, metadata) => {
265
+ isConnectorOpen.value = false
266
+ options?.onExit?.(type, metadata)
267
+ })
268
+ if (options?.onExitSuccess) {
269
+ newConnector.onExitSuccess(options.onExitSuccess)
270
+ }
271
+ if (options?.onExitAbort) {
272
+ newConnector.onExitAbort(options.onExitAbort)
273
+ }
274
+ if (options?.onExitError) {
275
+ newConnector.onExitError(options.onExitError)
276
+ }
277
+ },
278
+ { immediate: true }
279
+ )
280
+
281
+ // Handle deferred opening
282
+ watch([connector, isOpening], ([conn, opening]) => {
283
+ if (conn && opening) {
284
+ isOpening.value = false
285
+ conn.open()
286
+ }
287
+ })
288
+
289
+ // Warn on unmount if connector is still open
290
+ onUnmounted(() => {
291
+ if (isConnectorOpen.value) {
292
+ console.error(
293
+ '[Quiltt] useQuilttConnector: Component unmounted while Connector is still open. ' +
294
+ 'This may lead to memory leaks or unexpected behavior.'
295
+ )
296
+ }
297
+ })
298
+
299
+ /**
300
+ * Open the connector modal
301
+ */
302
+ const open = (): void => {
303
+ if (getConnectorId()) {
304
+ isOpening.value = true
305
+ updateConnector()
306
+ } else {
307
+ throw new Error('Must provide connectorId to open Quiltt Connector')
308
+ }
309
+ }
310
+
311
+ return { open }
312
+ }