From 0a7575bfb7014c05e736570705f6c5f25b80813c Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Fri, 3 Jul 2026 10:25:11 +0530 Subject: [PATCH 1/7] refactor(extension): extract target-neutral recorder host and platform layer --- .../{offscreen.html => recorder.html} | 4 +- .../src/background/recorder-host.ts | 59 ++++++++++++++++ .../src/background/service-worker.ts | 70 +++---------------- .../src/platform/capabilities.ts | 14 ++++ .../src/platform/extension-protocol.ts | 4 ++ apps/chrome-extension/src/platform/target.ts | 8 +++ .../src/{offscreen => recorder}/recorder.ts | 15 +++- apps/chrome-extension/vite.config.ts | 2 +- 8 files changed, 110 insertions(+), 66 deletions(-) rename apps/chrome-extension/{offscreen.html => recorder.html} (62%) create mode 100644 apps/chrome-extension/src/background/recorder-host.ts create mode 100644 apps/chrome-extension/src/platform/capabilities.ts create mode 100644 apps/chrome-extension/src/platform/extension-protocol.ts create mode 100644 apps/chrome-extension/src/platform/target.ts rename apps/chrome-extension/src/{offscreen => recorder}/recorder.ts (98%) diff --git a/apps/chrome-extension/offscreen.html b/apps/chrome-extension/recorder.html similarity index 62% rename from apps/chrome-extension/offscreen.html rename to apps/chrome-extension/recorder.html index 07811d502c2..115b7fa7abd 100644 --- a/apps/chrome-extension/offscreen.html +++ b/apps/chrome-extension/recorder.html @@ -3,9 +3,9 @@ - Cap Recorder Offscreen + Cap Recorder - + diff --git a/apps/chrome-extension/src/background/recorder-host.ts b/apps/chrome-extension/src/background/recorder-host.ts new file mode 100644 index 00000000000..b23ff029d28 --- /dev/null +++ b/apps/chrome-extension/src/background/recorder-host.ts @@ -0,0 +1,59 @@ +// The recorder document (recorder.html) hosts capture, upload, device +// enumeration, the mic probe and the camera-preview relay. On Chrome it runs +// as an offscreen document; this module owns its lifecycle so the rest of the +// service worker never touches chrome.offscreen directly. +export const RECORDER_URL = "recorder.html"; + +let recorderDocumentCreation: Promise | null = null; + +const getRecorderContexts = async () => { + const recorderUrl = chrome.runtime.getURL(RECORDER_URL); + return new Promise>((resolve) => { + chrome.runtime.getContexts( + { + contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], + documentUrls: [recorderUrl], + }, + (contexts) => resolve(contexts), + ); + }); +}; + +export const hasRecorderHost = async () => + (await getRecorderContexts()).length > 0; + +const createOffscreenDocument = () => + new Promise((resolve, reject) => { + chrome.offscreen.createDocument( + { + url: RECORDER_URL, + reasons: ["USER_MEDIA", "DISPLAY_MEDIA", "BLOBS", "AUDIO_PLAYBACK"], + justification: "Record and upload Cap videos from an extension page.", + }, + () => { + const error = chrome.runtime.lastError; + if (!error) { + resolve(); + return; + } + + const message = error.message ?? "Failed to create offscreen document"; + if (message.toLowerCase().includes("single offscreen document")) { + resolve(); + return; + } + + reject(new Error(message)); + }, + ); + }); + +export const ensureRecorderHost = async () => { + const contexts = await getRecorderContexts(); + if (contexts.length > 0) return; + + recorderDocumentCreation ??= createOffscreenDocument().finally(() => { + recorderDocumentCreation = null; + }); + await recorderDocumentCreation; +}; diff --git a/apps/chrome-extension/src/background/service-worker.ts b/apps/chrome-extension/src/background/service-worker.ts index 43eccb06c54..b601f050e06 100644 --- a/apps/chrome-extension/src/background/service-worker.ts +++ b/apps/chrome-extension/src/background/service-worker.ts @@ -1,3 +1,4 @@ +import { EXTENSION_PROTOCOL } from "../platform/extension-protocol"; import { ApiRequestError, createAuthStart, @@ -56,6 +57,7 @@ import type { ServiceWorkerRequest, ServiceWorkerResponse, } from "../shared/types"; +import { ensureRecorderHost, hasRecorderHost } from "./recorder-host"; // popup.html is web-accessible with use_dynamic_url so sites cannot fingerprint // the extension via the overlay iframe's static URL; that same flag makes its @@ -63,7 +65,6 @@ import type { // a window. The standalone fallback therefore loads a privileged twin that is // not in web_accessible_resources. const POPUP_URL = "popup-window.html"; -const OFFSCREEN_URL = "offscreen.html"; const AUTH_TIMEOUT_MS = 10 * 60 * 1000; const OFFSCREEN_MESSAGE_ATTEMPTS = 3; const OFFSCREEN_MESSAGE_RETRY_DELAY_MS = 75; @@ -83,7 +84,6 @@ let uploadProgressTabId: number | null = null; let activePreviewTabId: number | null = null; let pendingPreviewTabId: number | null = null; let readyPreviewTabId: number | null = null; -let offscreenDocumentCreation: Promise | null = null; let browserWindowFocused = true; let externalCaptureAutoPipPending = false; @@ -197,58 +197,6 @@ const focusTab = async (tabId: number) => { await activateTab(tabId); }; -const getOffscreenDocumentContexts = async () => { - const offscreenUrl = chrome.runtime.getURL(OFFSCREEN_URL); - return new Promise>((resolve) => { - chrome.runtime.getContexts( - { - contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], - documentUrls: [offscreenUrl], - }, - (contexts) => resolve(contexts), - ); - }); -}; - -const hasOffscreenDocument = async () => - (await getOffscreenDocumentContexts()).length > 0; - -const createOffscreenDocument = () => - new Promise((resolve, reject) => { - chrome.offscreen.createDocument( - { - url: OFFSCREEN_URL, - reasons: ["USER_MEDIA", "DISPLAY_MEDIA", "BLOBS", "AUDIO_PLAYBACK"], - justification: "Record and upload Cap videos from an extension page.", - }, - () => { - const error = chrome.runtime.lastError; - if (!error) { - resolve(); - return; - } - - const message = error.message ?? "Failed to create offscreen document"; - if (message.toLowerCase().includes("single offscreen document")) { - resolve(); - return; - } - - reject(new Error(message)); - }, - ); - }); - -const ensureOffscreenDocument = async () => { - const contexts = await getOffscreenDocumentContexts(); - if (contexts.length > 0) return; - - offscreenDocumentCreation ??= createOffscreenDocument().finally(() => { - offscreenDocumentCreation = null; - }); - await offscreenDocumentCreation; -}; - const wait = (durationMs: number) => new Promise((resolve) => { globalThis.setTimeout(resolve, durationMs); @@ -279,12 +227,12 @@ const sendOffscreen = async ( options: { createIfMissing?: boolean } = {}, ) => { if (options.createIfMissing === false) { - const hasDocument = await hasOffscreenDocument(); + const hasDocument = await hasRecorderHost(); if (!hasDocument) { return { ok: true, status: recordingStatus } satisfies OffscreenResponse; } } else { - await ensureOffscreenDocument(); + await ensureRecorderHost(); } let lastError: unknown; @@ -301,7 +249,7 @@ const sendOffscreen = async ( break; } await wait(OFFSCREEN_MESSAGE_RETRY_DELAY_MS); - await ensureOffscreenDocument(); + await ensureRecorderHost(); } } @@ -441,7 +389,7 @@ const canInjectIntoTab = (tab: chrome.tabs.Tab) => { const isWebPageSender = (sender: chrome.runtime.MessageSender) => { if (!sender.tab) return false; const senderUrl = sender.url ?? ""; - return !senderUrl.startsWith("chrome-extension:"); + return !senderUrl.startsWith(EXTENSION_PROTOCOL); }; // camera-preview.html is web accessible, so any site can load it in an @@ -456,7 +404,7 @@ const isCameraPreviewRequestAllowed = async ( if (!(await isOverlayTokenRegistered(token))) return false; const senderUrl = sender.url ?? ""; - if (senderUrl.startsWith("chrome-extension:")) { + if (senderUrl.startsWith(EXTENSION_PROTOCOL)) { // The camera preview document is the only extension page that drives // the camera. try { @@ -481,7 +429,7 @@ const isCameraPreviewEventAllowed = async ( ) => { if (!token || !(await isOverlayTokenRegistered(token))) return false; const senderUrl = sender.url ?? ""; - if (!senderUrl.startsWith("chrome-extension:")) return false; + if (!senderUrl.startsWith(EXTENSION_PROTOCOL)) return false; try { return new URL(senderUrl).pathname === "/camera-preview.html"; } catch { @@ -1311,7 +1259,7 @@ const forwardToOffscreen = (type: OffscreenRequest["type"]) => sendOffscreen({ target: "offscreen", type } as OffscreenRequest); const syncRecordingStatus = async () => { - const hasDocument = await hasOffscreenDocument(); + const hasDocument = await hasRecorderHost(); if (!hasDocument) { if (isActiveRecordingStatus(recordingStatus)) { setRecordingStatusAndBroadcast({ phase: "idle" }); diff --git a/apps/chrome-extension/src/platform/capabilities.ts b/apps/chrome-extension/src/platform/capabilities.ts new file mode 100644 index 00000000000..5949cb5f226 --- /dev/null +++ b/apps/chrome-extension/src/platform/capabilities.ts @@ -0,0 +1,14 @@ +import { TARGET } from "./target"; + +// Per-target feature availability. Firefox has no chrome.offscreen or +// chrome.tabCapture, its getDisplayMedia exposes no system audio or per-tab +// surface, MV3 host permissions are user-grantable rather than granted at +// install, and getDisplayMedia requires transient user activation so capture +// cannot start without a click inside the recorder document. +export const capabilities = { + supportsTabCapture: TARGET === "chrome", + supportsOffscreen: TARGET === "chrome", + supportsSystemAudioCapture: TARGET === "chrome", + hostPermissionsGrantedAtInstall: TARGET === "chrome", + recorderNeedsUserGesture: TARGET === "firefox", +} as const; diff --git a/apps/chrome-extension/src/platform/extension-protocol.ts b/apps/chrome-extension/src/platform/extension-protocol.ts new file mode 100644 index 00000000000..51343207880 --- /dev/null +++ b/apps/chrome-extension/src/platform/extension-protocol.ts @@ -0,0 +1,4 @@ +// "chrome-extension:" on Chromium, "moz-extension:" on Firefox. Sender checks +// must use this instead of a hardcoded literal or Firefox extension pages get +// misclassified as web pages. +export const EXTENSION_PROTOCOL = new URL(chrome.runtime.getURL("")).protocol; diff --git a/apps/chrome-extension/src/platform/target.ts b/apps/chrome-extension/src/platform/target.ts new file mode 100644 index 00000000000..f4bad385e87 --- /dev/null +++ b/apps/chrome-extension/src/platform/target.ts @@ -0,0 +1,8 @@ +// Injected by vite `define` per build target; undefined under vitest, which +// runs without a define and must behave like the Chrome build. +declare const __TARGET__: "chrome" | "firefox" | undefined; + +export type ExtensionTarget = "chrome" | "firefox"; + +export const TARGET: ExtensionTarget = + typeof __TARGET__ === "undefined" ? "chrome" : __TARGET__; diff --git a/apps/chrome-extension/src/offscreen/recorder.ts b/apps/chrome-extension/src/recorder/recorder.ts similarity index 98% rename from apps/chrome-extension/src/offscreen/recorder.ts rename to apps/chrome-extension/src/recorder/recorder.ts index d9ea4377a1c..7179f9d2016 100644 --- a/apps/chrome-extension/src/offscreen/recorder.ts +++ b/apps/chrome-extension/src/recorder/recorder.ts @@ -19,7 +19,7 @@ import { RECORDING_SPOOL_LIVE_MIN_IDLE_MS, RecordingSpool, recoverRecordingSpoolSession, - selectRecordingPipeline, + selectRecordingPipelineFromSupport, shouldRetryDisplayMediaWithoutPreferences, type VideoId, } from "@cap/recorder-core"; @@ -599,6 +599,9 @@ const addAudioTracks = ({ if (streamsWithAudio.length === 0) return undefined; const audioContext = new AudioContext(); + // Autoplay policy can hand back a suspended context in a document that has + // never seen user activation, which would silently mute the mixed tracks. + void audioContext.resume().catch(() => undefined); const destination = audioContext.createMediaStreamDestination(); streamsWithAudio.forEach((stream, index) => { @@ -867,7 +870,15 @@ const startRecording = async (request: StartRecordingRequest) => { routeFirstStreamToSpeakers: request.mode === "tab", }); const hasAudio = recordingStream.getAudioTracks().length > 0; - const pipeline = selectRecordingPipeline(hasAudio); + // The extension always streams the recording through + // InstantRecordingUploader, so the container must stay streamable + // regardless of what selectRecordingPipeline's user-agent heuristic + // (written for the web recorder, and false on Firefox) would decide. + const pipeline = selectRecordingPipelineFromSupport( + hasAudio, + (candidate) => MediaRecorder.isTypeSupported(candidate), + { preferStreamingUpload: true }, + ); if (!pipeline) throw new Error("No supported recorder format is available"); const { videoCodec, audioCodec } = describeRecordingCodecs( diff --git a/apps/chrome-extension/vite.config.ts b/apps/chrome-extension/vite.config.ts index bbf6f130b36..bffb41fc238 100644 --- a/apps/chrome-extension/vite.config.ts +++ b/apps/chrome-extension/vite.config.ts @@ -15,7 +15,7 @@ export default defineConfig({ welcome: resolve(__dirname, "welcome.html"), "how-it-works": resolve(__dirname, "how-it-works.html"), uploading: resolve(__dirname, "uploading.html"), - offscreen: resolve(__dirname, "offscreen.html"), + recorder: resolve(__dirname, "recorder.html"), "camera-preview": resolve(__dirname, "camera-preview.html"), "camera-permission": resolve(__dirname, "camera-permission.html"), "service-worker": resolve( From 92c255eedfdc219666bf89005e27c19919c7237b Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Fri, 3 Jul 2026 10:33:42 +0530 Subject: [PATCH 2/7] build(extension): add TARGET-driven dual builds with Firefox manifest --- apps/chrome-extension/e2e/overlay-ui.spec.ts | 2 +- .../e2e/recording-upload.spec.ts | 2 +- .../chrome-extension/e2e/webcam-start.spec.ts | 2 +- .../manifest.chrome.json} | 0 .../manifests/manifest.firefox.json | 59 ++ apps/chrome-extension/package.json | 16 +- .../src/shared/manifest.test.ts | 109 ++ apps/chrome-extension/vite.config.ts | 23 +- .../vite.content-overlay.config.ts | 6 +- apps/chrome-extension/vite.content.config.ts | 6 +- apps/chrome-extension/vite.shared.ts | 20 + pnpm-lock.yaml | 977 +++++++++++++++++- turbo.json | 1 + 13 files changed, 1206 insertions(+), 17 deletions(-) rename apps/chrome-extension/{public/manifest.json => manifests/manifest.chrome.json} (100%) create mode 100644 apps/chrome-extension/manifests/manifest.firefox.json create mode 100644 apps/chrome-extension/src/shared/manifest.test.ts create mode 100644 apps/chrome-extension/vite.shared.ts diff --git a/apps/chrome-extension/e2e/overlay-ui.spec.ts b/apps/chrome-extension/e2e/overlay-ui.spec.ts index dc4a287ee86..c45910d9200 100644 --- a/apps/chrome-extension/e2e/overlay-ui.spec.ts +++ b/apps/chrome-extension/e2e/overlay-ui.spec.ts @@ -45,7 +45,7 @@ type ChromeGlobal = typeof globalThis & { }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const extensionPath = path.resolve(__dirname, "../dist"); +const extensionPath = path.resolve(__dirname, "../dist/chrome"); const SETTINGS_KEY = "cap-extension-settings"; const AUTH_KEY = "cap-extension-auth"; const BOOTSTRAP_CACHE_KEY = "cap-extension-bootstrap-cache"; diff --git a/apps/chrome-extension/e2e/recording-upload.spec.ts b/apps/chrome-extension/e2e/recording-upload.spec.ts index bec7fef5bec..7688b3f215e 100644 --- a/apps/chrome-extension/e2e/recording-upload.spec.ts +++ b/apps/chrome-extension/e2e/recording-upload.spec.ts @@ -55,7 +55,7 @@ type ChromeGlobal = typeof globalThis & { }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const extensionPath = path.resolve(__dirname, "../dist"); +const extensionPath = path.resolve(__dirname, "../dist/chrome"); const SETTINGS_KEY = "cap-extension-settings"; const AUTH_KEY = "cap-extension-auth"; const BOOTSTRAP_CACHE_KEY = "cap-extension-bootstrap-cache"; diff --git a/apps/chrome-extension/e2e/webcam-start.spec.ts b/apps/chrome-extension/e2e/webcam-start.spec.ts index 940cd39c0d9..b5cb1a5a6f8 100644 --- a/apps/chrome-extension/e2e/webcam-start.spec.ts +++ b/apps/chrome-extension/e2e/webcam-start.spec.ts @@ -34,7 +34,7 @@ type ChromeGlobal = typeof globalThis & { }; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const extensionPath = path.resolve(__dirname, "../dist"); +const extensionPath = path.resolve(__dirname, "../dist/chrome"); const SETTINGS_KEY = "cap-extension-settings"; const AUTH_KEY = "cap-extension-auth"; const BOOTSTRAP_CACHE_KEY = "cap-extension-bootstrap-cache"; diff --git a/apps/chrome-extension/public/manifest.json b/apps/chrome-extension/manifests/manifest.chrome.json similarity index 100% rename from apps/chrome-extension/public/manifest.json rename to apps/chrome-extension/manifests/manifest.chrome.json diff --git a/apps/chrome-extension/manifests/manifest.firefox.json b/apps/chrome-extension/manifests/manifest.firefox.json new file mode 100644 index 00000000000..a61fa8b778b --- /dev/null +++ b/apps/chrome-extension/manifests/manifest.firefox.json @@ -0,0 +1,59 @@ +{ + "manifest_version": 3, + "name": "Cap - Screen Recorder & Screen Capture", + "short_name": "Cap", + "description": "Free, open source screen recorder. Capture your screen, camera & mic in Firefox and share a video link the moment you stop.", + "version": "1.0.2", + "homepage_url": "https://cap.so", + "browser_specific_settings": { + "gecko": { + "id": "extension@cap.so", + "strict_min_version": "128.0", + "data_collection_permissions": { + "required": ["authenticationInfo", "websiteContent"] + } + } + }, + "icons": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "action": { + "default_title": "Record your screen with Cap", + "default_icon": { + "16": "icons/icon-16.png", + "32": "icons/icon-32.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "background": { + "scripts": ["assets/service-worker.js"], + "type": "module" + }, + "options_ui": { + "page": "options.html", + "open_in_tab": true + }, + "permissions": ["activeTab", "identity", "scripting", "storage"], + "host_permissions": ["http://*/*", "https://*/*", "file:///*"], + "content_scripts": [ + { + "matches": ["http://*/*", "https://*/*", "file:///*"], + "js": ["assets/content-bootstrap.js"], + "run_at": "document_idle" + } + ], + "web_accessible_resources": [ + { + "resources": ["icons/*", "content/overlay.js", "welcome.html"], + "matches": ["http://*/*", "https://*/*", "file:///*"] + }, + { + "resources": ["camera-preview.html", "popup.html"], + "matches": ["http://*/*", "https://*/*", "file:///*"] + } + ] +} diff --git a/apps/chrome-extension/package.json b/apps/chrome-extension/package.json index b77b2a3362b..3c63b68f80e 100644 --- a/apps/chrome-extension/package.json +++ b/apps/chrome-extension/package.json @@ -4,13 +4,18 @@ "private": true, "type": "module", "scripts": { - "build": "rm -rf dist && vite build && vite build --config vite.content.config.ts && vite build --config vite.content-overlay.config.ts", - "dev": "rm -rf dist && (trap 'kill 0' INT TERM; vite build --watch --config vite.content.config.ts --mode development & vite build --watch --config vite.content-overlay.config.ts --mode development & vite build --watch --mode development & wait)", + "build": "pnpm build:chrome && pnpm build:firefox", + "build:chrome": "rm -rf dist/chrome && TARGET=chrome vite build && TARGET=chrome vite build --config vite.content.config.ts && TARGET=chrome vite build --config vite.content-overlay.config.ts", + "build:firefox": "rm -rf dist/firefox && TARGET=firefox vite build && TARGET=firefox vite build --config vite.content.config.ts && TARGET=firefox vite build --config vite.content-overlay.config.ts", + "dev": "rm -rf dist/chrome && (trap 'kill 0' INT TERM; TARGET=chrome vite build --watch --config vite.content.config.ts --mode development & TARGET=chrome vite build --watch --config vite.content-overlay.config.ts --mode development & TARGET=chrome vite build --watch --mode development & wait)", + "dev:firefox": "rm -rf dist/firefox && (trap 'kill 0' INT TERM; TARGET=firefox vite build --watch --config vite.content.config.ts --mode development & TARGET=firefox vite build --watch --config vite.content-overlay.config.ts --mode development & TARGET=firefox vite build --watch --mode development & wait)", + "run:firefox": "web-ext run --source-dir dist/firefox", + "lint:firefox": "web-ext lint --source-dir dist/firefox", "typecheck": "tsc --noEmit", "test": "vitest run src", "test:e2e:install": "playwright install chromium", - "test:e2e": "pnpm build && playwright test", - "test:e2e:headed": "pnpm build && playwright test --headed" + "test:e2e": "pnpm build:chrome && playwright test", + "test:e2e:headed": "pnpm build:chrome && playwright test --headed" }, "dependencies": { "@cap/recorder-core": "workspace:*", @@ -36,6 +41,7 @@ "tailwindcss": "^3.4.0", "typescript": "^5.8.3", "vite": "6.3.5", - "vitest": "^3.2.0" + "vitest": "^3.2.0", + "web-ext": "^8.9.0" } } diff --git a/apps/chrome-extension/src/shared/manifest.test.ts b/apps/chrome-extension/src/shared/manifest.test.ts new file mode 100644 index 00000000000..923ad4088a3 --- /dev/null +++ b/apps/chrome-extension/src/shared/manifest.test.ts @@ -0,0 +1,109 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +// The two manifests are maintained by hand; these assertions keep the parts +// that must not drift (version, entry points, content scripts, resources) in +// lockstep and pin the deliberate per-browser differences. + +type Manifest = { + manifest_version: number; + name: string; + short_name: string; + version: string; + homepage_url: string; + icons: Record; + action: unknown; + background: { + service_worker?: string; + scripts?: string[]; + type?: string; + }; + permissions: string[]; + host_permissions: string[]; + content_scripts: unknown[]; + web_accessible_resources: Array<{ + resources: string[]; + matches: string[]; + use_dynamic_url?: boolean; + }>; + options_page?: string; + options_ui?: { page: string }; + browser_specific_settings?: { + gecko?: { + id?: string; + strict_min_version?: string; + }; + }; +}; + +const loadManifest = (target: "chrome" | "firefox"): Manifest => + JSON.parse( + readFileSync( + resolve(__dirname, `../../manifests/manifest.${target}.json`), + "utf8", + ), + ); + +const chrome = loadManifest("chrome"); +const firefox = loadManifest("firefox"); + +describe("manifest parity", () => { + it("keeps shared identity fields in sync", () => { + expect(firefox.manifest_version).toBe(chrome.manifest_version); + expect(firefox.name).toBe(chrome.name); + expect(firefox.short_name).toBe(chrome.short_name); + expect(firefox.version).toBe(chrome.version); + expect(firefox.homepage_url).toBe(chrome.homepage_url); + expect(firefox.icons).toEqual(chrome.icons); + expect(firefox.action).toEqual(chrome.action); + }); + + it("keeps content scripts and host permissions in sync", () => { + expect(firefox.content_scripts).toEqual(chrome.content_scripts); + expect(firefox.host_permissions).toEqual(chrome.host_permissions); + }); + + it("exposes the same web accessible resources (modulo use_dynamic_url)", () => { + const strip = (entries: Manifest["web_accessible_resources"]) => + entries.map(({ resources, matches }) => ({ resources, matches })); + expect(strip(firefox.web_accessible_resources)).toEqual( + strip(chrome.web_accessible_resources), + ); + // use_dynamic_url is Chromium-only. + expect( + firefox.web_accessible_resources.every( + (entry) => entry.use_dynamic_url === undefined, + ), + ).toBe(true); + }); + + it("points both backgrounds at the same script", () => { + expect(chrome.background.service_worker).toBe("assets/service-worker.js"); + expect(firefox.background.scripts).toEqual(["assets/service-worker.js"]); + expect(firefox.background.service_worker).toBeUndefined(); + }); + + it("only differs in the Chrome-only permissions", () => { + expect(chrome.permissions).toEqual( + expect.arrayContaining(["offscreen", "tabCapture"]), + ); + const chromeOnly = ["offscreen", "tabCapture"]; + expect(firefox.permissions.filter((p) => chromeOnly.includes(p))).toEqual( + [], + ); + expect([...firefox.permissions].sort()).toEqual( + chrome.permissions.filter((p) => !chromeOnly.includes(p)).sort(), + ); + }); + + it("declares Firefox-specific settings and options_ui", () => { + expect(firefox.browser_specific_settings?.gecko?.id).toBeTruthy(); + expect( + firefox.browser_specific_settings?.gecko?.strict_min_version, + ).toBeTruthy(); + expect(firefox.options_ui?.page).toBe(chrome.options_page); + expect(firefox.options_page).toBeUndefined(); + expect(chrome.browser_specific_settings).toBeUndefined(); + }); +}); diff --git a/apps/chrome-extension/vite.config.ts b/apps/chrome-extension/vite.config.ts index bffb41fc238..20591cb63d4 100644 --- a/apps/chrome-extension/vite.config.ts +++ b/apps/chrome-extension/vite.config.ts @@ -1,12 +1,29 @@ +import { copyFileSync } from "node:fs"; import { resolve } from "node:path"; import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; +import { defineConfig, type Plugin } from "vite"; +import { outDirFor, resolveTarget, targetDefine } from "./vite.shared"; + +const target = resolveTarget(); + +// The manifests live outside public/ because public/ is copied verbatim into +// every outDir, which would ship the Chrome manifest into the Firefox build. +const copyManifest = (): Plugin => ({ + name: "cap-copy-manifest", + closeBundle() { + copyFileSync( + resolve(__dirname, `manifests/manifest.${target}.json`), + resolve(__dirname, outDirFor(target), "manifest.json"), + ); + }, +}); export default defineConfig({ - plugins: [react()], + plugins: [react(), copyManifest()], + define: targetDefine(target), build: { emptyOutDir: false, - outDir: "dist", + outDir: outDirFor(target), rollupOptions: { input: { popup: resolve(__dirname, "popup.html"), diff --git a/apps/chrome-extension/vite.content-overlay.config.ts b/apps/chrome-extension/vite.content-overlay.config.ts index 91b72a1ad93..959f03a5450 100644 --- a/apps/chrome-extension/vite.content-overlay.config.ts +++ b/apps/chrome-extension/vite.content-overlay.config.ts @@ -1,6 +1,9 @@ import { resolve } from "node:path"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +import { outDirFor, resolveTarget, targetDefine } from "./vite.shared"; + +const target = resolveTarget(); // The full overlay/recording-bar UI, lazily import()ed by the bootstrap // content script. It must be an ES module (the bootstrap dynamic-imports @@ -8,9 +11,10 @@ import { defineConfig } from "vite"; // manifest's web_accessible_resources entry can list it. export default defineConfig({ plugins: [react()], + define: targetDefine(target), build: { emptyOutDir: false, - outDir: "dist", + outDir: outDirFor(target), rollupOptions: { input: resolve(__dirname, "src/content/overlay.tsx"), // Keep the init() export the bootstrap calls after import(). diff --git a/apps/chrome-extension/vite.content.config.ts b/apps/chrome-extension/vite.content.config.ts index 012b5b95bcb..d82b00d3482 100644 --- a/apps/chrome-extension/vite.content.config.ts +++ b/apps/chrome-extension/vite.content.config.ts @@ -1,5 +1,8 @@ import { resolve } from "node:path"; import { defineConfig } from "vite"; +import { outDirFor, resolveTarget, targetDefine } from "./vite.shared"; + +const target = resolveTarget(); // Chrome runs manifest content scripts as classic scripts, so this entry // must be a single self-contained IIFE with no ES module imports. Only the @@ -7,9 +10,10 @@ import { defineConfig } from "vite"; // real overlay UI (built by vite.content-overlay.config.ts) from // web_accessible_resources when a tab actually needs it. export default defineConfig({ + define: targetDefine(target), build: { emptyOutDir: false, - outDir: "dist", + outDir: outDirFor(target), rollupOptions: { input: resolve(__dirname, "src/content/bootstrap.ts"), output: { diff --git a/apps/chrome-extension/vite.shared.ts b/apps/chrome-extension/vite.shared.ts new file mode 100644 index 00000000000..9f3aa88928b --- /dev/null +++ b/apps/chrome-extension/vite.shared.ts @@ -0,0 +1,20 @@ +// Shared between vite.config.ts, vite.content.config.ts and +// vite.content-overlay.config.ts so all three chunks of one target build +// agree on the output directory and the injected __TARGET__ constant. +export type BuildTarget = "chrome" | "firefox"; + +export const resolveTarget = (): BuildTarget => { + const target = process.env.TARGET ?? "chrome"; + if (target !== "chrome" && target !== "firefox") { + throw new Error( + `Unknown TARGET "${target}" (expected "chrome" or "firefox")`, + ); + } + return target; +}; + +export const outDirFor = (target: BuildTarget) => `dist/${target}`; + +export const targetDefine = (target: BuildTarget) => ({ + __TARGET__: JSON.stringify(target), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e13e2503e9..4504a01d2d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: vitest: specifier: ^3.2.0 version: 3.2.6(@types/debug@4.1.13)(@types/node@22.20.0)(@vitest/ui@3.2.6)(jiti@1.21.7)(jsdom@26.1.0)(lightningcss@1.32.0)(terser@5.48.0)(yaml@2.9.0) + web-ext: + specifier: ^8.9.0 + version: 8.10.0 apps/desktop: dependencies: @@ -300,7 +303,7 @@ importers: version: 4.3.2 '@tailwindcss/typography': specifier: ^0.5.9 - version: 0.5.20(tailwindcss@3.4.19(yaml@2.9.0)) + version: 0.5.20(tailwindcss@4.3.2) '@tauri-apps/cli': specifier: '>=2.1.0' version: 2.11.4 @@ -2345,6 +2348,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.4': + resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} @@ -2612,6 +2619,19 @@ packages: resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} engines: {node: '>=6.0.0'} + '@devicefarmer/adbkit-logcat@2.1.3': + resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} + engines: {node: '>= 4'} + + '@devicefarmer/adbkit-monkey@1.2.1': + resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} + engines: {node: '>= 0.10.4'} + + '@devicefarmer/adbkit@3.3.8': + resolution: {integrity: sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw==} + engines: {node: '>= 0.10.4'} + hasBin: true + '@dnd-kit/accessibility@3.1.1': resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} peerDependencies: @@ -3896,6 +3916,10 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fluent/syntax@0.19.0': + resolution: {integrity: sha512-5D2qVpZrgpjtqU4eNOcWGp1gnUCgjfM+vKGE2y03kKN6z5EBhtx0qdRFbg8QuNNj8wXNoX93KJoYb+NqoxswmQ==} + engines: {node: '>=14.0.0', npm: '>=7.0.0'} + '@fontsource/geist-sans@5.2.5': resolution: {integrity: sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==} @@ -3926,6 +3950,10 @@ packages: '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7 react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@fregante/relaxed-json@2.0.0': + resolution: {integrity: sha512-PyUXQWB42s4jBli435TDiYuVsadwRHnMc27YaLouINktvTWsL3FcKrRMGawTayFk46X+n5bE23RjUTWQwrukWw==} + engines: {node: '>= 0.10.0'} + '@gar/promise-retry@1.0.3': resolution: {integrity: sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==} engines: {node: ^20.17.0 || >=22.9.0} @@ -4396,6 +4424,9 @@ packages: '@mattrax/mysql-planetscale@0.0.3': resolution: {integrity: sha512-A6M1qF1RxlwAWLY7ETKH/t3yaH6FVHlr5YkYfjNPMKL+c7T9HDtYyI05/0j7HeEwTOtsltedRVLl7yAQSHSU7Q==} + '@mdn/browser-compat-data@7.1.5': + resolution: {integrity: sha512-j7XpHbvSU3GOtXawlGq6TIETj0wgcE9o7FXu88ragQE/ak8/OsqOxn+wZ2v/2QUe8vrVhb9OJes+gHgyGEOYEQ==} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -5331,6 +5362,18 @@ packages: engines: {node: '>=18'} hasBin: true + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@3.0.3': + resolution: {integrity: sha512-//0sR/cow/s4ICQaYoAobOl4aU8cjU6x/V24V7XkKotb9+O+3zySIYp146vpaobYHnxa4pZX8NkV54Z5AwbDKA==} + engines: {node: '>=12'} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -8027,6 +8070,9 @@ packages: '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/minimatch@3.0.5': + resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} + '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -8134,6 +8180,9 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@typescript-eslint/eslint-plugin@5.62.0': resolution: {integrity: sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -8747,6 +8796,35 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + addons-linter@7.20.0: + resolution: {integrity: sha512-c6drrGsO91oGyqEdEZ3KIBFrSiSlhM5CKxoebBatH1l2vN1ByuRAlp9YUHLCjypLONThPcb+tVTA7X6yKIhpsw==} + engines: {node: '>=18.0.0'} + hasBin: true + + addons-moz-compare@1.3.0: + resolution: {integrity: sha512-/rXpQeaY0nOKhNx00pmZXdk5Mu+KhVlL3/pSBuAYwrxRrNiTvI/9xfQI8Lmm7DMMl+PDhtfAHY/0ibTpdeoQQQ==} + + addons-scanner-utils@9.13.0: + resolution: {integrity: sha512-8OnHK/pbvgbCejGlnEYw+V3URSTVHLkMZmV270QtNh8N9pAgK10IaiJ9DcL0FsrufZ9HxRcR8/wkavh1FgK6Kg==} + peerDependencies: + body-parser: 1.20.3 + express: 4.21.2 + node-fetch: 2.6.11 + safe-compare: 1.1.4 + peerDependenciesMeta: + body-parser: + optional: true + express: + optional: true + node-fetch: + optional: true + safe-compare: + optional: true + + adm-zip@0.5.18: + resolution: {integrity: sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==} + engines: {node: '>=12.0'} + aes-js@3.1.2: resolution: {integrity: sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==} @@ -8765,6 +8843,9 @@ packages: ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -8868,6 +8949,10 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} + array-differ@4.0.0: + resolution: {integrity: sha512-Q6VPTLMsmXZ47ENG3V+wQyZS1ZxXMxFyYzA+Z/GMrJ6yIutAIEf9wTyroTzmGjNfox9/h3GdGBCVh43GVFx4Uw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -8879,6 +8964,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + array-union@3.0.1: + resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} + engines: {node: '>=12'} + array.prototype.findlast@1.2.5: resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} engines: {node: '>= 0.4'} @@ -8959,6 +9048,13 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + atomically@2.1.1: + resolution: {integrity: sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==} + autoprefixer@10.5.2: resolution: {integrity: sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==} engines: {node: ^10 || ^12 || >=14} @@ -9261,6 +9357,9 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} @@ -9418,6 +9517,13 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -9451,6 +9557,11 @@ packages: engines: {node: '>=12.13.0'} hasBin: true + chrome-launcher@1.2.0: + resolution: {integrity: sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q==} + engines: {node: '>=12.13.0'} + hasBin: true + chromium-edge-launcher@0.2.0: resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} @@ -9556,6 +9667,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + columnify@1.6.0: + resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==} + engines: {node: '>=8.0.0'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -9574,6 +9689,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@2.9.0: + resolution: {integrity: sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A==} + engines: {node: '>= 0.6.x'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -9590,10 +9709,18 @@ packages: resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} engines: {node: '>= 12'} + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + common-ancestor-path@2.0.0: resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} engines: {node: '>= 18'} + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -9615,6 +9742,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + concat-stream@2.0.0: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} @@ -9630,6 +9761,13 @@ packages: confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + configstore@7.1.0: + resolution: {integrity: sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==} + engines: {node: '>=18'} + connect@3.7.0: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} @@ -9871,6 +10009,9 @@ packages: sqlite3: optional: true + debounce@1.2.1: + resolution: {integrity: sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==} + debounce@2.2.0: resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} engines: {node: '>=18'} @@ -9891,6 +10032,15 @@ packages: supports-color: optional: true + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -9909,6 +10059,10 @@ packages: supports-color: optional: true + decamelize@6.0.1: + resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -9934,6 +10088,10 @@ packages: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -10068,6 +10226,10 @@ packages: resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} engines: {node: '>=20'} + dot-prop@9.0.0: + resolution: {integrity: sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==} + engines: {node: '>=18'} + dotenv-cli@10.0.0: resolution: {integrity: sha512-lnOnttzfrzkRx2echxJHQRB6vOAMSCzzZg79IxpC00tU42wZPuZkQxNNrrwVAxaQZIIh001l4PxVlCrBxngBzA==} hasBin: true @@ -10304,6 +10466,9 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} @@ -10358,6 +10523,9 @@ packages: es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -10403,6 +10571,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -10504,6 +10676,11 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 + eslint-plugin-no-unsanitized@4.1.4: + resolution: {integrity: sha512-cjAoZoq3J+5KJuycYYOWrc0/OpZ7pl2Z3ypfFq4GtaAgheg+L7YGxUo2YS3avIvo/dYU5/zR2hXu3v81M9NxhQ==} + peerDependencies: + eslint: ^8 || ^9 + eslint-plugin-prettier@4.2.5: resolution: {integrity: sha512-9Ni+xgemM2IWLq6aXEpP2+V/V30GeA/46Ar629vcMqVPodFFWC9skHu/D1phvuqtS8bJCFnNf01/qcmqYEwNfg==} engines: {node: '>=12.0.0'} @@ -11012,12 +11189,19 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -11041,6 +11225,9 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -11139,6 +11326,15 @@ packages: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} engines: {node: '>=18'} + firefox-profile@4.7.0: + resolution: {integrity: sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ==} + engines: {node: '>=18'} + hasBin: true + + first-chunk-stream@3.0.0: + resolution: {integrity: sha512-LNRvR4hr/S8cXXkIY5pTgVP7L3tq6LlYWcg9nWBuW7o1NMxKZo6oOVa/6GIekMGI0Iw7uC+HWimMe9u/VAeKqw==} + engines: {node: '>=8'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -11276,6 +11472,10 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fx-runner@1.4.0: + resolution: {integrity: sha512-rci1g6U0rdTg6bAaBboP7XdRu01dzTAaKXxFf+PUqGuCv6Xu7o8NZdY1D5MvKGIjb6EdS1g3VlXOgksir1uGkg==} + hasBin: true + geist@1.7.2: resolution: {integrity: sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==} peerDependencies: @@ -11386,6 +11586,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@13.24.0: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} @@ -11433,15 +11637,24 @@ packages: resolution: {integrity: sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==} engines: {node: '>=20'} + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graceful-readlink@1.0.1: + resolution: {integrity: sha512-8tLu60LgxF6XpdbK8OW3FA+IfTNBn1ZHGHKF4KQbEeSkajYw5PlYJcKluntgegDPTg8UkHjpet1T82vk6TQ68w==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} groq-sdk@0.29.0: resolution: {integrity: sha512-oDbTqE5yg7mEzSRG0WnvezA/loaM6rY+hHl1Es8MbeOs/zgZFw+DZu3zTXJ0ik4fdNQ3/N6MluOjmytUMkZHtQ==} + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + gzip-size@7.0.0: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -11689,6 +11902,11 @@ packages: engines: {node: '>=16.x'} hasBin: true + image-size@2.0.2: + resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} + engines: {node: '>=16.x'} + hasBin: true + immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} @@ -11717,10 +11935,21 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@2.0.0: resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} engines: {node: '>=10'} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -11787,6 +12016,10 @@ packages: iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-absolute@0.1.7: + resolution: {integrity: sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA==} + engines: {node: '>=0.10.0'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -11801,6 +12034,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-arrayish@0.3.4: resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} @@ -11879,6 +12115,11 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ci@1.0.0: + resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} + engines: {node: '>=18'} + hasBin: true + is-in-ssh@1.0.0: resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} engines: {node: '>=20'} @@ -11888,6 +12129,10 @@ packages: engines: {node: '>=14.16'} hasBin: true + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -11903,6 +12148,10 @@ packages: resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} engines: {node: '>= 0.4'} + is-npm@6.1.0: + resolution: {integrity: sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-number-object@1.1.1: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} @@ -11947,6 +12196,10 @@ packages: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} + is-relative@0.1.3: + resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==} + engines: {node: '>=0.10.0'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -11987,6 +12240,9 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-utf8@0.2.1: + resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -12021,6 +12277,9 @@ packages: isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@1.1.2: + resolution: {integrity: sha512-d2eJzK691yZwPHcv1LbeAOa91yMJ9QmfTgSO1oXB65ezVhXQsxBac2vEB4bMVms9cGzaA99n6V2viHMq82VLDw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -12151,6 +12410,9 @@ packages: jose@5.6.3: resolution: {integrity: sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==} + jose@5.9.6: + resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -12199,6 +12461,13 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-merge-patch@1.0.2: + resolution: {integrity: sha512-M6Vp2GN9L7cfuMXiWOmHj9bEFbeC250iVtcKQbqVgEsDVYnIsrNsbU+h/Y/PkbBQCtEa4Bez+Ebv0zfbC8ObLg==} + + json-parse-even-better-errors@3.0.2: + resolution: {integrity: sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + json-parse-even-better-errors@5.0.0: resolution: {integrity: sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ==} engines: {node: ^20.17.0 || >=22.9.0} @@ -12272,6 +12541,10 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + ky@1.14.3: + resolution: {integrity: sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw==} + engines: {node: '>=18'} + lan-network@0.2.1: resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==} hasBin: true @@ -12283,6 +12556,10 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + latest-version@9.0.0: + resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} + engines: {node: '>=18'} + lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} @@ -12307,6 +12584,9 @@ packages: lighthouse-logger@1.4.2: resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -12384,6 +12664,10 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lines-and-columns@2.0.4: + resolution: {integrity: sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + listhen@1.10.0: resolution: {integrity: sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ==} hasBin: true @@ -12521,6 +12805,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + make-fetch-happen@15.0.6: resolution: {integrity: sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw==} engines: {node: ^20.17.0 || >=22.9.0} @@ -13005,6 +13292,10 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true + multimatch@6.0.0: + resolution: {integrity: sha512-I7tSVxHGPlmPN/enE3mS1aOSo6bWBfls+3HmuEeCUBCE7gWnm3cBXCBkpurzFjVRwC6Kld8lLaZ1Iv5vOcjvcQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + multipasta@0.2.7: resolution: {integrity: sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==} @@ -13195,6 +13486,9 @@ packages: node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-notifier@10.0.1: + resolution: {integrity: sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==} + node-releases@2.0.50: resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} engines: {node: '>=18'} @@ -13358,6 +13652,10 @@ packages: resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} engines: {node: ^10.13.0 || >=12.0.0} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -13444,6 +13742,10 @@ packages: resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} engines: {node: '>= 6.0'} + os-shim@0.1.3: + resolution: {integrity: sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==} + engines: {node: '>= 0.4.0'} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -13499,6 +13801,10 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-json@10.0.1: + resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} + engines: {node: '>=18'} + package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} @@ -13531,6 +13837,10 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@7.1.1: + resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} + engines: {node: '>=16'} + parse-ms@4.0.0: resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} engines: {node: '>=18'} @@ -13542,6 +13852,9 @@ packages: resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} engines: {node: '>=10'} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} @@ -13630,6 +13943,16 @@ packages: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.9.5: + resolution: {integrity: sha512-d1s98p8/4TfYhsJ09r/Azt30aYELRi6NNnZtEbqFw6BoGsdPVf5lKNK3kUwH8BmJJfpTLNuicjUQjaMbd93dVg==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -13816,6 +14139,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -13834,6 +14160,10 @@ packages: promise-call-limit@3.0.2: resolution: {integrity: sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw==} + promise-toolbox@0.21.0: + resolution: {integrity: sha512-NV8aTmpwrZv+Iys54sSFOBx3tuVaOBvvrft5PNppnxy9xpU/akHbaWIril22AB22zaPgrgwKdD0KsrM0ptUtpg==} + engines: {node: '>=6'} + promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} @@ -13853,6 +14183,9 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protobufjs@7.6.4: resolution: {integrity: sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==} engines: {node: '>=12.0.0'} @@ -13872,6 +14205,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + pupa@3.3.0: + resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} + engines: {node: '>=12.20'} + pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} @@ -13914,6 +14251,9 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + quick-lru@5.1.1: resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} engines: {node: '>=10'} @@ -13940,6 +14280,10 @@ packages: rc9@3.0.1: resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + re-resizable@6.11.2: resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==} peerDependencies: @@ -14244,6 +14588,10 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + recast@0.23.12: resolution: {integrity: sha512-dEWRjcINDu/F4l2dYx57ugBtD7HV9KXESyxhzw/MqWLeglJrsjJKqACPyUPg+6AF8mIgm+Zi0dZ3ACoIg+QtpA==} engines: {node: '>= 4'} @@ -14334,6 +14682,14 @@ packages: resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} engines: {node: '>=4'} + registry-auth-token@5.1.1: + resolution: {integrity: sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + regjsgen@0.8.0: resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} @@ -14537,6 +14893,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -14584,6 +14944,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} @@ -14680,6 +15045,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.7.3: + resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} + shell-quote@1.8.4: resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} engines: {node: '>= 0.4'} @@ -14688,6 +15056,9 @@ packages: resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + shiki@1.29.2: resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} @@ -14843,6 +15214,9 @@ packages: peerDependencies: solid-js: ^1.7 + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -14883,6 +15257,9 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + spawn-sync@1.0.15: + resolution: {integrity: sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==} + spdx-correct@3.2.0: resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} @@ -14902,6 +15279,13 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -15106,10 +15490,22 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-bom-buf@2.0.0: + resolution: {integrity: sha512-gLFNHucd6gzb8jMsl5QmZ3QgnUJmp7qn4uUSHNwEXumAp7YizoGYw19ZUVfuq4aBOQUtyn2k8X/CwzWB73W2lQ==} + engines: {node: '>=8'} + + strip-bom-stream@4.0.0: + resolution: {integrity: sha512-0ApK3iAkHv6WbgLICw/J4nhwHeDZsBxIIsOD+gHgZICL6SeJ0S9f/WZqemka9cjkTyMN5geId6e8U5WGFAn3cQ==} + engines: {node: '>=8'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-bom@5.0.0: + resolution: {integrity: sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==} + engines: {node: '>=12'} + strip-dirs@3.0.0: resolution: {integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==} @@ -15129,10 +15525,18 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} @@ -15150,6 +15554,12 @@ packages: structured-headers@0.4.1: resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} + stubborn-fs@2.0.0: + resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} + + stubborn-utils@1.0.2: + resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -15338,6 +15748,9 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@3.2.0: + resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==} + throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -15410,6 +15823,10 @@ packages: resolution: {integrity: sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==} hasBin: true + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + tmp@0.2.7: resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} @@ -15596,6 +16013,10 @@ packages: resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} engines: {node: '>=8'} + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -15987,12 +16408,20 @@ packages: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} + upath@2.0.1: + resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} + engines: {node: '>=4'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + update-notifier@7.3.1: + resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} + engines: {node: '>=18'} + uqr@0.1.3: resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} @@ -16371,6 +16800,10 @@ packages: warn-once@0.1.1: resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==} + watchpack@2.4.4: + resolution: {integrity: sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==} + engines: {node: '>=10.13.0'} + watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} engines: {node: '>=10.13.0'} @@ -16378,6 +16811,11 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-ext@8.10.0: + resolution: {integrity: sha512-RM15PF8/xBBTe3pkgTvTkPaRoJbwE4HZcKy9NeD0lHIdir23oy9kd2oC4LuOqhrtarFvhTK4HlJ6NPYXt8Isxw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -16456,6 +16894,12 @@ packages: whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + when-exit@2.1.5: + resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} + + when@3.7.7: + resolution: {integrity: sha512-9lFZp/KHoqH6bPKjbWqa+3Dg/K/r2v0X/3/G2x4DBGchVS2QX2VXL3cZV994WQVnTM1/PD71Az25nAzryEUugw==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -16472,6 +16916,10 @@ packages: resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} engines: {node: '>= 0.4'} + which@1.2.4: + resolution: {integrity: sha512-zDRAqDSBudazdfM9zpiI30Fu9ve47htYXcGi3ln0wfKu2a7SmrT6F3VDoYONu//48V8Vz4TdCRNPjtvyRO3yBA==} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -16500,6 +16948,9 @@ packages: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} + winreg@0.0.12: + resolution: {integrity: sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -16625,6 +17076,10 @@ packages: resolution: {integrity: sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==} engines: {node: '>= 6.0'} + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + xdg-portable@7.3.0: resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==} engines: {node: '>= 6.0'} @@ -16691,6 +17146,9 @@ packages: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@3.4.0: resolution: {integrity: sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==} engines: {node: '>=12'} @@ -16719,6 +17177,9 @@ packages: youch@4.1.1: resolution: {integrity: sha512-mxW3qiSnl+GRxXsaUMzv2Mbada1Y8CDltET9UxejDQe6DBYlSekghl5U5K0ReAikcHDi0G1vKZEmmo/NWAGKLA==} + zip-dir@2.0.0: + resolution: {integrity: sha512-uhlsJZWz26FLYXOD6WVuq+fIcZ3aBPGo/cFdiLlv3KNwpa52IF3ISV8fLhQLiqVu5No3VhlqlgthN6gehil1Dg==} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -17712,6 +18173,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/runtime@7.28.4': {} + '@babel/runtime@7.29.7': {} '@babel/standalone@7.29.7': {} @@ -17949,6 +18412,22 @@ snapshots: http-response-object: 3.0.2 parse-cache-control: 1.0.1 + '@devicefarmer/adbkit-logcat@2.1.3': {} + + '@devicefarmer/adbkit-monkey@1.2.1': {} + + '@devicefarmer/adbkit@3.3.8': + dependencies: + '@devicefarmer/adbkit-logcat': 2.1.3 + '@devicefarmer/adbkit-monkey': 1.2.1 + bluebird: 3.7.2 + commander: 9.5.0 + debug: 4.3.7 + node-forge: 1.4.0 + split: 1.0.1 + transitivePeerDependencies: + - supports-color + '@dnd-kit/accessibility@3.1.1(react@19.2.4)': dependencies: react: 19.2.4 @@ -19028,6 +19507,8 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@fluent/syntax@0.19.0': {} + '@fontsource/geist-sans@5.2.5': {} '@fortawesome/fontawesome-common-types@6.7.2': {} @@ -19052,6 +19533,8 @@ snapshots: prop-types: 15.8.1 react: 19.2.4 + '@fregante/relaxed-json@2.0.0': {} + '@gar/promise-retry@1.0.3': {} '@grpc/grpc-js@1.14.4': @@ -19508,6 +19991,8 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@mdn/browser-compat-data@7.1.5': {} + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 @@ -20599,6 +21084,18 @@ snapshots: dependencies: playwright: 1.61.1 + '@pnpm/config.env-replace@1.1.0': {} + + '@pnpm/network.ca-file@1.0.2': + dependencies: + graceful-fs: 4.2.10 + + '@pnpm/npm-conf@3.0.3': + dependencies: + '@pnpm/config.env-replace': 1.1.0 + '@pnpm/network.ca-file': 1.0.2 + config-chain: 1.1.13 + '@polka/url@1.0.0-next.29': {} '@poppinss/colors@4.1.6': @@ -23526,6 +24023,8 @@ snapshots: '@types/mime@1.3.5': {} + '@types/minimatch@3.0.5': {} + '@types/ms@2.1.0': {} '@types/node-fetch@2.6.13': @@ -23632,6 +24131,10 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 20.19.43 + '@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -24597,6 +25100,53 @@ snapshots: acorn@8.17.0: {} + addons-linter@7.20.0: + dependencies: + '@fluent/syntax': 0.19.0 + '@fregante/relaxed-json': 2.0.0 + '@mdn/browser-compat-data': 7.1.5 + addons-moz-compare: 1.3.0 + addons-scanner-utils: 9.13.0 + ajv: 8.17.1 + chalk: 4.1.2 + cheerio: 1.0.0-rc.12 + columnify: 1.6.0 + common-tags: 1.8.2 + deepmerge: 4.3.1 + eslint: 8.57.1 + eslint-plugin-no-unsanitized: 4.1.4(eslint@8.57.1) + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esprima: 4.0.1 + fast-json-patch: 3.1.1 + image-size: 2.0.2 + json-merge-patch: 1.0.2 + pino: 9.9.5 + semver: 7.7.2 + source-map-support: 0.5.21 + upath: 2.0.1 + yargs: 17.7.2 + yauzl: 2.10.0 + transitivePeerDependencies: + - body-parser + - express + - node-fetch + - safe-compare + - supports-color + + addons-moz-compare@1.3.0: {} + + addons-scanner-utils@9.13.0: + dependencies: + '@types/yauzl': 2.10.3 + common-tags: 1.8.2 + first-chunk-stream: 3.0.0 + strip-bom-stream: 4.0.0 + upath: 2.0.1 + yauzl: 2.10.0 + + adm-zip@0.5.18: {} + aes-js@3.1.2: {} agent-base@6.0.2: @@ -24618,6 +25168,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -24723,6 +25280,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 + array-differ@4.0.0: {} + array-flatten@1.1.1: {} array-includes@3.1.9: @@ -24738,6 +25297,8 @@ snapshots: array-union@2.1.0: {} + array-union@3.0.1: {} + array.prototype.findlast@1.2.5: dependencies: call-bind: 1.0.9 @@ -24838,6 +25399,13 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + + atomically@2.1.1: + dependencies: + stubborn-fs: 2.0.0 + when-exit: 2.1.5 + autoprefixer@10.5.2(postcss@8.5.16): dependencies: browserslist: 4.28.4 @@ -25235,6 +25803,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-crc32@0.2.13: {} + buffer-crc32@1.0.0: {} buffer-from@1.1.2: {} @@ -25409,6 +25979,25 @@ snapshots: check-error@2.1.3: {} + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.0.0-rc.12: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + htmlparser2: 8.0.2 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -25442,6 +26031,15 @@ snapshots: transitivePeerDependencies: - supports-color + chrome-launcher@1.2.0: + dependencies: + '@types/node': 20.19.43 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + chromium-edge-launcher@0.2.0: dependencies: '@types/node': 20.19.43 @@ -25547,6 +26145,11 @@ snapshots: color-convert: 2.0.1 color-string: 1.9.1 + columnify@1.6.0: + dependencies: + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -25559,6 +26162,10 @@ snapshots: commander@2.20.3: {} + commander@2.9.0: + dependencies: + graceful-readlink: 1.0.1 + commander@4.1.1: {} commander@6.2.1: {} @@ -25567,8 +26174,12 @@ snapshots: commander@8.3.0: {} + commander@9.5.0: {} + common-ancestor-path@2.0.0: {} + common-tags@1.8.2: {} + commondir@1.0.1: {} compatx@0.2.0: {} @@ -25599,6 +26210,13 @@ snapshots: concat-map@0.0.1: {} + concat-stream@1.6.2: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 @@ -25619,6 +26237,18 @@ snapshots: confbox@0.2.4: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + configstore@7.1.0: + dependencies: + atomically: 2.1.1 + dot-prop: 9.0.0 + graceful-fs: 4.2.11 + xdg-basedir: 5.1.0 + connect@3.7.0: dependencies: debug: 2.6.9 @@ -25824,6 +26454,8 @@ snapshots: drizzle-orm: 0.44.6(@cloudflare/workers-types@4.20260630.1)(@opentelemetry/api@1.9.1)(@planetscale/database@1.20.1)(bun-types@1.3.14)(mysql2@3.22.5(@types/node@22.20.0)) mysql2: 3.22.5(@types/node@22.20.0) + debounce@1.2.1: {} + debounce@2.2.0: {} debug@2.6.9: @@ -25834,6 +26466,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.0: dependencies: ms: 2.1.3 @@ -25844,6 +26480,8 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + decamelize@6.0.1: {} + decimal.js-light@2.5.1: {} decimal.js@10.6.0: {} @@ -25881,6 +26519,8 @@ snapshots: which-collection: 1.0.2 which-typed-array: 1.1.22 + deep-extend@0.6.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -25994,6 +26634,10 @@ snapshots: dependencies: type-fest: 5.7.0 + dot-prop@9.0.0: + dependencies: + type-fest: 4.41.0 + dotenv-cli@10.0.0: dependencies: cross-spawn: 7.0.6 @@ -26149,6 +26793,10 @@ snapshots: environment@1.1.0: {} + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} error-stack-parser@2.1.4: @@ -26284,6 +26932,8 @@ snapshots: es-toolkit@1.49.0: {} + es6-error@4.1.1: {} + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -26470,6 +27120,8 @@ snapshots: escalade@3.2.0: {} + escape-goat@4.0.0: {} + escape-html@1.0.3: {} escape-string-regexp@1.0.5: {} @@ -26505,7 +27157,7 @@ snapshots: eslint: 9.39.4(jiti@2.7.0) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)) @@ -26547,7 +27199,7 @@ snapshots: tinyglobby: 0.2.17 unrs-resolver: 1.12.2 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)) transitivePeerDependencies: - supports-color @@ -26617,7 +27269,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.62.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -26684,6 +27336,10 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 + eslint-plugin-no-unsanitized@4.1.4(eslint@8.57.1): + dependencies: + eslint: 8.57.1 + eslint-plugin-prettier@4.2.5(eslint-config-prettier@8.10.2(eslint@8.57.1))(eslint@8.57.1)(prettier@3.9.4): dependencies: eslint: 8.57.1 @@ -27456,10 +28112,14 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-patch@3.1.1: {} + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-redact@3.5.0: {} + fast-safe-stringify@2.1.1: {} fast-uri@3.1.3: {} @@ -27488,6 +28148,10 @@ snapshots: transitivePeerDependencies: - encoding + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -27608,6 +28272,16 @@ snapshots: semver-regex: 4.0.5 super-regex: 1.1.0 + firefox-profile@4.7.0: + dependencies: + adm-zip: 0.5.18 + fs-extra: 11.3.6 + ini: 4.1.3 + minimist: 1.2.8 + xml2js: 0.6.2 + + first-chunk-stream@3.0.0: {} + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 @@ -27729,6 +28403,15 @@ snapshots: functions-have-names@1.2.3: {} + fx-runner@1.4.0: + dependencies: + commander: 2.9.0 + shell-quote: 1.7.3 + spawn-sync: 1.0.15 + when: 3.7.7 + which: 1.2.4 + winreg: 0.0.12 + geist@1.7.2(next@16.2.1(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)): dependencies: next: 16.2.1(@opentelemetry/api@1.9.1)(@playwright/test@1.61.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -27851,6 +28534,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@13.24.0: dependencies: type-fest: 0.20.2 @@ -27909,8 +28596,12 @@ snapshots: responselike: 4.0.2 type-fest: 4.41.0 + graceful-fs@4.2.10: {} + graceful-fs@4.2.11: {} + graceful-readlink@1.0.1: {} + graphemer@1.4.0: {} groq-sdk@0.29.0: @@ -27925,6 +28616,8 @@ snapshots: transitivePeerDependencies: - encoding + growly@1.3.0: {} + gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -28248,6 +28941,8 @@ snapshots: dependencies: queue: 6.0.2 + image-size@2.0.2: {} + immediate@3.0.6: {} immer@11.1.8: {} @@ -28275,8 +28970,14 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + ini@2.0.0: {} + ini@4.1.1: {} + + ini@4.1.3: {} + ini@6.0.0: {} inline-style-parser@0.1.1: {} @@ -28342,6 +29043,10 @@ snapshots: iron-webcrypto@1.2.1: {} + is-absolute@0.1.7: + dependencies: + is-relative: 0.1.3 + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -28360,6 +29065,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-arrayish@0.3.4: {} is-async-function@2.1.1: @@ -28436,12 +29143,19 @@ snapshots: is-hexadecimal@2.0.1: {} + is-in-ci@1.0.0: {} + is-in-ssh@1.0.0: {} is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + is-interactive@2.0.0: {} is-map@2.0.3: {} @@ -28450,6 +29164,8 @@ snapshots: is-negative-zero@2.0.3: {} + is-npm@6.1.0: {} + is-number-object@1.1.1: dependencies: call-bound: 1.0.4 @@ -28486,6 +29202,8 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.4 + is-relative@0.1.3: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -28517,6 +29235,8 @@ snapshots: is-unicode-supported@2.1.0: {} + is-utf8@0.2.1: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -28546,6 +29266,8 @@ snapshots: isarray@2.0.5: {} + isexe@1.1.2: {} + isexe@2.0.0: {} isexe@3.1.5: {} @@ -28716,6 +29438,8 @@ snapshots: jose@5.6.3: {} + jose@5.9.6: {} + joycon@3.1.1: {} js-cookie@3.0.8: {} @@ -28770,6 +29494,12 @@ snapshots: json-buffer@3.0.1: {} + json-merge-patch@1.0.2: + dependencies: + fast-deep-equal: 3.1.3 + + json-parse-even-better-errors@3.0.2: {} + json-parse-even-better-errors@5.0.0: {} json-schema-traverse@0.4.1: {} @@ -28832,6 +29562,8 @@ snapshots: kolorist@1.8.0: {} + ky@1.14.3: {} + lan-network@0.2.1: {} language-subtag-registry@0.3.23: {} @@ -28840,6 +29572,10 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + latest-version@9.0.0: + dependencies: + package-json: 10.0.1 + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 @@ -28866,6 +29602,13 @@ snapshots: transitivePeerDependencies: - supports-color + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + lightningcss-android-arm64@1.32.0: optional: true @@ -28919,6 +29662,8 @@ snapshots: lines-and-columns@1.2.4: {} + lines-and-columns@2.0.4: {} + listhen@1.10.0: dependencies: '@parcel/watcher': 2.5.6 @@ -29067,6 +29812,8 @@ snapshots: dependencies: semver: 7.8.5 + make-error@1.3.6: {} + make-fetch-happen@15.0.6: dependencies: '@gar/promise-retry': 1.0.3 @@ -29932,6 +30679,13 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 + multimatch@6.0.0: + dependencies: + '@types/minimatch': 3.0.5 + array-differ: 4.0.0 + array-union: 3.0.1 + minimatch: 3.1.5 + multipasta@0.2.7: {} multitars@1.0.0: {} @@ -30281,6 +31035,15 @@ snapshots: node-mock-http@1.0.4: {} + node-notifier@10.0.1: + dependencies: + growly: 1.3.0 + is-wsl: 2.2.0 + semver: 7.8.5 + shellwords: 0.1.1 + uuid: 8.3.2 + which: 2.0.2 + node-releases@2.0.50: {} nodemailer@6.10.1: {} @@ -30470,6 +31233,8 @@ snapshots: oidc-token-hash@5.2.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -30603,6 +31368,8 @@ snapshots: os-paths@4.4.0: {} + os-shim@0.1.3: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -30649,6 +31416,13 @@ snapshots: package-json-from-dist@1.0.1: {} + package-json@10.0.1: + dependencies: + ky: 1.14.3 + registry-auth-token: 5.1.1 + registry-url: 6.0.1 + semver: 7.8.5 + package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 @@ -30705,6 +31479,14 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-json@7.1.1: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 3.0.2 + lines-and-columns: 2.0.4 + type-fest: 3.13.1 + parse-ms@4.0.0: {} parse-numeric-range@1.3.0: {} @@ -30713,6 +31495,11 @@ snapshots: dependencies: pngjs: 3.4.0 + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + parse5@7.3.0: dependencies: entities: 6.0.1 @@ -30774,6 +31561,26 @@ snapshots: pify@2.3.0: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@9.9.5: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.2.0 + pirates@4.0.7: {} piscina@4.9.3: @@ -30960,6 +31767,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@5.0.0: {} + process@0.11.10: {} proggy@4.0.0: {} @@ -30970,6 +31779,10 @@ snapshots: promise-call-limit@3.0.2: {} + promise-toolbox@0.21.0: + dependencies: + make-error: 1.3.6 + promise@7.3.1: dependencies: asap: 2.0.6 @@ -30993,6 +31806,8 @@ snapshots: property-information@7.2.0: {} + proto-list@1.2.4: {} + protobufjs@7.6.4: dependencies: '@protobufjs/aspromise': 1.1.2 @@ -31018,6 +31833,10 @@ snapshots: punycode@2.3.1: {} + pupa@3.3.0: + dependencies: + escape-goat: 4.0.0 + pure-rand@6.1.0: {} pvtsutils@1.3.6: @@ -31056,6 +31875,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} + quick-lru@5.1.1: {} radix3@1.1.2: {} @@ -31083,6 +31904,13 @@ snapshots: defu: 6.1.7 destr: 2.0.5 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + re-resizable@6.11.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -31494,6 +32322,8 @@ snapshots: readdirp@5.0.0: {} + real-require@0.2.0: {} + recast@0.23.12: dependencies: ast-types: 0.16.1 @@ -31628,6 +32458,14 @@ snapshots: unicode-match-property-ecmascript: 2.0.0 unicode-match-property-value-ecmascript: 2.2.1 + registry-auth-token@5.1.1: + dependencies: + '@pnpm/npm-conf': 3.0.3 + + registry-url@6.0.1: + dependencies: + rc: 1.2.8 + regjsgen@0.8.0: {} regjsparser@0.13.2: @@ -31937,6 +32775,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.2.1: {} @@ -31971,6 +32811,8 @@ snapshots: semver@7.6.3: {} + semver@7.7.2: {} + semver@7.7.3: {} semver@7.7.4: {} @@ -32140,10 +32982,14 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.7.3: {} + shell-quote@1.8.4: {} shell-quote@1.9.0: {} + shellwords@0.1.1: {} + shiki@1.29.2: dependencies: '@shikijs/core': 1.29.2 @@ -32367,6 +33213,10 @@ snapshots: dependencies: solid-js: 1.9.13 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: react: 19.2.4 @@ -32397,6 +33247,11 @@ snapshots: space-separated-tokens@2.0.2: {} + spawn-sync@1.0.15: + dependencies: + concat-stream: 1.6.2 + os-shim: 0.1.3 + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 @@ -32418,6 +33273,12 @@ snapshots: split-on-first@1.1.0: {} + split2@4.2.0: {} + + split@1.0.1: + dependencies: + through: 2.3.8 + sprintf-js@1.0.3: {} sql-escaper@1.3.3: {} @@ -32656,8 +33517,19 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-buf@2.0.0: + dependencies: + is-utf8: 0.2.1 + + strip-bom-stream@4.0.0: + dependencies: + first-chunk-stream: 3.0.0 + strip-bom-buf: 2.0.0 + strip-bom@3.0.0: {} + strip-bom@5.0.0: {} + strip-dirs@3.0.0: dependencies: inspect-with-kind: 1.0.5 @@ -32673,8 +33545,12 @@ snapshots: dependencies: min-indent: 1.0.1 + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + strip-literal@2.1.1: dependencies: js-tokens: 9.0.1 @@ -32694,6 +33570,12 @@ snapshots: structured-headers@0.4.1: {} + stubborn-fs@2.0.0: + dependencies: + stubborn-utils: 1.0.2 + + stubborn-utils@1.0.2: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -32928,6 +33810,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@3.2.0: + dependencies: + real-require: 0.2.0 + throat@5.0.0: {} through@2.3.8: {} @@ -32980,6 +33866,8 @@ snapshots: dependencies: tldts-core: 7.4.5 + tmp@0.2.5: {} + tmp@0.2.7: {} tmpl@1.0.5: {} @@ -33151,6 +34039,8 @@ snapshots: type-fest@0.7.1: {} + type-fest@3.13.1: {} + type-fest@4.41.0: {} type-fest@5.7.0: @@ -33578,12 +34468,27 @@ snapshots: upath@1.2.0: {} + upath@2.0.1: {} + update-browserslist-db@1.2.3(browserslist@4.28.4): dependencies: browserslist: 4.28.4 escalade: 3.2.0 picocolors: 1.1.1 + update-notifier@7.3.1: + dependencies: + boxen: 8.0.1 + chalk: 5.6.2 + configstore: 7.1.0 + is-in-ci: 1.0.0 + is-installed-globally: 1.0.0 + is-npm: 6.1.0 + latest-version: 9.0.0 + pupa: 3.3.0 + semver: 7.8.5 + xdg-basedir: 5.1.0 + uqr@0.1.3: {} uri-js@4.4.1: @@ -34219,6 +35124,11 @@ snapshots: warn-once@0.1.1: {} + watchpack@2.4.4: + dependencies: + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + watchpack@2.5.1: dependencies: glob-to-regexp: 0.4.1 @@ -34228,6 +35138,42 @@ snapshots: dependencies: defaults: 1.0.4 + web-ext@8.10.0: + dependencies: + '@babel/runtime': 7.28.4 + '@devicefarmer/adbkit': 3.3.8 + addons-linter: 7.20.0 + camelcase: 8.0.0 + chrome-launcher: 1.2.0 + debounce: 1.2.1 + decamelize: 6.0.1 + es6-error: 4.1.1 + firefox-profile: 4.7.0 + fx-runner: 1.4.0 + https-proxy-agent: 7.0.6 + jose: 5.9.6 + jszip: 3.10.1 + multimatch: 6.0.0 + node-notifier: 10.0.1 + open: 10.2.0 + parse-json: 7.1.1 + pino: 9.9.5 + promise-toolbox: 0.21.0 + source-map-support: 0.5.21 + strip-bom: 5.0.0 + strip-json-comments: 5.0.3 + tmp: 0.2.5 + update-notifier: 7.3.1 + watchpack: 2.4.4 + yargs: 17.7.2 + zip-dir: 2.0.0 + transitivePeerDependencies: + - body-parser + - express + - node-fetch + - safe-compare + - supports-color + web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} @@ -34343,6 +35289,10 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + when-exit@2.1.5: {} + + when@3.7.7: {} + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -34384,6 +35334,11 @@ snapshots: gopd: 1.2.0 has-tostringtag: 1.0.2 + which@1.2.4: + dependencies: + is-absolute: 0.1.7 + isexe: 1.1.2 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -34409,6 +35364,8 @@ snapshots: dependencies: string-width: 7.2.0 + winreg@0.0.12: {} + word-wrap@1.2.5: {} wordwrap@1.0.0: {} @@ -34555,6 +35512,8 @@ snapshots: os-paths: 4.4.0 xdg-portable: 7.3.0 + xdg-basedir@5.1.0: {} + xdg-portable@7.3.0: dependencies: os-paths: 4.4.0 @@ -34620,6 +35579,11 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + yauzl@3.4.0: dependencies: pend: 1.2.0 @@ -34655,6 +35619,11 @@ snapshots: cookie-es: 3.1.1 youch-core: 0.3.3 + zip-dir@2.0.0: + dependencies: + async: 3.2.6 + jszip: 3.10.1 + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/turbo.json b/turbo.json index bf4056ab19c..1652dfbdbcb 100644 --- a/turbo.json +++ b/turbo.json @@ -19,6 +19,7 @@ "**/*.css", "*.html", "public/**", + "manifests/**", "vite.config.ts", "vite.content.config.ts", "vite.content-overlay.config.ts", From c4421bc433d2cc548fd20e55a492f2c03d6b27a6 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Fri, 3 Jul 2026 14:32:42 +0530 Subject: [PATCH 3/7] feat(extension): add Firefox recorder window, arm gesture, and capability gating --- apps/chrome-extension/recorder.html | 49 +++++++ .../src/background/recorder-host.ts | 120 ++++++++++++++++-- .../src/background/service-worker.ts | 35 ++++- .../src/permission/camera-permission.tsx | 17 ++- .../components/recording-mode-selector.tsx | 7 +- apps/chrome-extension/src/popup/main.tsx | 42 +++++- .../src/recorder/arm-gesture.ts | 63 +++++++++ .../chrome-extension/src/recorder/recorder.ts | 3 + apps/chrome-extension/src/welcome/main.ts | 32 +++++ apps/chrome-extension/welcome.html | 19 +++ 10 files changed, 368 insertions(+), 19 deletions(-) create mode 100644 apps/chrome-extension/src/recorder/arm-gesture.ts diff --git a/apps/chrome-extension/recorder.html b/apps/chrome-extension/recorder.html index 115b7fa7abd..ffcde1d6dd9 100644 --- a/apps/chrome-extension/recorder.html +++ b/apps/chrome-extension/recorder.html @@ -4,8 +4,57 @@ Cap Recorder + + +
+ +

