Skip to content

Modernize#6

Merged
ib-bsb-br merged 9 commits intoib-bsb-br:masterfrom
crittermike:modernize
Feb 23, 2026
Merged

Modernize#6
ib-bsb-br merged 9 commits intoib-bsb-br:masterfrom
crittermike:modernize

Conversation

@ib-bsb-br
Copy link
Copy Markdown
Owner

@ib-bsb-br ib-bsb-br commented Feb 23, 2026

Summary by Sourcery

Enable direct importing of shared shortcuts from shortkeys.app into the extension and allow the website to communicate with the extension for this purpose.

New Features:

  • Add an import button on the shared shortcuts page that sends shortcuts directly to the browser extension or falls back to copying JSON.
  • Allow the extension background script to accept external messages to import and merge shared shortcuts into existing ones.

Enhancements:

  • Configure the extension to be externally connectable from shortkeys.app and local development hosts.

Summary by cubic

Enable one-click import of shared shortcuts from shortkeys.app, add 26 built-in Page Scripts as first-class actions, and support importing userscripts from Greasyfork/OpenUserJS. Also polishes the groups UI to fix corner mismatches.

  • New Features

    • Share page: Import N shortcuts or Copy JSON; extension listens via onMessageExternal, merges into storage, auto-generates missing IDs; externally_connectable allows shortkeys.app and localhost.
    • Page Scripts: 26 curated scripts are selectable in the Behavior dropdown. In “Run JavaScript,” “Import from URL” fetches userscripts (Greasyfork/OpenUserJS or .user.js), strips metadata, and fills the editor.
  • Bug Fixes

    • Import button always visible; on failure, auto-copies JSON with a friendly “Copied!” state.
    • Cleaned up JavaScript action UI: removed the old Snippets modal, fixed userscript bar visibility, improved Greasyfork URL matching, converted labels to sentence case, removed duplicate markdown action, and adjusted header padding.
    • Groups UI: group container is a single rounded box with overflow hidden; cards and the add button sit flush for consistent corners.

Written for commit 957d9a2. Summary will update on new commits.

crittermike and others added 4 commits February 22, 2026 13:17
The share page now imports shortcuts directly into the extension
via chrome.runtime.sendMessage (externally_connectable API).

Flow:
1. User clicks 'Import N shortcuts' on the share page
2. Share page sends message to extension with shortcut data
3. Background script merges shortcuts into existing storage
4. Button turns green: '✓ Imported N shortcuts!'

Fallbacks:
- Extension not installed → shows 'Install Shortkeys first' link
- Not Chrome → shows 'Copy JSON to clipboard' button

Config:
- externally_connectable matches shortkeys.app and localhost
- onMessageExternal listener in background.ts

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the import button was hidden by a try/catch when the
extension ID didn't match (dev vs store). Now always shows both
'Import' and 'Copy JSON' buttons. If import fails, auto-copies
JSON and shows a temporary error state before resetting.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rror

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Feb 23, 2026

Reviewer's Guide

Adds a direct import flow from shortkeys.app share pages into the extension by replacing the static Web Store link with an import button that talks to the extension via external messages, implements the corresponding background listener to merge imported shortcuts into stored keys, and updates the extension manifest config to allow external connections from the share domain and localhost.

Sequence diagram for importing shared shortcuts into the extension

sequenceDiagram
  actor User
  participant SharePage as Share_page_shortkeys.app
  participant ChromeRuntime as Chrome_runtime
  participant Background as Extension_background
  participant Storage as Extension_storage

  User->>SharePage: Click import button
  SharePage->>SharePage: importToExtension()
  alt chrome.runtime.sendMessage available
    SharePage->>ChromeRuntime: sendMessage(STORE_ID, importShortcuts, shortcutsData)
    ChromeRuntime->>Background: onMessageExternal(message)
    Background->>Background: validate action and shortcuts
    Background->>Storage: loadKeys()
    Storage-->>Background: existingShortcuts
    Background->>Background: add ids and merge with existing
    Background->>Storage: saveKeys(mergedShortcuts)
    Storage-->>Background: save complete
    Background-->>ChromeRuntime: sendResponse({ success, count })
    ChromeRuntime-->>SharePage: response
    SharePage->>SharePage: Update button to Imported state
  else runtime missing or error/unsuccessful response
    SharePage->>SharePage: showFallback(btn)
    SharePage->>SharePage: copyJson()
    SharePage->>User: Show Copied confirmation on button
  end
