Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/content/2.module/0.guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ nuxt.hook('devtools:customTabs', (tabs) => {
})
```

### Iframe Permissions

By default, iframes have `clipboard-write` and `clipboard-read` permissions enabled. You can add additional permissions using the `permissions` option:

```ts
const view: ModuleIframeView = {
type: 'iframe',
src: '/url-to-your-module-view',
// Additional permissions to allow in the iframe
permissions: ['camera', 'microphone', 'geolocation'],
}
```

These permissions will be merged with the default ones and set on the iframe's `allow` attribute.

Learn more about [DevTools Utility Kit](/module/utils-kit).

## Lazy Service Launching
Expand Down
15 changes: 15 additions & 0 deletions docs/content/2.module/1.utils-kit.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ addCustomTab(() => ({
}))
```

The iframe view supports the following options:

- `src`: URL of the iframe
- `persistent`: Whether to persist the iframe instance when switching tabs (default: `true`)
- `permissions`: Additional permissions to allow in the iframe (merged with default `clipboard-write` and `clipboard-read`)

```ts
const view: ModuleIframeView = {
type: 'iframe',
src: '/url-to-your-module-view',
persistent: true,
permissions: ['camera', 'microphone'],
}
```

### `refreshCustomTabs()`

A shorthand for call hook `devtools:customTabs:refresh`. It will refresh all custom tabs.
Expand Down
7 changes: 7 additions & 0 deletions packages/devtools-kit/src/_types/custom-tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export interface ModuleIframeView {
* @default true
*/
persistent?: boolean
/**
* Additional permissions to allow in the iframe
* These will be merged with the default permissions (clipboard-write, clipboard-read)
*
* @example ['camera', 'microphone', 'geolocation']
*/
permissions?: string[]
}

export interface ModuleVNodeView {
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/client/components/IframeView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const box = reactive(useElementBounding(anchor))
onMounted(() => {
const view = props.tab.view as ModuleIframeView
const isPersistent = view.persistent !== false
const allowedPermissions = ['clipboard-write', 'clipboard-read']
const allowedPermissions = ['clipboard-write', 'clipboard-read', ...(view.permissions || [])]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cached iframes don't update their permissions when remounted with different permission configurations. The allow attribute is only set when creating a new iframe, not when reusing a cached one.

View Details
πŸ“ Patch Details
diff --git a/packages/devtools/client/components/IframeView.vue b/packages/devtools/client/components/IframeView.vue
index b9d87d69..fe8b119d 100644
--- a/packages/devtools/client/components/IframeView.vue
+++ b/packages/devtools/client/components/IframeView.vue
@@ -3,7 +3,7 @@ import { useElementBounding } from '@vueuse/core'
 import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watchEffect } from 'vue'
 import { getColorMode, useInjectionClient } from '~/composables/client'
 
-const iframeCacheMap = new Map<string, HTMLIFrameElement>()
+const iframeCacheMap = new Map<string, { element: HTMLIFrameElement, allowAttribute: string }>()
 </script>
 
 <script setup lang="ts">
@@ -24,18 +24,20 @@ onMounted(() => {
   const view = props.tab.view as ModuleIframeView
   const isPersistent = view.persistent !== false
   const allowedPermissions = ['clipboard-write', 'clipboard-read', ...(view.permissions || [])]
+  const allowAttribute = allowedPermissions.join('; ')
 
-  if (iframeCacheMap.get(key.value) && isPersistent) {
-    iframeEl.value = iframeCacheMap.get(key.value)!
+  const cached = iframeCacheMap.get(key.value)
+  if (cached && isPersistent && cached.allowAttribute === allowAttribute) {
+    iframeEl.value = cached.element
     iframeEl.value.style.visibility = 'visible'
   }
   else {
     iframeEl.value = document.createElement('iframe')
-    iframeEl.value.setAttribute('allow', allowedPermissions.join('; '))
+    iframeEl.value.setAttribute('allow', allowAttribute)
     iframeEl.value.setAttribute('aria-label', 'Nuxt Devtools')
 
     if (isPersistent)
-      iframeCacheMap.set(key.value, iframeEl.value)
+      iframeCacheMap.set(key.value, { element: iframeEl.value, allowAttribute })
     iframeEl.value.src = view.src
     // CORS
     try {

Analysis

Cached iframes don't update permissions when remounted with different configurations

What fails: IframeView.vue caches iframes by tab name and reuses them on remount. When a cached iframe is remounted with different permissions in the tab configuration, the iframe retains its original allow attribute instead of updating to the new permissions.

How to reproduce:

  1. Create a module with a custom tab that uses a factory function for addCustomTab()
  2. Mount the tab with permissions: ['camera']
  3. Navigate away (iframe is cached but hidden)
  4. Call refreshCustomTabs() to trigger a refresh where the factory returns the same tab name but with permissions: ['microphone', 'geolocation']
  5. Navigate back to the tab
  6. The cached iframe still has allow="clipboard-write; clipboard-read; camera" instead of the new permissions

Result: The iframe retains the old permission set. Since the allow attribute cannot be updated after an iframe loads per browser security specifications, the cached iframe cannot dynamically acquire new permissions.

Expected: When a cached iframe is reused, its allow attribute should match the current permissions configuration. If permissions have changed, either the cache should be invalidated or a new iframe should be created.

Technical context: The bug occurs because:

  • The cache is keyed by tab.name only, not considering the permissions configuration
  • When retrieving a cached iframe (lines 28-31), the code skips permission setup entirely
  • The allow attribute is only set when creating new iframes (line 33)
  • Per web standards, the allow attribute's permissions are evaluated at iframe load time and cannot be updated afterward


if (iframeCacheMap.get(key.value) && isPersistent) {
iframeEl.value = iframeCacheMap.get(key.value)!
Expand Down
Loading