+ Firefox needs one click here before Cap can ask which screen or + window to share. +

+
diff --git a/apps/chrome-extension/src/background/recorder-host.ts b/apps/chrome-extension/src/background/recorder-host.ts index b23ff029d28..9d4bb1414ae 100644 --- a/apps/chrome-extension/src/background/recorder-host.ts +++ b/apps/chrome-extension/src/background/recorder-host.ts @@ -1,20 +1,40 @@ +import { capabilities } from "../platform/capabilities"; + // The recorder document (recorder.html) hosts capture, upload, device // enumeration, the mic probe and the camera-preview relay. On Chrome it runs -// as an offscreen document; this module owns its lifecycle so the rest of the -// service worker never touches chrome.offscreen directly. +// as an offscreen document; Firefox has no offscreen API, so it runs in a +// small popup window instead (minimized unless the user must interact with +// it — getDisplayMedia there requires transient user activation, which the +// page collects with an arm button). Either way the document closes itself +// once idle, so this module only ever has to create and find it. export const RECORDER_URL = "recorder.html"; -let recorderDocumentCreation: Promise | null = null; +const RECORDER_WINDOW_WIDTH = 400; +const RECORDER_WINDOW_HEIGHT = 360; +const RECORDER_READY_TIMEOUT_MS = 10_000; +const RECORDER_READY_POLL_INTERVAL_MS = 100; + +let recorderHostCreation: Promise | null = null; -const getRecorderContexts = async () => { +type RecorderContext = { + documentUrl?: string; + windowId?: number; +}; + +const getRecorderContexts = async (): Promise => { const recorderUrl = chrome.runtime.getURL(RECORDER_URL); - return new Promise>((resolve) => { + // String literals rather than the chrome.runtime.ContextType enum object, + // which Firefox does not guarantee to expose. + const contextType = ( + capabilities.supportsOffscreen ? "OFFSCREEN_DOCUMENT" : "TAB" + ) as chrome.runtime.ContextType; + return new Promise((resolve) => { chrome.runtime.getContexts( { - contextTypes: [chrome.runtime.ContextType.OFFSCREEN_DOCUMENT], + contextTypes: [contextType], documentUrls: [recorderUrl], }, - (contexts) => resolve(contexts), + (contexts) => resolve(contexts ?? []), ); }); }; @@ -22,6 +42,15 @@ const getRecorderContexts = async () => { export const hasRecorderHost = async () => (await getRecorderContexts()).length > 0; +const getRecorderWindowId = async () => { + for (const context of await getRecorderContexts()) { + if (typeof context.windowId === "number" && context.windowId >= 0) { + return context.windowId; + } + } + return null; +}; + const createOffscreenDocument = () => new Promise((resolve, reject) => { chrome.offscreen.createDocument( @@ -48,12 +77,81 @@ const createOffscreenDocument = () => ); }); -export const ensureRecorderHost = async () => { +const createRecorderWindow = (interactive: boolean) => + new Promise((resolve, reject) => { + // state cannot be combined with bounds or focus in windows.create. + const createData: chrome.windows.CreateData = interactive + ? { + url: RECORDER_URL, + type: "popup", + focused: true, + width: RECORDER_WINDOW_WIDTH, + height: RECORDER_WINDOW_HEIGHT, + } + : { url: RECORDER_URL, type: "popup", state: "minimized" }; + chrome.windows.create(createData, () => { + const error = chrome.runtime.lastError; + if (error) { + reject(new Error(error.message ?? "Failed to open the Cap recorder")); + return; + } + resolve(); + }); + }); + +const pingRecorder = () => + new Promise((resolve) => { + chrome.runtime.sendMessage( + { target: "offscreen", type: "get-recording-status" }, + (response) => { + resolve(!chrome.runtime.lastError && Boolean(response)); + }, + ); + }); + +// chrome.offscreen.createDocument resolves only once the document has loaded, +// but windows.create resolves as soon as the window exists, well before the +// recorder script runs — so the Firefox path waits until the document +// actually answers before callers start sending it real messages. +const waitForRecorderReady = async () => { + const deadline = Date.now() + RECORDER_READY_TIMEOUT_MS; + while (Date.now() < deadline) { + if (await pingRecorder()) return; + await new Promise((resolve) => { + globalThis.setTimeout(resolve, RECORDER_READY_POLL_INTERVAL_MS); + }); + } + throw new Error("The Cap recorder window did not become ready in time"); +}; + +export const ensureRecorderHost = async ( + options: { interactive?: boolean } = {}, +) => { const contexts = await getRecorderContexts(); if (contexts.length > 0) return; - recorderDocumentCreation ??= createOffscreenDocument().finally(() => { - recorderDocumentCreation = null; + recorderHostCreation ??= ( + capabilities.supportsOffscreen + ? createOffscreenDocument() + : createRecorderWindow(options.interactive === true).then( + waitForRecorderReady, + ) + ).finally(() => { + recorderHostCreation = null; + }); + await recorderHostCreation; +}; + +// Brings the Firefox recorder window forward so the user can click its arm +// button (and see any permission prompts anchored to it). No-op on Chrome. +export const focusRecorderHost = async () => { + if (capabilities.supportsOffscreen) return; + const windowId = await getRecorderWindowId(); + if (windowId === null) return; + await new Promise((resolve) => { + chrome.windows.update(windowId, { focused: true, state: "normal" }, () => { + void chrome.runtime.lastError; + resolve(); + }); }); - await recorderDocumentCreation; }; diff --git a/apps/chrome-extension/src/background/service-worker.ts b/apps/chrome-extension/src/background/service-worker.ts index b601f050e06..d5e1782149e 100644 --- a/apps/chrome-extension/src/background/service-worker.ts +++ b/apps/chrome-extension/src/background/service-worker.ts @@ -1,3 +1,4 @@ +import { capabilities } from "../platform/capabilities"; import { EXTENSION_PROTOCOL } from "../platform/extension-protocol"; import { ApiRequestError, @@ -57,7 +58,11 @@ import type { ServiceWorkerRequest, ServiceWorkerResponse, } from "../shared/types"; -import { ensureRecorderHost, hasRecorderHost } from "./recorder-host"; +import { + ensureRecorderHost, + focusRecorderHost, + hasRecorderHost, +} from "./recorder-host"; // popup.html is web-accessible with use_dynamic_url so sites cannot fingerprint // the extension via the overlay iframe's static URL; that same flag makes its @@ -1168,6 +1173,11 @@ const resolveMicWarning = async ( }; const startRecording = async (mode: RecordingMode) => { + // Defense against stale overlay/content-script messages: the mode selector + // already hides tab capture where it is unsupported. + if (mode === "tab" && !capabilities.supportsTabCapture) { + throw new Error("Tab recording is not available in this browser."); + } const { settings, auth, bootstrap } = await requireSignedInState(); externalCaptureAutoPipPending = false; const recordingSettings = @@ -1232,6 +1242,12 @@ const startRecording = async (mode: RecordingMode) => { try { await Promise.all(readyWaits); + // On Firefox the recorder document must collect a click (transient + // activation for getDisplayMedia), so make sure its window exists and + // is in front before asking it to start. Both calls are cheap no-ops + // beyond document creation on Chrome. + await ensureRecorderHost({ interactive: true }); + await focusRecorderHost(); return await sendOffscreen({ target: "offscreen", type: "start-recording", @@ -1872,6 +1888,23 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { } }); +// The Firefox recorder host is a window the user can close mid-recording; +// syncRecordingStatus already degrades to idle when the host is gone, it +// just needs to be triggered promptly. Chrome's offscreen document is not +// user-closable, so this never fires anything meaningful there. +chrome.windows.onRemoved.addListener(() => { + if (capabilities.supportsOffscreen) return; + void syncRecordingStatus().catch(() => undefined); +}); + +// Firefox grants host permissions after install (welcome page); inject the +// bootstrap into tabs that were already open once that happens. +chrome.permissions.onAdded.addListener((permissions) => { + if (permissions.origins?.length) { + void injectOverlayIntoOpenTabs(); + } +}); + chrome.tabs.onRemoved.addListener((tabId) => { if (uploadProgressTabId === tabId) { setUploadProgressTabId(null); diff --git a/apps/chrome-extension/src/permission/camera-permission.tsx b/apps/chrome-extension/src/permission/camera-permission.tsx index cf6f909d12f..b36bebb481f 100644 --- a/apps/chrome-extension/src/permission/camera-permission.tsx +++ b/apps/chrome-extension/src/permission/camera-permission.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; +import { TARGET } from "../platform/target"; import { CapBrand, DoodleBoilFilter } from "../shared/cap-brand"; import { toCameraDevices } from "../shared/devices"; import { mountPageNav } from "../shared/page-nav"; @@ -18,14 +19,24 @@ mountPageNav("camera"); type Status = "idle" | "requesting" | "ready" | "error"; +const BROWSER_NAME = TARGET === "firefox" ? "Firefox" : "Chrome"; + +// Firefox forgets getUserMedia grants when the prompt is dismissed without +// the checkbox, which would resurface the prompt inside a minimized recorder +// window on every recording. +const REMEMBER_HINT = + TARGET === "firefox" + ? ' Tick "Remember this decision" so Firefox keeps the grant for future recordings.' + : ""; + const headlines: Record = { idle: { title: "Camera & microphone access", lede: "Allow access once so Cap can show your camera preview and record your voice.", }, requesting: { - title: "Waiting for Chrome", - lede: "Click Allow in the browser prompt up by the address bar.", + title: `Waiting for ${BROWSER_NAME}`, + lede: `Click Allow in the browser prompt up by the address bar.${REMEMBER_HINT}`, }, ready: { title: "You're all set", @@ -49,7 +60,7 @@ const getCameraErrorMessage = (error: unknown) => { error.name === "NotAllowedError" || error.message.toLowerCase().includes("permission") ) { - return "Chrome did not grant camera access. Click Allow in the browser prompt."; + return `${BROWSER_NAME} did not grant camera access. Click Allow in the browser prompt.${REMEMBER_HINT}`; } if (error.name === "NotFoundError") return "No camera was found."; if (error.name === "NotReadableError") return "Camera is already in use."; diff --git a/apps/chrome-extension/src/popup/components/recording-mode-selector.tsx b/apps/chrome-extension/src/popup/components/recording-mode-selector.tsx index 7ef1d188a55..bb413ea45a5 100644 --- a/apps/chrome-extension/src/popup/components/recording-mode-selector.tsx +++ b/apps/chrome-extension/src/popup/components/recording-mode-selector.tsx @@ -5,6 +5,7 @@ import { MonitorIcon, RectangleHorizontal, } from "lucide-react"; +import { capabilities } from "../../platform/capabilities"; import type { RecordingMode } from "../../shared/types"; import { SelectContent, @@ -55,6 +56,10 @@ export const RecordingModeSelector = ({ }, }; + const availableModeEntries = Object.entries(recordingModeOptions).filter( + ([value]) => value !== "tab" || capabilities.supportsTabCapture, + ); + const selectedOption = mode ? recordingModeOptions[mode] : null; const SelectedIcon = selectedOption?.icon; @@ -81,7 +86,7 @@ export const RecordingModeSelector = ({ - {Object.entries(recordingModeOptions).map(([value, option]) => { + {availableModeEntries.map(([value, option]) => { const OptionIcon = option.icon; return ( diff --git a/apps/chrome-extension/src/popup/main.tsx b/apps/chrome-extension/src/popup/main.tsx index be8fc9c0469..081c6c55b12 100644 --- a/apps/chrome-extension/src/popup/main.tsx +++ b/apps/chrome-extension/src/popup/main.tsx @@ -1,6 +1,7 @@ import clsx from "clsx"; import { useCallback, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; +import { capabilities } from "../platform/capabilities"; import { toCameraDevices, toMicrophoneDevices } from "../shared/devices"; import { reconcileRememberedDevices, @@ -92,6 +93,13 @@ const isRecordingStatus = ( status.phase === "paused" || status.phase === "uploading"; +// A remembered "tab" mode can arrive from settings synced on a browser that +// supports tab capture; fall back rather than render an unselectable mode. +const resolveAvailableMode = (mode: RecordingMode): RecordingMode => + mode === "tab" && !capabilities.supportsTabCapture ? "fullscreen" : mode; + +const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*"]; + function App() { // This page is web accessible, so any site can put it in an iframe and // overlay it for clickjacking. When embedded, render nothing until the @@ -114,6 +122,9 @@ function App() { const [cameraSelectOpen, setCameraSelectOpen] = useState(false); const [micSelectOpen, setMicSelectOpen] = useState(false); const [failedRecordingsCount, setFailedRecordingsCount] = useState(0); + const [hostAccessGranted, setHostAccessGranted] = useState( + capabilities.hostPermissionsGrantedAtInstall, + ); const settingsRef = useRef(defaultSettings); const recordingActive = isRecordingStatus(status); @@ -131,7 +142,7 @@ function App() { const updateSettings = useCallback(async (next: ExtensionSettings) => { settingsRef.current = next; setSettings(next); - setMode(next.capture.recordingMode); + setMode(resolveAvailableMode(next.capture.recordingMode)); await saveSettings(next); await sendServiceWorkerMessage({ target: "service-worker", @@ -143,7 +154,7 @@ function App() { const applySettings = useCallback((next: ExtensionSettings) => { settingsRef.current = next; setSettings(next); - setMode(next.capture.recordingMode); + setMode(resolveAvailableMode(next.capture.recordingMode)); }, []); const loadDevices = useCallback(async () => { @@ -225,6 +236,21 @@ function App() { }); }; + const openWelcomePage = () => { + chrome.tabs.create({ + url: chrome.runtime.getURL("welcome.html"), + active: true, + }); + }; + + useEffect(() => { + if (capabilities.hostPermissionsGrantedAtInstall) return; + chrome.permissions.contains( + { origins: HOST_PERMISSION_ORIGINS }, + (granted) => setHostAccessGranted(Boolean(granted)), + ); + }, []); + useEffect(() => { if (!IS_EMBEDDED) return; let disposed = false; @@ -664,7 +690,7 @@ function App() { onPermissionBlocked={openPermissionPage} /> - {mode !== "camera" && ( + {mode !== "camera" && capabilities.supportsSystemAudioCapture && (
void stop()} />
+ {!hostAccessGranted && ( + + )} {recordingBarStatus && (
void) => { + if (TARGET !== "firefox") return; + const container = document.getElementById("arm-capture"); + const button = document.getElementById("arm-capture-button"); + if ( + !(container instanceof HTMLElement) || + !(button instanceof HTMLButtonElement) + ) { + return; + } + + container.hidden = false; + try { + await new Promise((resolve, reject) => { + const onClick = () => { + window.clearInterval(intervalId); + resolve(); + }; + const intervalId = window.setInterval(() => { + try { + throwIfCanceled(); + } catch (error) { + window.clearInterval(intervalId); + button.removeEventListener("click", onClick); + reject(error instanceof Error ? error : new Error(String(error))); + } + }, ARM_CANCEL_POLL_INTERVAL_MS); + button.addEventListener("click", onClick, { once: true }); + }); + } finally { + container.hidden = true; + } +}; + +// Once every capture prompt has been answered this window has no further +// business on screen; get it out of the shot (and out of the picker's way) +// before the countdown runs. Cosmetic, so callers fire-and-forget it. +export const minimizeRecorderWindow = async () => { + if (TARGET !== "firefox") return; + await new Promise((resolve) => { + chrome.windows.getCurrent((currentWindow) => { + if (chrome.runtime.lastError || currentWindow?.id === undefined) { + resolve(); + return; + } + chrome.windows.update(currentWindow.id, { state: "minimized" }, () => { + void chrome.runtime.lastError; + resolve(); + }); + }); + }); +}; diff --git a/apps/chrome-extension/src/recorder/recorder.ts b/apps/chrome-extension/src/recorder/recorder.ts index 7179f9d2016..9aabb3a9bac 100644 --- a/apps/chrome-extension/src/recorder/recorder.ts +++ b/apps/chrome-extension/src/recorder/recorder.ts @@ -69,6 +69,7 @@ import { toSessionDescriptionInit, waitForIceGatheringComplete, } from "../shared/webrtc"; +import { awaitCaptureGesture, minimizeRecorderWindow } from "./arm-gesture"; const RECORDING_TIMESLICE_MS = 1000; const RECORDING_TIMESLICE_GUARD_MS = RECORDING_TIMESLICE_MS * 3; @@ -840,6 +841,7 @@ const startRecording = async (request: StartRecordingRequest) => { try { status = { phase: "creating" }; + await awaitCaptureGesture(throwIfStartCanceled); const mainStream = await getMainStream(request); ownedStreams.push(mainStream); throwIfStartCanceled(); @@ -855,6 +857,7 @@ const startRecording = async (request: StartRecordingRequest) => { ownedStreams.push(microphoneStream); } throwIfStartCanceled(); + void minimizeRecorderWindow(); const { width, height, fps } = getStreamSize(mainStream); const videoTracks = mainStream.getVideoTracks(); if (videoTracks.length === 0) { diff --git a/apps/chrome-extension/src/welcome/main.ts b/apps/chrome-extension/src/welcome/main.ts index 45f8d8fd02b..c66391ca891 100644 --- a/apps/chrome-extension/src/welcome/main.ts +++ b/apps/chrome-extension/src/welcome/main.ts @@ -1,3 +1,4 @@ +import { capabilities } from "../platform/capabilities"; import { mountPageNav } from "../shared/page-nav"; import { sendServiceWorkerMessage } from "../shared/runtime"; import { loadAuth } from "../shared/storage"; @@ -44,3 +45,34 @@ signInButton.addEventListener("click", () => { authPollId = window.setInterval(() => void checkAuth(), 1000); void checkAuth(); + +// Firefox treats MV3 host permissions as opt-in, so the declared content +// script (overlay, countdown, recording bar) stays inert until the user +// grants access here. The click on the button supplies the required user +// gesture for permissions.request. +const HOST_PERMISSION_ORIGINS = ["http://*/*", "https://*/*"]; + +if (!capabilities.hostPermissionsGrantedAtInstall) { + const hostPermissionRow = byId("host-permission-row"); + const hostPermissionNote = byId("host-permission-note"); + const grantHostsButton = byId("grant-hosts"); + const hostsGrantedPill = byId("hosts-granted"); + + const reflectHostPermission = (granted: boolean) => { + grantHostsButton.hidden = granted; + hostsGrantedPill.hidden = !granted; + hostPermissionNote.hidden = granted; + }; + + hostPermissionRow.hidden = false; + hostPermissionNote.hidden = false; + chrome.permissions.contains({ origins: HOST_PERMISSION_ORIGINS }, (granted) => + reflectHostPermission(Boolean(granted)), + ); + grantHostsButton.addEventListener("click", () => { + chrome.permissions.request( + { origins: HOST_PERMISSION_ORIGINS }, + (granted) => reflectHostPermission(Boolean(granted)), + ); + }); +} diff --git a/apps/chrome-extension/welcome.html b/apps/chrome-extension/welcome.html index edae273207a..e728f448935 100644 --- a/apps/chrome-extension/welcome.html +++ b/apps/chrome-extension/welcome.html @@ -168,6 +168,25 @@

Pin Cap to your toolbar

Click the Cap icon whenever you're ready to record.

+ +
- + +
+ Cap +

Cap recording engine

- Firefox needs one click here before Cap can ask which screen or - window to share. + This window powers your recording and uploads. It stays minimized + while you record and closes by itself when it is no longer needed. +

+
+ +
+ Cap +

Ready to record?

+

+ Firefox asks for one click here, then you'll choose what to share. +

+ +

+ Recording starts after the countdown. This window gets out of the + way by itself.

diff --git a/apps/chrome-extension/src/background/recorder-host.ts b/apps/chrome-extension/src/background/recorder-host.ts index 9d4bb1414ae..a5ce4c013de 100644 --- a/apps/chrome-extension/src/background/recorder-host.ts +++ b/apps/chrome-extension/src/background/recorder-host.ts @@ -9,8 +9,8 @@ import { capabilities } from "../platform/capabilities"; // once idle, so this module only ever has to create and find it. export const RECORDER_URL = "recorder.html"; -const RECORDER_WINDOW_WIDTH = 400; -const RECORDER_WINDOW_HEIGHT = 360; +const RECORDER_WINDOW_WIDTH = 440; +const RECORDER_WINDOW_HEIGHT = 400; const RECORDER_READY_TIMEOUT_MS = 10_000; const RECORDER_READY_POLL_INTERVAL_MS = 100; @@ -77,18 +77,54 @@ const createOffscreenDocument = () => ); }); -const createRecorderWindow = (interactive: boolean) => - new Promise((resolve, reject) => { - // state cannot be combined with bounds or focus in windows.create. - const createData: chrome.windows.CreateData = interactive - ? { - url: RECORDER_URL, - type: "popup", - focused: true, - width: RECORDER_WINDOW_WIDTH, - height: RECORDER_WINDOW_HEIGHT, - } - : { url: RECORDER_URL, type: "popup", state: "minimized" }; +// Center the interactive recorder window over the browser window the user is +// looking at, so the arm dialog reads as part of the flow rather than a stray +// popup. +const getCenteredBounds = () => + new Promise<{ left?: number; top?: number }>((resolve) => { + chrome.windows.getLastFocused((focusedWindow) => { + if ( + chrome.runtime.lastError || + focusedWindow?.left === undefined || + focusedWindow.top === undefined || + !focusedWindow.width || + !focusedWindow.height + ) { + resolve({}); + return; + } + resolve({ + left: Math.max( + 0, + Math.round( + focusedWindow.left + + (focusedWindow.width - RECORDER_WINDOW_WIDTH) / 2, + ), + ), + top: Math.max( + 0, + Math.round( + focusedWindow.top + + (focusedWindow.height - RECORDER_WINDOW_HEIGHT) / 2, + ), + ), + }); + }); + }); + +const createRecorderWindow = async (interactive: boolean) => { + // state cannot be combined with bounds or focus in windows.create. + const createData: chrome.windows.CreateData = interactive + ? { + url: RECORDER_URL, + type: "popup", + focused: true, + width: RECORDER_WINDOW_WIDTH, + height: RECORDER_WINDOW_HEIGHT, + ...(await getCenteredBounds()), + } + : { url: RECORDER_URL, type: "popup", state: "minimized" }; + await new Promise((resolve, reject) => { chrome.windows.create(createData, () => { const error = chrome.runtime.lastError; if (error) { @@ -98,6 +134,7 @@ const createRecorderWindow = (interactive: boolean) => resolve(); }); }); +}; const pingRecorder = () => new Promise((resolve) => { diff --git a/apps/chrome-extension/src/recorder/arm-gesture.ts b/apps/chrome-extension/src/recorder/arm-gesture.ts index cb4bce7ef6b..c8149cb24f3 100644 --- a/apps/chrome-extension/src/recorder/arm-gesture.ts +++ b/apps/chrome-extension/src/recorder/arm-gesture.ts @@ -13,6 +13,7 @@ export const awaitCaptureGesture = async (throwIfCanceled: () => void) => { if (TARGET !== "firefox") return; const container = document.getElementById("arm-capture"); const button = document.getElementById("arm-capture-button"); + const idleState = document.getElementById("idle-state"); if ( !(container instanceof HTMLElement) || !(button instanceof HTMLButtonElement) @@ -20,6 +21,7 @@ export const awaitCaptureGesture = async (throwIfCanceled: () => void) => { return; } + if (idleState) idleState.hidden = true; container.hidden = false; try { await new Promise((resolve, reject) => { @@ -40,6 +42,7 @@ export const awaitCaptureGesture = async (throwIfCanceled: () => void) => { }); } finally { container.hidden = true; + if (idleState) idleState.hidden = false; } }; From 29f0437a3ad18d16b130194f7405a536258263c6 Mon Sep 17 00:00:00 2001 From: ManthanNimodiya Date: Tue, 7 Jul 2026 10:47:15 +0530 Subject: [PATCH 7/7] fix(extension): harden Firefox recorder host lifecycle and guard bundle completeness --- apps/chrome-extension/package.json | 4 +- .../src/background/recorder-host.ts | 87 +++++++++++-------- .../src/background/service-worker.ts | 53 ++++++----- .../chrome-extension/src/content/bootstrap.ts | 30 +++---- .../src/recorder/arm-gesture.ts | 13 +++ apps/chrome-extension/src/shared/runtime.ts | 5 ++ apps/chrome-extension/src/shared/storage.ts | 4 + apps/chrome-extension/vite.config.ts | 29 +++++-- 8 files changed, 133 insertions(+), 92 deletions(-) diff --git a/apps/chrome-extension/package.json b/apps/chrome-extension/package.json index d8edae8024f..0ed03bbf7ac 100644 --- a/apps/chrome-extension/package.json +++ b/apps/chrome-extension/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "build": "pnpm build:chrome && pnpm build:firefox", - "build:chrome": "rm -rf dist/chrome && TARGET=chrome vite build && TARGET=chrome vite build --config vite.background.config.ts && TARGET=chrome vite build --config vite.content.config.ts && TARGET=chrome vite build --config vite.content-overlay.config.ts", - "build:firefox": "rm -rf dist/firefox && TARGET=firefox vite build && TARGET=firefox vite build --config vite.background.config.ts && TARGET=firefox vite build --config vite.content.config.ts && TARGET=firefox vite build --config vite.content-overlay.config.ts", + "build:chrome": "rm -rf dist/chrome && TARGET=chrome vite build --config vite.background.config.ts && TARGET=chrome vite build --config vite.content.config.ts && TARGET=chrome vite build --config vite.content-overlay.config.ts && TARGET=chrome vite build", + "build:firefox": "rm -rf dist/firefox && TARGET=firefox vite build --config vite.background.config.ts && TARGET=firefox vite build --config vite.content.config.ts && TARGET=firefox vite build --config vite.content-overlay.config.ts && TARGET=firefox vite build", "dev": "rm -rf dist/chrome && (trap 'kill 0' INT TERM; TARGET=chrome vite build --watch --config vite.background.config.ts --mode development & TARGET=chrome vite build --watch --config vite.content.config.ts --mode development & TARGET=chrome vite build --watch --config vite.content-overlay.config.ts --mode development & TARGET=chrome vite build --watch --mode development & wait)", "dev:firefox": "rm -rf dist/firefox && (trap 'kill 0' INT TERM; TARGET=firefox vite build --watch --config vite.background.config.ts --mode development & TARGET=firefox vite build --watch --config vite.content.config.ts --mode development & TARGET=firefox vite build --watch --config vite.content-overlay.config.ts --mode development & TARGET=firefox vite build --watch --mode development & wait)", "run:firefox": "web-ext run --source-dir dist/firefox", diff --git a/apps/chrome-extension/src/background/recorder-host.ts b/apps/chrome-extension/src/background/recorder-host.ts index a5ce4c013de..6a51d7cd2d0 100644 --- a/apps/chrome-extension/src/background/recorder-host.ts +++ b/apps/chrome-extension/src/background/recorder-host.ts @@ -1,4 +1,5 @@ import { capabilities } from "../platform/capabilities"; +import { wait } from "../shared/runtime"; // The recorder document (recorder.html) hosts capture, upload, device // enumeration, the mic probe and the camera-preview relay. On Chrome it runs @@ -21,20 +22,16 @@ type RecorderContext = { windowId?: number; }; +// Match by document URL only: Chrome reports the recorder as an +// OFFSCREEN_DOCUMENT context while Firefox's popup window may surface as TAB +// (or another type — the spec leaves extension pages in popup windows +// underspecified). Only the recorder document ever has this URL, so the URL +// filter alone is unambiguous on both browsers. const getRecorderContexts = async (): Promise => { const recorderUrl = chrome.runtime.getURL(RECORDER_URL); - // String literals rather than the chrome.runtime.ContextType enum object, - // which Firefox does not guarantee to expose. - const contextType = ( - capabilities.supportsOffscreen ? "OFFSCREEN_DOCUMENT" : "TAB" - ) as chrome.runtime.ContextType; return new Promise((resolve) => { - chrome.runtime.getContexts( - { - contextTypes: [contextType], - documentUrls: [recorderUrl], - }, - (contexts) => resolve(contexts ?? []), + chrome.runtime.getContexts({ documentUrls: [recorderUrl] }, (contexts) => + resolve(contexts ?? []), ); }); }; @@ -42,8 +39,8 @@ const getRecorderContexts = async (): Promise => { export const hasRecorderHost = async () => (await getRecorderContexts()).length > 0; -const getRecorderWindowId = async () => { - for (const context of await getRecorderContexts()) { +const recorderWindowIdFrom = (contexts: RecorderContext[]) => { + for (const context of contexts) { if (typeof context.windowId === "number" && context.windowId >= 0) { return context.windowId; } @@ -124,16 +121,32 @@ const createRecorderWindow = async (interactive: boolean) => { ...(await getCenteredBounds()), } : { url: RECORDER_URL, type: "popup", state: "minimized" }; - await new Promise((resolve, reject) => { - chrome.windows.create(createData, () => { + const windowId = await new Promise((resolve, reject) => { + chrome.windows.create(createData, (created) => { const error = chrome.runtime.lastError; if (error) { reject(new Error(error.message ?? "Failed to open the Cap recorder")); return; } - resolve(); + resolve(created?.id); }); }); + + try { + await waitForRecorderReady(); + } catch (error) { + // Close the unresponsive window, or every later ensureRecorderHost call + // would find its context, assume the host is healthy, and fail forever. + if (windowId !== undefined) { + await new Promise((resolve) => { + chrome.windows.remove(windowId, () => { + void chrome.runtime.lastError; + resolve(); + }); + }); + } + throw error; + } }; const pingRecorder = () => @@ -154,41 +167,41 @@ const waitForRecorderReady = async () => { const deadline = Date.now() + RECORDER_READY_TIMEOUT_MS; while (Date.now() < deadline) { if (await pingRecorder()) return; - await new Promise((resolve) => { - globalThis.setTimeout(resolve, RECORDER_READY_POLL_INTERVAL_MS); - }); + await wait(RECORDER_READY_POLL_INTERVAL_MS); } throw new Error("The Cap recorder window did not become ready in time"); }; +const focusRecorderWindow = (windowId: number) => + new Promise((resolve) => { + chrome.windows.update(windowId, { focused: true, state: "normal" }, () => { + void chrome.runtime.lastError; + resolve(); + }); + }); + +// Ensures the recorder document exists; with `interactive` it also brings the +// Firefox recorder window forward so the user can click its arm button (and +// see permission prompts anchored to it). Chrome's offscreen document has no +// window, so `interactive` is a no-op there. export const ensureRecorderHost = async ( options: { interactive?: boolean } = {}, ) => { const contexts = await getRecorderContexts(); - if (contexts.length > 0) return; + if (contexts.length > 0) { + if (options.interactive === true && !capabilities.supportsOffscreen) { + const windowId = recorderWindowIdFrom(contexts); + if (windowId !== null) await focusRecorderWindow(windowId); + } + return; + } recorderHostCreation ??= ( capabilities.supportsOffscreen ? createOffscreenDocument() - : createRecorderWindow(options.interactive === true).then( - waitForRecorderReady, - ) + : createRecorderWindow(options.interactive === true) ).finally(() => { recorderHostCreation = null; }); await recorderHostCreation; }; - -// Brings the Firefox recorder window forward so the user can click its arm -// button (and see any permission prompts anchored to it). No-op on Chrome. -export const focusRecorderHost = async () => { - if (capabilities.supportsOffscreen) return; - const windowId = await getRecorderWindowId(); - if (windowId === null) return; - await new Promise((resolve) => { - chrome.windows.update(windowId, { focused: true, state: "normal" }, () => { - void chrome.runtime.lastError; - resolve(); - }); - }); -}; diff --git a/apps/chrome-extension/src/background/service-worker.ts b/apps/chrome-extension/src/background/service-worker.ts index bf2019504b5..d4f4b97a2bd 100644 --- a/apps/chrome-extension/src/background/service-worker.ts +++ b/apps/chrome-extension/src/background/service-worker.ts @@ -12,6 +12,7 @@ import { isServiceWorkerRequest, } from "../shared/messages"; import { rememberRecordingMode } from "../shared/preferences"; +import { wait } from "../shared/runtime"; import { clearAuth, clearAuthError, @@ -59,11 +60,7 @@ import type { ServiceWorkerRequest, ServiceWorkerResponse, } from "../shared/types"; -import { - ensureRecorderHost, - focusRecorderHost, - hasRecorderHost, -} from "./recorder-host"; +import { ensureRecorderHost, hasRecorderHost } from "./recorder-host"; // popup.html is web-accessible with use_dynamic_url so sites cannot fingerprint // the extension via the overlay iframe's static URL; that same flag makes its @@ -212,11 +209,6 @@ const focusTab = async (tabId: number) => { await activateTab(tabId); }; -const wait = (durationMs: number) => - new Promise((resolve) => { - globalThis.setTimeout(resolve, durationMs); - }); - const isTransientOffscreenMessageError = (error: unknown) => { if (!(error instanceof Error)) return false; const message = error.message.toLowerCase(); @@ -239,15 +231,19 @@ const sendOffscreenRuntimeMessage = (message: OffscreenRequest) => const sendOffscreen = async ( message: OffscreenRequest, - options: { createIfMissing?: boolean } = {}, + options: { createIfMissing?: boolean; interactive?: boolean } = {}, ) => { + // `interactive` must survive into the retry path: recreating the Firefox + // recorder window minimized for a start-recording message would leave its + // arm button waiting for a click no one can make. + const hostOptions = { interactive: options.interactive === true }; if (options.createIfMissing === false) { const hasDocument = await hasRecorderHost(); if (!hasDocument) { return { ok: true, status: recordingStatus } satisfies OffscreenResponse; } } else { - await ensureRecorderHost(); + await ensureRecorderHost(hostOptions); } let lastError: unknown; @@ -264,7 +260,7 @@ const sendOffscreen = async ( break; } await wait(OFFSCREEN_MESSAGE_RETRY_DELAY_MS); - await ensureRecorderHost(); + await ensureRecorderHost(hostOptions); } } @@ -1253,21 +1249,22 @@ const startRecording = async (mode: RecordingMode) => { try { await Promise.all(readyWaits); // On Firefox the recorder document must collect a click (transient - // activation for getDisplayMedia), so make sure its window exists and - // is in front before asking it to start. Both calls are cheap no-ops - // beyond document creation on Chrome. - await ensureRecorderHost({ interactive: true }); - await focusRecorderHost(); - return await sendOffscreen({ - target: "offscreen", - type: "start-recording", - mode, - settings: recordingSettings, - auth, - bootstrap, - tabId, - tabStreamId, - }); + // activation for getDisplayMedia), so its window is created — or + // surfaced — in front of the user. A no-op beyond document creation on + // Chrome. + return await sendOffscreen( + { + target: "offscreen", + type: "start-recording", + mode, + settings: recordingSettings, + auth, + bootstrap, + tabId, + tabStreamId, + }, + { interactive: true }, + ); } catch (error) { // The recorder panel closes as soon as the status leaves "idle", so a // silent reset would leave the user with no feedback at all. Broadcast diff --git a/apps/chrome-extension/src/content/bootstrap.ts b/apps/chrome-extension/src/content/bootstrap.ts index a3d6e7ed4fe..15a7257beed 100644 --- a/apps/chrome-extension/src/content/bootstrap.ts +++ b/apps/chrome-extension/src/content/bootstrap.ts @@ -3,6 +3,7 @@ import { isOverlayMessage, isRecordingStatusBroadcast, } from "../shared/messages"; +import { sendServiceWorkerMessage } from "../shared/runtime"; import { RECORDING_STATE_KEY, SHARED_STATE_AREA, @@ -83,26 +84,15 @@ const bootstrap = () => { // IIFE exposing CapOverlay; ask the service worker to executeScript it // into this same isolated world. This is the one message the bootstrap // sends, and only once a tab actually needs UI. - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage( - { target: "service-worker", type: "inject-overlay-module" }, - (response?: { ok?: boolean; error?: string }) => { - if (chrome.runtime.lastError) { - reject( - new Error(chrome.runtime.lastError.message ?? "Send failed"), - ); - return; - } - const module = (globalThis as { CapOverlay?: OverlayModule }) - .CapOverlay; - if (response?.ok && module) { - resolve(module); - return; - } - reject( - new Error(response?.error ?? "Overlay module injection failed"), - ); - }, + return sendServiceWorkerMessage({ + target: "service-worker", + type: "inject-overlay-module", + }).then((response) => { + const module = (globalThis as { CapOverlay?: OverlayModule }).CapOverlay; + if (response.ok && module) return module; + throw new Error( + ("error" in response ? response.error : undefined) ?? + "Overlay module injection failed", ); }); }; diff --git a/apps/chrome-extension/src/recorder/arm-gesture.ts b/apps/chrome-extension/src/recorder/arm-gesture.ts index c8149cb24f3..ec84f94d15a 100644 --- a/apps/chrome-extension/src/recorder/arm-gesture.ts +++ b/apps/chrome-extension/src/recorder/arm-gesture.ts @@ -23,6 +23,19 @@ export const awaitCaptureGesture = async (throwIfCanceled: () => void) => { if (idleState) idleState.hidden = true; container.hidden = false; + // Belt and braces: if any code path brought this document up minimized + // (e.g. a host created for device enumeration), surface it — a hidden arm + // button would otherwise wait forever for a click no one can make. + chrome.windows.getCurrent((currentWindow) => { + if (chrome.runtime.lastError || currentWindow?.id === undefined) return; + chrome.windows.update( + currentWindow.id, + { state: "normal", focused: true }, + () => { + void chrome.runtime.lastError; + }, + ); + }); try { await new Promise((resolve, reject) => { const onClick = () => { diff --git a/apps/chrome-extension/src/shared/runtime.ts b/apps/chrome-extension/src/shared/runtime.ts index d36940fe286..6131ee4cb4e 100644 --- a/apps/chrome-extension/src/shared/runtime.ts +++ b/apps/chrome-extension/src/shared/runtime.ts @@ -1,5 +1,10 @@ import type { ServiceWorkerRequest, ServiceWorkerResponse } from "./types"; +export const wait = (durationMs: number) => + new Promise((resolve) => { + globalThis.setTimeout(resolve, durationMs); + }); + export const sendServiceWorkerMessage = (message: ServiceWorkerRequest) => new Promise((resolve, reject) => { chrome.runtime.sendMessage(message, (response) => { diff --git a/apps/chrome-extension/src/shared/storage.ts b/apps/chrome-extension/src/shared/storage.ts index 12ccd783227..059780858c2 100644 --- a/apps/chrome-extension/src/shared/storage.ts +++ b/apps/chrome-extension/src/shared/storage.ts @@ -137,6 +137,10 @@ const removeLocal = (keys: string[] | string) => chrome.storage.local.remove(keys, resolve); }); +// Any key stored through getSession/setSession/removeSession must also be +// listed in clearSharedSessionState below: on Firefox these keys live in +// storage.local, and that list is what restores their session-only lifetime +// across browser restarts. const sharedStateStorage = () => SHARED_STATE_AREA === "session" ? chrome.storage.session diff --git a/apps/chrome-extension/vite.config.ts b/apps/chrome-extension/vite.config.ts index 450cb14cd84..c4b2a5c6a15 100644 --- a/apps/chrome-extension/vite.config.ts +++ b/apps/chrome-extension/vite.config.ts @@ -1,4 +1,4 @@ -import { copyFileSync } from "node:fs"; +import { copyFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; import react from "@vitejs/plugin-react"; import { defineConfig, type Plugin } from "vite"; @@ -8,9 +8,25 @@ const target = resolveTarget(); // The manifests live outside public/ because public/ is copied verbatim into // every outDir, which would ship the Chrome manifest into the Firefox build. -const copyManifest = (): Plugin => ({ +// This config is only one piece of the bundle (the background script and +// content scripts come from the vite.*.config.ts builds the package.json +// scripts chain BEFORE this one), so before stamping the manifest — the last +// step of a build — verify the background artifact it references exists. +const copyManifest = (watchMode: boolean): Plugin => ({ name: "cap-copy-manifest", closeBundle() { + const backgroundScript = resolve( + __dirname, + outDirFor(target), + "assets/service-worker.js", + ); + if (!watchMode && !existsSync(backgroundScript)) { + throw new Error( + "assets/service-worker.js is missing — the extension must be built " + + "with the package.json build scripts (build:chrome / build:firefox), " + + "not a bare `vite build`.", + ); + } copyFileSync( resolve(__dirname, `manifests/manifest.${target}.json`), resolve(__dirname, outDirFor(target), "manifest.json"), @@ -18,8 +34,11 @@ const copyManifest = (): Plugin => ({ }, }); -export default defineConfig({ - plugins: [react(), copyManifest()], +export default defineConfig(({ command, mode }) => ({ + plugins: [ + react(), + copyManifest(command === "serve" || mode === "development"), + ], define: targetDefine(target), build: { emptyOutDir: false, @@ -43,4 +62,4 @@ export default defineConfig({ }, }, }, -}); +}));