Loading

File-Level Changes

Change Details Files
Replace static "Install" link on share page with an import workflow that either sends shortcuts directly to the extension or falls back to copying JSON.
  • Introduce STORE_ID constant for the Chrome Web Store extension ID to use when sending runtime messages.
  • Swap the single anchor button for two buttons: an import button wired to importToExtension() and a secondary button that triggers existing JSON copy behavior.
  • Implement importToExtension() to disable the button, optimistically show an importing state, attempt chrome.runtime.sendMessage to the extension with the shortcuts payload, and then either show a success state or trigger a fallback.
  • Add showFallback() helper that silently copies JSON, shows a transient copied state on the button, and then restores the original label and enabled state after a timeout.
site/share.html
Handle external import messages in the background script and persist the imported shortcuts.
  • Add chrome.runtime.onMessageExternal listener that looks for an importShortcuts action with a shortcuts array payload.
  • Load existing shortcuts via loadKeys(), parse them as JSON, and generate IDs for any imported shortcuts that lack one using uuid().
  • Merge existing and new shortcuts, save them with saveKeys(), and reply with a success flag and count of imported shortcuts, using an async IIFE and returning true to keep sendResponse valid asynchronously.
src/entrypoints/background.ts
Permit the extension to receive external messages from the share site and local development origins. wxt.config.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @ib-bsb-br, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the user experience for sharing and importing shortcuts by introducing a direct import mechanism. Instead of manually copying JSON, users can now import shortcuts directly from the shortkeys.app share page into their browser extension. This change involves updates to the frontend to trigger the import, backend logic in the extension to handle the incoming data, and manifest adjustments to allow secure cross-origin communication.

Highlights

  • Direct Shortcut Import: Implemented functionality on the share page to allow users to directly import shared shortcuts into the Shortkeys extension with a single click, streamlining the import process.
  • External Message Handling: Added an external message listener in the background script to securely receive and process shortcut data sent from the shortkeys.app website, merging new shortcuts with existing ones.
  • Extension Manifest Updates: Configured the extension's manifest to permit external connections from the shortkeys.app domain and localhost, enabling secure communication for the direct import feature.
Changelog
  • site/share.html
    • Replaced the static 'Install Shortkeys' button with dynamic 'Import' and 'Copy JSON' buttons.
    • Added JavaScript functions importToExtension() and showFallback() to handle direct import attempts and provide a fallback to copy JSON if direct import fails or the extension is not detected.
  • src/entrypoints/background.ts
    • Implemented chrome.runtime.onMessageExternal.addListener to handle incoming importShortcuts messages from external sources.
    • Added logic to load existing shortcuts, merge them with the newly imported shortcuts, assign unique IDs if missing, and save the combined set.
  • wxt.config.ts
    • Added an externally_connectable property to the manifest configuration.
    • Specified https://shortkeys.app/* and http://localhost/* as allowed origins for external connections.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The chrome.runtime.onMessageExternal handler trusts any message that matches the action shape; consider validating the sender (e.g., sender.origin/sender.url against an allowlist) in addition to externally_connectable.matches before importing shortcuts.
  • When importing shortcuts you concat existing and new items and reuse incoming ids when present; if ids collide or the same shortcut is imported multiple times this can lead to duplicates—consider normalizing IDs and/or deduplicating based on a stable key.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `chrome.runtime.onMessageExternal` handler trusts any message that matches the action shape; consider validating the sender (e.g., `sender.origin`/`sender.url` against an allowlist) in addition to `externally_connectable.matches` before importing shortcuts.
- When importing shortcuts you `concat` existing and new items and reuse incoming `id`s when present; if `id`s collide or the same shortcut is imported multiple times this can lead to duplicates—consider normalizing IDs and/or deduplicating based on a stable key.

## Individual Comments

### Comment 1
<location> `src/entrypoints/background.ts:182-183` </location>
<code_context>
+    if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
+      ;(async () => {
+        const raw = await loadKeys()
+        const existing = JSON.parse(raw || '[]')
+        const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
+        const merged = existing.concat(newShortcuts)
+        await saveKeys(merged)
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Consider validating and normalizing imported shortcuts before merging them into storage.

Since `message.shortcuts` comes from an external origin, it’s currently written to storage with arbitrary shape. It would be safer to whitelist/normalize the expected fields and enforce a reasonable limit on the number of shortcuts before saving, so external pages can’t inject unexpected data into extension storage.

Suggested implementation:

```typescript
  const MAX_IMPORTED_SHORTCUTS = 100
  const MAX_TOTAL_SHORTCUTS = 1000

  const normalizeImportedShortcut = (s: any) => {
    if (!s || typeof s !== 'object') return null
    const normalized: any = {}

    if (typeof s.key === 'string') normalized.key = s.key
    if (typeof s.action === 'string') normalized.action = s.action
    if (typeof s.label === 'string') normalized.label = s.label

    if (Array.isArray(s.sites)) {
      normalized.sites = s.sites
        .filter((site: any) => typeof site === 'string')
        .slice(0, 50)
    }

    if (typeof s.blacklist === 'boolean') normalized.blacklist = s.blacklist
    if (typeof s.active === 'boolean') normalized.active = s.active
    if (typeof s.activeInInputs === 'boolean') {
      normalized.activeInInputs = s.activeInInputs
    }

    // Require at least a key and action for a valid shortcut
    if (!normalized.key || !normalized.action) return null

    normalized.id = typeof s.id === 'string' ? s.id : uuid()

    return normalized
  }

  // Handle imports from shortkeys.app share links

```

```typescript
  chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
    if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
      ;(async () => {
        const raw = await loadKeys()
        const existing = JSON.parse(raw || '[]')

        // Limit number of imported shortcuts and normalize their shape
        const limitedIncoming = message.shortcuts.slice(0, MAX_IMPORTED_SHORTCUTS)
        const newShortcuts = limitedIncoming
          .map(normalizeImportedShortcut)
          .filter((s): s is any => s !== null)

        const merged = existing.concat(newShortcuts).slice(0, MAX_TOTAL_SHORTCUTS)

        await saveKeys(merged)
        sendResponse({ success: true, count: newShortcuts.length })
      })()
      return true
    }
  })
})

```
</issue_to_address>

### Comment 2
<location> `site/share.html:126-133` </location>
<code_context>

       document.getElementById('copy-json').style.display = 'block'
+
+      function importToExtension() {
+        const btn = document.getElementById('import-btn')
+        btn.disabled = true
+        btn.innerHTML = 'Importing...'
+
+        // Try sending to the extension
+        if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
+          chrome.runtime.sendMessage(STORE_ID, {
+            action: 'importShortcuts',
+            shortcuts: shortcutsData
</code_context>

<issue_to_address>
**suggestion:** Add a timeout or fallback for cases where sendMessage never invokes the callback.

If `chrome.runtime.sendMessage` never calls its callback (e.g., background script not reachable or crashes), the button will stay disabled with `Importing...`. Consider adding a timeout that checks if the button is still in the loading state after a few seconds and then calls `showFallback(btn)` so the UI can recover in this scenario.
</issue_to_address>

### Comment 3
<location> `wxt.config.ts:142-144` </location>
<code_context>
       'userScripts',
     ],
     host_permissions: ['*://*/*'],
+    externally_connectable: {
+      matches: ['https://shortkeys.app/*', 'http://localhost/*'],
+    },
     content_security_policy: {
</code_context>

<issue_to_address>
**🚨 suggestion (security):** Consider scoping the localhost externally_connectable entry to development only.

This configuration lets any localhost origin send external messages to the extension in all builds. If this is only for local development/testing, consider narrowing it to a specific port/path or including the localhost entry only in dev builds to avoid overly broad permissions in production.

```suggestion
    externally_connectable: {
      matches: [
        'https://shortkeys.app/*',
        ...(process.env.NODE_ENV === 'development' ? ['http://localhost/*'] : []),
      ],
    },
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +182 to +183
const existing = JSON.parse(raw || '[]')
const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 suggestion (security): Consider validating and normalizing imported shortcuts before merging them into storage.

Since message.shortcuts comes from an external origin, it’s currently written to storage with arbitrary shape. It would be safer to whitelist/normalize the expected fields and enforce a reasonable limit on the number of shortcuts before saving, so external pages can’t inject unexpected data into extension storage.

Suggested implementation:

  const MAX_IMPORTED_SHORTCUTS = 100
  const MAX_TOTAL_SHORTCUTS = 1000

  const normalizeImportedShortcut = (s: any) => {
    if (!s || typeof s !== 'object') return null
    const normalized: any = {}

    if (typeof s.key === 'string') normalized.key = s.key
    if (typeof s.action === 'string') normalized.action = s.action
    if (typeof s.label === 'string') normalized.label = s.label

    if (Array.isArray(s.sites)) {
      normalized.sites = s.sites
        .filter((site: any) => typeof site === 'string')
        .slice(0, 50)
    }

    if (typeof s.blacklist === 'boolean') normalized.blacklist = s.blacklist
    if (typeof s.active === 'boolean') normalized.active = s.active
    if (typeof s.activeInInputs === 'boolean') {
      normalized.activeInInputs = s.activeInInputs
    }

    // Require at least a key and action for a valid shortcut
    if (!normalized.key || !normalized.action) return null

    normalized.id = typeof s.id === 'string' ? s.id : uuid()

    return normalized
  }

  // Handle imports from shortkeys.app share links
  chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
    if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
      ;(async () => {
        const raw = await loadKeys()
        const existing = JSON.parse(raw || '[]')

        // Limit number of imported shortcuts and normalize their shape
        const limitedIncoming = message.shortcuts.slice(0, MAX_IMPORTED_SHORTCUTS)
        const newShortcuts = limitedIncoming
          .map(normalizeImportedShortcut)
          .filter((s): s is any => s !== null)

        const merged = existing.concat(newShortcuts).slice(0, MAX_TOTAL_SHORTCUTS)

        await saveKeys(merged)
        sendResponse({ success: true, count: newShortcuts.length })
      })()
      return true
    }
  })
})

Comment on lines +126 to +133
function importToExtension() {
const btn = document.getElementById('import-btn')
btn.disabled = true
btn.innerHTML = 'Importing...'

// Try sending to the extension
if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage) {
chrome.runtime.sendMessage(STORE_ID, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion: Add a timeout or fallback for cases where sendMessage never invokes the callback.

If chrome.runtime.sendMessage never calls its callback (e.g., background script not reachable or crashes), the button will stay disabled with Importing.... Consider adding a timeout that checks if the button is still in the loading state after a few seconds and then calls showFallback(btn) so the UI can recover in this scenario.

Comment on lines +142 to +144
externally_connectable: {
matches: ['https://shortkeys.app/*', 'http://localhost/*'],
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 suggestion (security): Consider scoping the localhost externally_connectable entry to development only.

This configuration lets any localhost origin send external messages to the extension in all builds. If this is only for local development/testing, consider narrowing it to a specific port/path or including the localhost entry only in dev builds to avoid overly broad permissions in production.

Suggested change
externally_connectable: {
matches: ['https://shortkeys.app/*', 'http://localhost/*'],
},
externally_connectable: {
matches: [
'https://shortkeys.app/*',
...(process.env.NODE_ENV === 'development' ? ['http://localhost/*'] : []),
],
},

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 3 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/entrypoints/background.ts">

<violation number="1" location="src/entrypoints/background.ts:178">
P1: Security: The `_sender` origin is not validated. Even though `externally_connectable` restricts which origins can send messages, explicitly checking `_sender.origin` is a critical defense-in-depth measure — especially since `http://localhost/*` is in the allowlist, meaning any local dev server could invoke this endpoint to inject shortcuts (including `javascript` action shortcuts with arbitrary code).</violation>

<violation number="2" location="src/entrypoints/background.ts:183">
P1: Security: Imported shortcuts are spread (`...s`) without any property validation or sanitization. Since the extension executes JavaScript from shortcuts with `action: 'javascript'` (via `registerUserScript`), an external sender can inject arbitrary JS code that runs on all pages by including `{ action: 'javascript', code: '...' }` in the shortcuts array. At minimum, validate/allowlist the expected properties and consider rejecting or stripping `javascript` action types from external imports.</violation>
</file>

<file name="site/share.html">

<violation number="1" location="site/share.html:151">
P2: The fallback silently calls copyJson from an async callback and always shows “✓ Copied!” even though Clipboard.writeText requires transient user activation and may reject after the sendMessage callback. This can mislead users and produce unhandled rejections. Consider only showing success after the writeText promise resolves, or prompt users to click the explicit “Copy JSON” link when clipboard write isn’t allowed.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

})

// Handle imports from shortkeys.app share links
chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

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

P1: Security: The _sender origin is not validated. Even though externally_connectable restricts which origins can send messages, explicitly checking _sender.origin is a critical defense-in-depth measure — especially since http://localhost/* is in the allowlist, meaning any local dev server could invoke this endpoint to inject shortcuts (including javascript action shortcuts with arbitrary code).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/background.ts, line 178:

<comment>Security: The `_sender` origin is not validated. Even though `externally_connectable` restricts which origins can send messages, explicitly checking `_sender.origin` is a critical defense-in-depth measure — especially since `http://localhost/*` is in the allowlist, meaning any local dev server could invoke this endpoint to inject shortcuts (including `javascript` action shortcuts with arbitrary code).</comment>

<file context>
@@ -173,4 +173,19 @@ export default defineBackground(() => {
   })
+
+  // Handle imports from shortkeys.app share links
+  chrome.runtime.onMessageExternal.addListener((message, _sender, sendResponse) => {
+    if (message.action === 'importShortcuts' && Array.isArray(message.shortcuts)) {
+      ;(async () => {
</file context>
Fix with Cubic

;(async () => {
const raw = await loadKeys()
const existing = JSON.parse(raw || '[]')
const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

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

P1: Security: Imported shortcuts are spread (...s) without any property validation or sanitization. Since the extension executes JavaScript from shortcuts with action: 'javascript' (via registerUserScript), an external sender can inject arbitrary JS code that runs on all pages by including { action: 'javascript', code: '...' } in the shortcuts array. At minimum, validate/allowlist the expected properties and consider rejecting or stripping javascript action types from external imports.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/background.ts, line 183:

<comment>Security: Imported shortcuts are spread (`...s`) without any property validation or sanitization. Since the extension executes JavaScript from shortcuts with `action: 'javascript'` (via `registerUserScript`), an external sender can inject arbitrary JS code that runs on all pages by including `{ action: 'javascript', code: '...' }` in the shortcuts array. At minimum, validate/allowlist the expected properties and consider rejecting or stripping `javascript` action types from external imports.</comment>

<file context>
@@ -173,4 +173,19 @@ export default defineBackground(() => {
+      ;(async () => {
+        const raw = await loadKeys()
+        const existing = JSON.parse(raw || '[]')
+        const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
+        const merged = existing.concat(newShortcuts)
+        await saveKeys(merged)
</file context>
Fix with Cubic


function showFallback(btn) {
// Silently copy JSON and show success
copyJson()
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

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

P2: The fallback silently calls copyJson from an async callback and always shows “✓ Copied!” even though Clipboard.writeText requires transient user activation and may reject after the sendMessage callback. This can mislead users and produce unhandled rejections. Consider only showing success after the writeText promise resolves, or prompt users to click the explicit “Copy JSON” link when clipboard write isn’t allowed.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At site/share.html, line 151:

<comment>The fallback silently calls copyJson from an async callback and always shows “✓ Copied!” even though Clipboard.writeText requires transient user activation and may reject after the sendMessage callback. This can mislead users and produce unhandled rejections. Consider only showing success after the writeText promise resolves, or prompt users to click the explicit “Copy JSON” link when clipboard write isn’t allowed.</comment>

<file context>
@@ -111,13 +111,52 @@ <h1>Shared Shortcuts</h1>
+
+      function showFallback(btn) {
+        // Silently copy JSON and show success
+        copyJson()
+        btn.innerHTML = '✓ Copied!'
+        btn.style.background = '#059669'
</file context>
Fix with Cubic

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

The pull request introduces functionality to directly import shared shortcuts from shortkeys.app into the extension. This includes adding an import button on the website, allowing the extension to accept external messages for importing shortcuts, and configuring the extension's externally_connectable manifest property. The changes are well-structured and address the stated objectives. I've identified a few areas for improvement regarding error handling and code clarity.

Comment on lines +137 to +139
if (chrome.runtime.lastError || !response?.success) {
showFallback(btn)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The chrome.runtime.lastError can contain valuable information about why the message failed. It would be beneficial to log this error to the console for debugging purposes, especially during development. This helps in understanding potential issues with external message communication.

Suggested change
if (chrome.runtime.lastError || !response?.success) {
showFallback(btn)
return
if (chrome.runtime.lastError || !response?.success) {
console.error("Error importing shortcuts:", chrome.runtime.lastError);
showFallback(btn);
return;
}

;(async () => {
const raw = await loadKeys()
const existing = JSON.parse(raw || '[]')
const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The s: any type assertion is a bit broad. While it works, defining a more specific interface for Shortcut would improve type safety and code readability. This would help catch potential issues at compile time and make the code easier to understand for future maintainers.

Suggested change
const newShortcuts = message.shortcuts.map((s: any) => ({ ...s, id: s.id || uuid() }))
const newShortcuts = message.shortcuts.map((s: Shortcut) => ({ ...s, id: s.id || uuid() }))

crittermike and others added 2 commits February 23, 2026 06:42
When you select 'Run JavaScript' as the behavior, a 'Snippets'
button appears in the code editor header. Click it to browse a
searchable library of 26 curated scripts across 5 categories:

Page Tools (6): Dark mode, reader mode, hide sticky banners,
  increase text size, highlight links, make page editable

Productivity (6): Copy as markdown link, copy all page links,
  word count, extract emails, export table to CSV, show QR code

Content (4): Remove images, show image sizes, page outline,
  reading progress bar

Developer (5): Show layout grid, clear site storage, performance
  metrics, log all events, pretty print JSON

Media (5): Picture-in-picture, set video speed, list image URLs,
  show audio volume

Click any snippet to insert its code into the editor. Also sets
the shortcut label to the snippet name if not already set.

8 tests validate: required fields, unique IDs, valid categories,
valid JS syntax (via new Function()), searchability.
370 tests total.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Page Scripts (new behavior category):
All 26 JS snippets are now selectable directly from the Behavior
dropdown under 'Page Scripts' — no need to choose 'Run JavaScript'
and paste code manually. Includes:
- Dark mode, reader mode, hide sticky banners, bigger text
- Copy markdown link, copy all links, word count, extract emails
- Table→CSV, QR code, picture-in-picture, video speed
- Layout grid, clear storage, perf metrics, pretty-print JSON
- And 10 more

Greasyfork/OpenUserJS Import:
When using 'Run JavaScript', a new 'Import from URL' field appears
below the code editor. Paste a Greasyfork or OpenUserJS URL and
click Fetch — the extension downloads the userscript, strips the
metadata header, extracts the name, and populates the editor.
Supports:
- Greasyfork page URLs → auto-converts to raw code URL
- OpenUserJS page URLs → auto-converts to install URL
- Direct .user.js URLs

Background script handles the fetch (bypasses CORS via
host_permissions).

395 tests passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 6 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/utils/fetch-userscript.ts">

<violation number="1" location="src/utils/fetch-userscript.ts:6">
P2: Greasyfork locale codes can include hyphens (e.g., zh-CN), but the regex only matches \w+ so those URLs won’t resolve to the raw code URL. Allow hyphenated locale segments (and optional locale) so users on non-2-letter locales are handled.</violation>
</file>

<file name="src/entrypoints/options/App.vue">

<violation number="1" location="src/entrypoints/options/App.vue:443">
P2: importUserscript updates the shortcut by array index after an async fetch, but the list can be reordered or shortened while the request is pending. This can apply the userscript to the wrong shortcut or throw if the index no longer exists. Capture a stable id and re-find the shortcut after the await.</violation>
</file>

<file name="src/entrypoints/background.ts">

<violation number="1" location="src/entrypoints/background.ts:118">
P1: Security: `fetchUrl` handler fetches any arbitrary URL without validation, creating an open CORS-bypassing proxy. The extension's `*://*/*` host permissions mean this can reach any HTTP(S) origin including internal network addresses. Even though callers currently go through `resolveUserscriptUrl()`, the background handler should enforce an allowlist of permitted domains (e.g., `greasyfork.org`, `openuserjs.org`) as a defense-in-depth measure.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const action = request.action

if (action === 'fetchUrl') {
fetch(request.url)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

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

P1: Security: fetchUrl handler fetches any arbitrary URL without validation, creating an open CORS-bypassing proxy. The extension's *://*/* host permissions mean this can reach any HTTP(S) origin including internal network addresses. Even though callers currently go through resolveUserscriptUrl(), the background handler should enforce an allowlist of permitted domains (e.g., greasyfork.org, openuserjs.org) as a defense-in-depth measure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/background.ts, line 118:

<comment>Security: `fetchUrl` handler fetches any arbitrary URL without validation, creating an open CORS-bypassing proxy. The extension's `*://*/*` host permissions mean this can reach any HTTP(S) origin including internal network addresses. Even though callers currently go through `resolveUserscriptUrl()`, the background handler should enforce an allowlist of permitted domains (e.g., `greasyfork.org`, `openuserjs.org`) as a defense-in-depth measure.</comment>

<file context>
@@ -114,6 +114,14 @@ export default defineBackground(() => {
     const action = request.action
 
+    if (action === 'fetchUrl') {
+      fetch(request.url)
+        .then(r => r.text())
+        .then(text => sendResponse({ text }))
</file context>
Fix with Cubic

return
}
const { code, name } = parseUserscript(resp.text)
keys.value[index].code = code
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

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

P2: importUserscript updates the shortcut by array index after an async fetch, but the list can be reordered or shortened while the request is pending. This can apply the userscript to the wrong shortcut or throw if the index no longer exists. Capture a stable id and re-find the shortcut after the await.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/options/App.vue, line 443:

<comment>importUserscript updates the shortcut by array index after an async fetch, but the list can be reordered or shortened while the request is pending. This can apply the userscript to the wrong shortcut or throw if the index no longer exists. Capture a stable id and re-find the shortcut after the await.</comment>

<file context>
@@ -423,6 +427,29 @@ async function testJavascript(row: KeySetting) {
+      return
+    }
+    const { code, name } = parseUserscript(resp.text)
+    keys.value[index].code = code
+    userscriptMessage.value = '✓ Imported: ' + name
+    userscriptUrl.value = ''
</file context>
Fix with Cubic

crittermike and others added 3 commits February 23, 2026 07:34
…itor UI

- Removed Snippets button, modal, and all related code/CSS since
  all 26 snippets are now first-class actions in the Behavior dropdown
- Moved userscript import bar outside the dark code-editor-wrap so
  it sits cleanly below with light theme styling and a link icon
- Fixed Greasyfork URL regex to handle zh-CN locales and URLs without
  language prefix
- 11 new tests for userscript URL resolution and parsing
- 406 tests total

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…visibility

- Removed duplicate 'Copy as markdown link' from Page Scripts
  (already exists as copytitleurlmarkdown in Location)
- All action labels converted to sentence case ('Log all events'
  not 'Log All Events')
- Userscript URL import bar now only shows when action is
  'javascript' (was persisting after switching behaviors)
- Fixed shortcut-header padding so toggle doesn't clip edge

405 tests passing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…matches

The shortcut-group is now a single rounded container with overflow:
hidden. Cards inside have no border-radius — they sit flush within
the group. The group header, shortcut cards, and add-shortcut
button all share the group's rounded corners. No more sharp-meets-
round corner artifacts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ib-bsb-br ib-bsb-br merged commit 7985092 into ib-bsb-br:master Feb 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants