---
title: Compatibilité des extensions
description: >-
  Les API d'extension Chrome que Lilo prend en charge, celles qui se comportent
  autrement que dans Chrome, et ce que cela change pour vos extensions.
sidebar:
  order: 5
---
Lilo exécute les extensions Chrome sur WebKit et comble une grande partie des API Chrome qui manquent à WebKit. Cette page ne recense que ce qui manque ou diffère de Chrome, ainsi que les extensions du store qui ont été testées : une API qui n'y figure pas se comporte comme dans Chrome. Pour installer et gérer des extensions, consultez [Extensions](/fr/guides/extensions).

Le texte ci-dessous provient du suivi de compatibilité du projet, [COMPATIBILITY.md](https://github.com/Alexays/Lilo/blob/main/COMPATIBILITY.md), et s'adresse autant aux développeurs d'extensions qu'aux personnes qui utilisent Lilo. Pour savoir si une extension que vous utilisez fonctionne, rendez-vous à la section [Extensions du store testées](#tested-store-extensions) : son tableau indique l'état de chaque extension. Les paragraphes qui précèdent ce tableau décrivent la façon dont le projet teste les extensions : des noms tels que `make ext-smoke`, `top-extensions.json` ou `RealExtensionSmokeTests` désignent les commandes et fichiers de test propres au projet, dont vous n'avez pas besoin pour utiliser Lilo. Si une extension dont vous dépendez fonctionne mal et que son problème ne figure pas ici, [signalez-le](https://github.com/Alexays/Lilo/issues).

## Extensions Chrome [#chrome-extensions]

### Différences d'API [#api-differences]

MV3 uniquement, sur le runtime `WKWebExtension` de WebKit, complété par les API Gap de Lilo ([docs/ARCHITECTURE.md](https://github.com/Alexays/Lilo/blob/main/docs/ARCHITECTURE.md#extension-architecture)). Cette section ne liste que ce qui manque ou diffère de Chrome ; tout ce qui n'y figure pas se comporte comme dans Chrome.

- **Pris en charge comme dans Chrome :** `action` (couleurs de badge comprises), `alarms`, `bookmarks`, `contextMenus`, `cookies`, `history`, `idle`, `management`, `offscreen`, `omnibox`, `permissions`, `power`, la messagerie `runtime`, `scripting`, `search`, `sessions`, `storage` (`local`, `session`, `sync`), `system.cpu`, `system.display`, `system.memory`, `system.network`, `system.storage`, `topSites`, `chrome_url_overrides.newtab`.
- **Manquant :** le blocage par `webRequest` (observation seule, donc `webRequest.onActionIgnored` ne se déclenche jamais et `handlerBehaviorChanged` n'a rien à vider), les extensions MV2, `chrome_settings_overrides` (une extension de moteur de recherche ne change ni le moteur de recherche ni les pages d'accueil et de démarrage), l'autorisation `favicon` (les URL `chrome-extension://<id>/_favicon/` ne se chargent pas). `declarativeNetRequest.updateStaticRules` et `getDisabledRuleIds` fonctionnent en réécrivant le fichier du jeu de règles, car WebKit ne peut pas désactiver une seule règle statique.
- **Espaces de noms manquants :** `accessibilityFeatures`, `readingList` (Lilo n'a pas de liste de lecture), `ttsEngine`, `webAuthenticationProxy`.
- **Membres manquants :** `runtime.getPackageDirectoryEntry`. `ChromeSurfaceTests` vérifie cette liste, méthodes d'événements et `browser.*` compris, par rapport aux schémas d'API de Chrome (`make chrome-surface` les actualise), avec le type de chaque membre et la valeur de chaque énumération et de chaque constante.
- **Scripts de contenu et pages :** `ChromeContractTests` compare ce qu'un script de contenu, un script du monde principal et la page voient de `chrome` et de `browser` avec ce qu'ils en voient dans Chrome. Un script de contenu n'a pas de `storage.session` (celui de Chrome refuse les appels tant que `setAccessLevel` ne l'a pas ouvert), pas de `scripting` (`globalParams` et ses énumérations), ni `storage.AccessLevel` ni `setAccessLevel`, pas de `runtime.dynamicId`, pas de `extension.ViewType` et pas de `storage.sync.MAX_SUSTAINED_WRITE_OPERATIONS_PER_MINUTE` ; son `storage.managed` n'a que `get`, `getBytesInUse` et `onChanged`, et WebKit ajoute `i18n.getPreferredSystemLanguages`, `i18n.getSystemUILanguage`, `runtime.getDocumentId` et `runtime.getFrameId`. Ni un script de contenu ni la page n'ont les `chrome.csi` et `chrome.loadTimes` de Chrome, et la page n'a aucun objet `chrome` (celui de Chrome contient `app`, `csi` et `loadTimes`). Un script du monde principal (`world: 'MAIN'`) reçoit le `browser.runtime` de WebKit avec `connect` et `sendMessage`, là où Chrome ne lui donne rien sans `externally_connectable`.
- **Événements qui ne se déclenchent jamais :** `runtime.onConnectNative` (Lilo ne démarre jamais d'hôte natif qui se connecte en premier), `declarativeNetRequest.onRuleMatchedDebug` (WebKit ne dit pas quelle règle a correspondu à une requête), `bookmarks.onChildrenReordered`, `onImportBegan` et `onImportEnded` (rien dans Lilo ne trie ni n'importe les favoris que voient les extensions), `tabGroups.onMoved` (Lilo ne réordonne aucun dossier) et `notifications.onPermissionLevelChanged`.
- **runtime :** les messages et les ports destinés à une autre extension du profil prennent l'expéditeur que déclare l'extension émettrice : l'onglet où se trouve sa page, ou l'onglet, le cadre et l'url de son script de contenu, que son worker relaie. Un script de contenu n'atteint une autre extension que par le worker de son extension : sans worker, son message échoue comme non reçu ; avec un worker qui n'écoute pas `runtime.onConnect`, son port se déconnecte aussitôt. Le rappel d'un script de contenu n'apprend aucun échec (son `runtime.lastError` reste celui de WebKit). Un script de contenu dans un cadre d'une page d'extension qu'aucun onglet n'affiche (l'iframe d'un document hors écran) envoie ses messages par cette page : les écouteurs de celle-ci ne l'entendent pas, les destinataires reçoivent l'expéditeur de la page avec l'`url` et l'`origin` du cadre (ses `frameId` et `documentId` sont ceux de la page), et rien n'atteint l'extension lorsque ses `web_accessible_resources` exposent les fichiers `_lilo/` de Lilo aux pages web. Un port vers une autre extension se déconnecte lorsque l'une des extrémités se déconnecte, que son extension est déchargée ou que les pages qui détiennent son autre extrémité disparaissent, et non lorsque le worker qui le détient s'arrête. `tabs.connect` n'atteint la page de l'extension elle-même que là où elle est la page principale de l'onglet, pas une page affichée dans un cadre d'une page web ni un cadre désigné par son id, et atteint aussi un autre onglet affichant la même url ; il ne se déconnecte au départ de cette page que si aucune autre page de l'extension écoutant `runtime.onConnect` n'est ouverte. Un port que `browser.runtime.connect` ouvre vers une page dans un onglet depuis un contexte écoutant `runtime.onConnect` se déconnecte presque toujours aussitôt (WebKit). Un `tabs.sendMessage` auquel personne ne répond dans un onglet encore en train de charger l'une des pages de l'extension se résout, comme si la page écoutait, au lieu d'échouer comme non reçu. Un message destiné à une extension dont le worker l'entend sans répondre échoue avec l'erreur « message port closed » de Chrome, même si l'une de ses pages devait répondre plus tard. Un message posté avant la connexion du port sur un port ouvert par `browser.runtime.connect`, entre les pages d'extension et le worker, arrive deux fois ; sur un port ouvert par `chrome.runtime.connect`, il arrive une fois. `onExtensionLoaded` signale `startup` après une mise à jour de Lilo, jamais `browser_update`, et jamais `reload`. Une page web que nomme l'`externally_connectable` d'une extension reçoit `chrome.runtime` avec `sendMessage`, `connect` et `lastError` seulement (claude.ai transmet ainsi sa connexion à l'extension Claude). Une mise à jour attend l'arrêt d'un worker en cours d'exécution, comme dans Chrome, mais pas la fermeture des pages ouvertes de l'extension. Un port qu'ouvre le worker désigne l'origine de l'extension comme `url` de son expéditeur, là où Chrome désigne le script du worker. `runtime.setUninstallURL` réussit et ne fait rien : aucune page ne s'ouvre lorsque l'extension est supprimée.
- **Espaces privés et profils :** une extension ne s'exécute dans les espaces privés et ne voit leurs onglets qu'une fois autorisée à y accéder (l'accès à la navigation privée de Chrome) ; leurs téléchargements et leurs onglets fermés ne sont jamais exposés. La valeur `split` de la clé `incognito` d'un manifeste fonctionne comme `spanning` : un seul worker et un seul stockage servent les espaces privés et le reste. Chaque profil a ses propres extensions, cookies et stockage ; une extension dont le profil n'est pas le profil actif ne peut que faire `downloads.search`, et `chrome.sessions` ne liste jamais les onglets d'un autre profil.
- **Worker d'arrière-plan :** chaque extension reçoit `nativeMessaging`, pour le canal du worker vers Lilo ; les hôtes natifs exigent toujours qu'elle soit déclarée ou accordée sur demande : sans elle, `connectNative`, que Chrome omet alors, ouvre un port qui se déconnecte à son premier message, sans `runtime.lastError`. Après un appel d'API, ou un événement d'une API que Lilo ajoute (favoris, notifications, inactivité...), un worker inactif est déchargé 20 à 50 s plus tard au lieu de 30 s. Un worker qui ne fait que recevoir des messages sur un port ouvert par un autre contexte s'arrête 2 à 2,5 minutes après y avoir posté son dernier message, là où Chrome le maintient tant que des messages arrivent ; un worker qui détient un port `connectNative` continue de s'exécuter tant que son hôte envoie des messages, là où Chrome le maintient aussi longtemps que le port est ouvert. WebKit exécute les rappels de promesses en attente pendant qu'`importScripts` charge un script que le worker n'avait pas encore chargé, là où Chrome les exécute une fois le code de premier niveau du worker terminé : une extension qui marque la fin de son démarrage par un rappel de promesse et importe un script avant ses écouteurs considère la suite de son démarrage comme tardive. Lilo ignore l'import d'un script vide, ce qui couvre Tampermonkey. Le `WebSocket` du worker est celui de Lilo (celui de WebKit fige le worker) : il n'envoie aucun cookie, son `Origin` est celle de l'extension, `bufferedAmount` reste à 0, aucune extension n'est négociée, et un socket se ferme lorsque le worker qui l'a ouvert s'arrête. Le worker est enregistré pour son propre répertoire, pas pour la racine de l'extension, et les pages d'extension ne sont pas ses clients : `clients.matchAll()` liste les pages que Lilo trouve ouvertes (popup, panneau latéral, onglets et fenêtres, mais ni document hors écran ni cadre), chacune avec son url comme `id` et sans `focused`, `visibilityState`, `focus` ni `navigate`, et un message posté à l'une d'elles atteint toutes les pages affichant cette url. Dans une page extérieure à ce répertoire, `navigator.serviceWorker.ready` se résout avec l'enregistrement que Lilo trouve ou, dans une page à laquelle WebKit n'en montre aucun (une page dans le processus du worker), avec un substitut qui ne fait que transporter les messages vers le worker (pas de `pushManager`, de `showNotification` ni d'`update`). Lilo relaie au worker le `postMessage` d'une page, et les ports qu'il transfère, sous la forme d'un événement `message` provenant d'un `WindowClient` ; son `waitUntil` ne prolonge rien. Un `fetch` avec `credentials: "include"` depuis une page d'extension ou le worker, et un `XMLHttpRequest` asynchrone d'une page d'extension avec `withCredentials`, vers un hôte auquel l'extension peut accéder passent par Lilo, et transportent donc les cookies `SameSite` du site comme dans Chrome ; un tel `XMLHttpRequest` reçoit sa réponse en entier (un seul événement `progress`, pas de `responseText` partiel), signale un `statusText` vide et ne déclenche aucun événement `upload`. Un `XMLHttpRequest` synchrone, ou dont le `responseType` est `document`, omet toujours ces cookies. Une `CryptoKey` que le worker stocke dans IndexedDB, comme valeur ou comme l'un de ses champs, est stockée sous la forme de son matériau, désenveloppé comme dans Chrome, et relue sous la forme d'un substitut que `crypto.subtle` utilise comme clé, dans le worker comme dans ses pages (WebKit bloquerait le worker pour de bon, ce qui interrompait la première configuration de 2FAS Auth) ; une clé imbriquée plus profondément lève `DataCloneError` dans le worker (une page la stocke comme le fait WebKit, et le worker se bloque alors en la lisant), une clé relue arrive sous la forme d'un objet simple une fois postée ou clonée, et un `postMessage` d'une clé depuis le worker le bloque toujours.
- **Manifeste d'extension :** les entrées de `web_accessible_resources` qui nomment des `extension_ids` n'accordent rien. Un `externally_connectable` sans `matches` ni `ids` laisse toutes les extensions se connecter, là où Chrome n'en laisse aucune. Un motif de correspondance d'`externally_connectable` qui nomme un port correspond à tous les ports de son hôte. Les sites auxquels correspond `externally_connectable` sont accordés à l'extension comme autorisations d'hôte, que l'invite d'installation liste (`<all_urls>` accorde tous les sites), là où Chrome n'accorde aucun accès par ce biais. Un caractère générique d'`externally_connectable` portant sur un suffixe public de la section privée de la liste (comme `*://*.netlify.app/*`) est abandonné, de sorte que ces sites ne peuvent pas envoyer de messages à l'extension : WebKit refuse le motif, là où Chrome l'accepte. `runtime.getManifest()` renvoie le manifeste que Lilo charge : `background.service_worker` est le chemin du worker depuis la racine de l'extension (`js/sw.js` pour `./js/sw.js`) ; `host_permissions` et `optional_host_permissions` se terminent par `/*` (Chrome ignore leur chemin, si bien que le `https://*/` d'Okta atteint toutes les pages) ; une commande `_execute_action`, un `content_security_policy` ou un `content_scripts` vides, les commandes `_execute_browser_action` et `_execute_page_action` de MV3, ces entrées de `web_accessible_resources` et une entrée d'`externally_connectable` sans `matches` ni `ids` sont omis ; une `description` manquante ou vide se lit `" "` ; les touches de raccourci perdent les espaces qui les entourent ; les motifs `file://` des scripts de contenu se lisent avec l'hôte vide de Chrome (`file:///*` pour `file://*`) ; et `externally_connectable` se lit `*://*/*` pour `<all_urls>`, et ses motifs sans leur port. Les pages que liste `sandbox.pages` sont servies comme les autres pages de l'extension : dans son origine, avec `chrome.*` et la politique `content_security_policy.extension_pages` (par défaut `script-src 'self'`), de sorte que `eval`, `new Function` et les scripts en ligne y sont refusés, là où Chrome leur donne `content_security_policy.sandbox` dans une origine opaque sans API d'extension. Les ressources dont l'entrée de `web_accessible_resources` définit `use_dynamic_url` sont servies sous l'id fixe de l'extension, si bien qu'une page web peut savoir que l'extension est installée.
- **Onglets et fenêtres :** du début d'une importation depuis un navigateur jusqu'à la fermeture de son écran, les onglets qu'une extension ouvre avec `tabs.create` ou `runtime.openOptionsPage` s'ouvrent en arrière-plan, de sorte que les pages de bienvenue des extensions qu'elle réinstalle laissent la configuration à l'écran. Un `tabs.create` ou un `windows.create` de la page `chrome://extensions` de Chrome ouvre à la place les réglages d'extensions de Lilo (sauf si `active` ou `focused` vaut false, ou pendant cette importation), sur l'extension que nomme son `?id=`, ou sur la page de détails de l'extension appelante, qui contient ses raccourcis, pour `chrome://extensions/shortcuts` ; il n'ouvre aucun onglet et renvoie l'onglet actif de la fenêtre, ou la fenêtre, ni l'un ni l'autre dans un espace privé que l'extension ne peut pas voir. Un onglet que `tabs.create` ouvre dans un espace qu'aucune fenêtre n'affiche, en arrière-plan ou pendant que la fenêtre affiche un espace privé que l'extension ne peut pas voir, reste déchargé jusqu'à ce qu'une fenêtre affiche cet espace. `tabs.highlight` active seulement le premier onglet : Lilo n'a pas de sélection de plusieurs onglets, donc `highlighted` désigne l'onglet actif. `tabs.setZoom` modifie l'onglet seulement, jamais son origine. Les vues partagées sont les dossiers partagés de Lilo : les onglets ne portent pas de `splitViewId`, et partager deux onglets d'un groupe les en fait sortir. `windows.onBoundsChanged` se déclenche jusqu'à une demi-seconde après la fin d'un déplacement ou d'un redimensionnement. `tabs.discard` exige un id d'onglet et ne décharge jamais l'onglet à l'écran ; les `goBack`/`goForward` d'un onglet déchargé échouent. Un onglet d'origine (`openerTabId`) n'est conservé que pour la session. Tant qu'une fenêtre affiche un onglet qui n'est pas une page web (un onglet terminal ou de diff, l'onglet d'un dossier verrouillé, ou au lancement jusqu'à l'activation de l'onglet restauré), les extensions y voient un onglet de plus, actif, qui représente ce qu'elle affiche, de même que Chrome liste ses propres pages comme des onglets : avec `tabs`, il se lit comme le `chrome://newtab/` de Chrome, et les scripts, les messages et la navigation ne l'atteignent pas. Une fenêtre sans aucun onglet, ce que Chrome n'a jamais, n'a pas d'onglet actif. `activeTab` est accordée par un clic sur l'action de l'extension ou par sa propre commande, jamais par des clics dans la page ni par des frappes au clavier. `windows.create` ouvre un panneau flottant qui n'affiche que la première URL, avec son propre id de fenêtre, quel que soit son `type`, et, avec `incognito`, des données privées qui lui sont propres, comme en a un nouvel espace privé, là où les fenêtres de navigation privée de Chrome partagent les leurs ; lorsque le réglage « Tout garder dans une seule fenêtre » est activé, un `windows.create` normal ouvre ses URL comme onglets de la fenêtre actuelle et renvoie cette fenêtre, et le `windows.remove` de la fenêtre d'un espace ne ferme alors que les onglets que `windows.create` y a ouverts, jamais la fenêtre du navigateur, et échoue lorsqu'il n'y en a aucun. Un popup de page (un `window.open` qui en demande un) est une fenêtre `popup` contenant un seul onglet, mais `windows.getAll` et `tabs.query` l'omettent jusqu'à ce qu'il soit ouvert comme onglet. Avec l'autorisation `tabs`, l'`url` et le `title` d'un onglet apparaissent dans ce que renvoient les méthodes de `tabs` et de `windows`, un onglet vide se lisant comme le `chrome://newtab/` de Chrome (la seule page à laquelle correspond un motif `chrome://` de `tabs.query`), mais les événements d'onglet les laissent vides pour les hôtes auxquels l'extension ne peut pas accéder ; sans elle, ils sont omis, comme dans Chrome.
- **Les espaces comme fenêtres :** chaque espace est une fenêtre pour les extensions, et un passage d'un espace à un autre un changement de focus de fenêtre. Un espace rejoint `windows.getAll` et `tabs.query` la première fois qu'une fenêtre l'affiche après le lancement. Un espace affiché dans deux fenêtres du navigateur est une seule fenêtre, si bien qu'un `windows.create` normal ouvre un panneau flottant, une fenêtre à part entière, plutôt qu'une seconde fenêtre du navigateur sur l'espace, ce qui renverrait la fenêtre de l'espace. Un espace en arrière-plan signale le cadre de la fenêtre du navigateur qui l'a affiché en dernier et l'état `normal` ; `windows.update` modifie cette fenêtre du navigateur, `focused: true` y affiche l'espace, comme le fait un `tabs.create` dans celui-ci, et `windows.remove` échoue. `windows.onBoundsChanged` ne se déclenche que pour l'espace affiché. Un onglet déplacé vers un espace d'un autre profil ou d'une autre confidentialité, ou vers un espace qu'aucune fenêtre n'a encore affiché, se ferme et s'y ouvre avec un nouvel id. Les onglets d'un espace supprimé se ferment avant sa fenêtre, avec `isWindowClosing` à false.
- **debugger :** onglets seulement : ni page d'arrière-plan, ni worker, ni autre cible, donc `getTargets` liste les onglets comme des cibles `page` dont l'`id` est l'id d'onglet de Lilo. Le Chrome DevTools Protocol est traduit vers le protocole de l'Inspecteur web de WebKit pour `Runtime` (`evaluate`, `callFunctionOn`, `awaitPromise`, `getProperties`, libération d'objets, événements de console et d'exception), `Page` (`navigate`, `reload`, `captureScreenshot`, `printToPDF`, `getLayoutMetrics`, arborescences de cadres et de ressources, scripts de nouveau document, événements de chargement et de cadre, événements de cycle de vie `init`, ainsi que `DOMContentLoaded` et `load` pour le cadre principal seulement, à partir de la navigation suivante et que `setLifecycleEventsEnabled` les ait demandés ou non), `Network` (événements, `getResponseBody`, en-têtes supplémentaires, cache, agent utilisateur, `getCookies`), `DOM` (document, requêtes, attributs, HTML, `resolveNode`, `requestNode`, `getBoxModel`, `getContentQuads`, `focus`, recherche, événements de mutation), `Input` (souris, touches, `insertText`) et `Emulation` (agent utilisateur, média et caractéristiques de média, exécution des scripts, facteur d'échelle de l'appareil) ; toute autre méthode échoue avec l'erreur de méthode introuvable de CDP, comme sur un Chrome plus ancien (pas de `Fetch`, `Debugger`, `Target`, `Overlay` ni `CSS`, et pas de `Page.createIsolatedWorld`), bien que le `disable` de chaque domaine réussisse ; `Log`, `Performance` et `Audits` peuvent être activés mais n'envoient aucun événement. `Page.navigate` renvoie un `loaderId` qui lui est propre, pas celui que portent les événements du nouveau document. `captureScreenshot` encode `webp` en PNG et ignore `clip.scale` ; `printToPDF` renvoie la page entière sur une seule page PDF et ignore ses options ; `setDeviceMetricsOverride` n'applique que `deviceScaleFactor` ; les événements réseau n'ont pas de compagnons `ExtraInfo` et ne signalent aucune taille encodée pour les réponses ; `callFunctionOn` abandonne les arguments `unserializableValue` ; le `backendNodeId` d'un nœud est son `nodeId`. Les outils devtools de l'agent et l'Inspecteur web partagent la connexion de protocole de l'onglet, si bien que l'émulation que l'un d'eux définit s'applique à tous.
- **Groupes d'onglets :** ils reposent sur les dossiers d'onglets de Lilo ; les onglets sont mis en correspondance d'une fenêtre à l'autre par position de fenêtre et par index. `tabGroups.move` laisse le groupe où il est. `tabs.group` échoue pour des onglets de fenêtres différentes, ou vers un groupe d'une autre fenêtre, là où Chrome déplace les onglets vers la fenêtre du groupe.
- **history :** `onVisited` se déclenche jusqu'à une seconde après une visite, une seule fois pour toutes les visites qu'une page reçoit dans cette seconde. Lilo n'enregistre pas la manière dont une page a été atteinte : la `transition` d'un `VisitItem` vaut toujours `link`, et `typedCount` vaut toujours 0.
- **Presse-papiers :** `clipboardRead` ne laisse que le document hors écran coller sans geste de l'utilisateur (`document.execCommand('paste')`). Les pages d'extension et le popup lisent le presse-papiers après un geste de l'utilisateur, par la bulle Coller de WebKit, et `navigator.clipboard.read` et `readText` exigent partout un geste de l'utilisateur.
- **downloads :** `onDeterminingFilename` attend au plus 10 s que chaque contexte à l'écoute appelle `suggest`, puis le téléchargement garde son propre nom. Son `DownloadItem` n'a pas encore d'`id`, car Lilo enregistre un téléchargement une fois son fichier choisi. Lorsque plusieurs extensions suggèrent un nom, celle qui a été chargée en dernier l'emporte (Chrome : la plus récemment installée), si bien qu'une extension désactivée puis réactivée l'emporte sur celles installées après elle. L'`overwrite` d'une suggestion sur le téléchargement d'une page supprime le fichier existant au démarrage du téléchargement, et non une fois celui-ci terminé.
- **declarativeNetRequest :** `MAX_NUMBER_OF_DYNAMIC_RULES` et `MAX_NUMBER_OF_SESSION_RULES` valent tous deux 30000, un plafond unique que WebKit partage entre eux. Une extension qui détient `declarativeNetRequest` signale aussi `declarativeNetRequestWithHostAccess` dans `permissions.getAll` et `contains` : Lilo l'accorde pour que les règles de redirection et `modifyHeaders` agissent sur les sites auxquels l'extension peut accéder, comme dans Chrome. `getAvailableStaticRuleCount` répond toujours 30000, le minimum garanti par Chrome, quoi que contiennent les jeux de règles activés. Une exclusion (`excludedRequestDomains`, `excludedRequestMethods`, ou `excludedInitiatorDomains` à côté d'`initiatorDomains`) laisse aussi les requêtes qu'elle exclut, pour les types de ressources et les initiateurs de sa règle, échapper à toutes les règles de l'extension de priorité égale ou inférieure : WebKit la compile en une règle à part entière qui ignore les règles suivantes. `initiatorDomains` et `excludedInitiatorDomains` ne correspondent jamais à un cadre dont l'URL nomme un port (`http://example.com:8080/`). Les règles de session de la dernière exécution que l'extension ne définit pas à nouveau continuent de s'appliquer jusqu'à 30 s après le démarrage de son worker, bien que `getSessionRules` ne les renvoie pas. `getMatchedRules` donne les `request`, `tabId` et `timeStamp` de chaque correspondance mais pas de `rule`, car WebKit ne dit pas quelle règle a correspondu. Les ids de règles dynamiques situés à moins de 30000 de 2147483647 peuvent entrer en collision avec les ids sous lesquels Lilo conserve les règles de session. `testMatchOutcome` (avec `declarativeNetRequestFeedback`) réévalue les règles que WebKit applique avec la mise en correspondance de Chrome. WebKit compile chaque règle en une seule expression régulière sans alternance, sans `\b`, sans références arrière et sans `$` à l'intérieur du motif : un `regexFilter` est réécrit dans ce sous-ensemble là où il garde le même sens (`\w`, `{n,m}`, alternance de caractères uniques, `(X|$)` à la fin, `(^|X)` au début), et une règle dont le filtre ne peut pas l'être, qui redirige vers une URL non HTTP autre que les pages de l'extension elle-même (une URL `data:`), qui ne modifie que des en-têtes que WebKit ne connaît pas (en-têtes `X-` personnalisés), qui est conditionnée à des ids d'onglets ou à des en-têtes de réponse, qui liste des méthodes de requête ou des méthodes exclues à côté de domaines de requête exclus, ou qui autorise toutes les requêtes seulement pour certains initiateurs, un `domainType`, certaines méthodes ou en dehors de certains domaines de requête est conservée sous son id, renvoyée par `getDynamicRules` et `getSessionRules`, mais ne correspond jamais ; `isRegexSupported` répond `syntaxError` pour un tel filtre. Une règle qui modifie à la fois des en-têtes connus et inconnus ne modifie que les en-têtes connus. Une règle `upgradeScheme` sans condition d'URL ni de domaine de requête ne met à niveau que les requêtes `http`, pas les requêtes `ws`. Une règle `regexFilter` ignore ses `requestDomains`. Une règle `requestDomains` ne fait correspondre son `urlFilter` qu'au-delà de l'hôte et, avec un `urlFilter` commençant par `/`, aussi sur les hôtes qui prolongent un domaine (`t.com` pour `t.co`) ; avec un `urlFilter` ancré au début (`|`), elle ne correspond jamais, et avec un filtre ancré sur l'hôte (`||`), WebKit fusionne les deux, ce qui convient à un filtre qui nomme le domaine (`||amazon.*/dp/` pour `amazon.ae`). Son `urlFilter` se terminant par le séparateur `^` ne correspond pas à la fin de l'URL. La `regexSubstitution` d'une redirection conserve le texte de l'URL autour de la correspondance, comme dans Chrome, mais à partir de la dernière correspondance du `regexFilter` là où Chrome prend la première ; une telle règle dont le filtre se termine par `(X|$)`, commence par `(^|X)`, répète un groupe capturant avec `*`, peut en sauter un imbriqué dans une autre partie (`(?:a(b))?`), ou aurait besoin d'un groupe au-delà de `\9` ne correspond jamais. Les jeux de règles statiques sont ainsi réécrits dans leurs fichiers lorsque Lilo prépare l'extension : une extension qui lit son propre fichier de jeu de règles trouve l'original de chaque règle modifiée sous `liloChromeRule`, sauf pour une règle dont seuls les domaines de requête ont changé : ils se terminent par `^`, et ceux qui sont exclus commencent par `||`. WebKit développe une règle en une règle de bloqueur de contenu par domaine de requête et en conserve 150000 par extension ; ainsi, au-delà de 120000 pour l'ensemble de ses jeux de règles statiques, les règles des jeux que le manifeste laisse désactivés partent en premier, et parmi elles, les règles qui ont le plus de domaines de requête ne correspondent jamais, les plus grandes d'abord (les plus grandes listes d'hôtes de SBlock et d'Adblock Ad Blocker Pro) ; ceux que le manifeste active gardent leurs règles jusqu'à ce qu'aucun autre jeu de règles n'en ait plus. La désactivation de tous les jeux de règles juste après une installation attend que WebKit ait compilé une fois les règles de l'extension (quelques secondes pour les grands jeux de règles).
- **runtime.getContexts :** les fenêtres popup issues de `windows.create` sont signalées comme des onglets, et les filtres `documentIds` ne correspondent à rien.
- **Objets et erreurs :** `ChromeContractTests` rejoue des appels enregistrés depuis Chrome (`make chrome-contracts`) et liste dans `LiloTests/Extensions/chrome-contract-differences.txt` les cas où Lilo répond différemment, et ceux où il manque à un objet une propriété qu'exigent les schémas de Chrome : il manque `splitViewId` aux objets `Tab` de WebKit, il manque aussi `pendingUrl`, `autoDiscardable`, `discarded`, `frozen`, `groupId` et `lastAccessed` aux onglets de la fenêtre que renvoie `windows.create`, un `MessageSender` pour une page d'extension ajoute `documentId`, et un événement d'onglet (`tabs.onCreated`, `tabs.onUpdated`) qui atteint une page ou un worker avant l'état d'onglet que Lilo lui envoie (les événements qui démarrent un worker, les premiers d'une fenêtre qu'il n'a pas encore vue) porte les `groupId`, `discarded` et `frozen` d'un onglet chargé hors de tout groupe et, dans `onUpdated`, pas de `lastAccessed` ; il manque `persistAcrossSessions` aux alarmes ; il manque `syncing`, `dateAdded` et, sur les dossiers, `folderType` aux nœuds de favoris ; il manque `homepageUrl`, `isApp`, `mayDisable`, `optionsUrl` et `versionName` aux informations de `management` ; `permissions` répond pour les noms que WebKit ne connaît pas (`tabGroups`, `offscreen`, `debugger`...) à partir du manifeste et des autorisations accordées que Lilo conserve, et `request` n'exige aucun geste de l'utilisateur pour eux ; le `runtime.sendMessage` d'un worker se résout au lieu d'être rejeté lorsque personne ne reçoit le message ; un rappel `chrome` laissé sans réponse reçoit le `runtime.lastError` de Chrome, formulé selon que le worker ou une autre page écoute le message d'une page, et comme un destinataire manquant pour celui d'un worker (là où une page à l'écoute qui ne répond pas reçoit l'autre formulation de Chrome), et un rappel de `browser.runtime` ou de script de contenu n'en reçoit aucun ; et la plupart des messages d'erreur sont formulés différemment. `storage` conserve une `Date` sous la forme de sa chaîne ISO, là où Chrome stocke `{}`. Par `browser` (l'objet de WebKit, là où celui de Chrome partage les espaces de noms de `chrome`), `runtime.lastError` vaut `null` en l'absence d'erreur, il manque ses événements à une copie d'un espace de noms (`{ ...browser.alarms }`), et un espace de noms ne peut pas être remplacé ; une extension qui affecte `browser` (`var browser = chrome`, webextension-polyfill) garde celui de WebKit, par lequel WebKit délivre les événements du contexte. Le `browser.extension` du worker a `getBackgroundPage`, que Chrome omet dans les workers (`chrome.extension.getBackgroundPage` y vaut `undefined`, comme dans Chrome) : AI Exporter, qui le teste par `browser`, ne parvient pas à démarrer.
- **cookies.onChanged :** se déclenche une fois que Lilo a lu le magasin de cookies modifié ; les changements faits pendant sa lecture (la rafale de cookies d'un chargement de page) sont signalés sous la forme de leur résultat net, sauf ceux d'un contexte à l'écoute, dont les `cookies.set` et `remove` se terminent une fois leur changement signalé. Il ne signale qu'une `cause` valant `explicit`, `overwrite` ou `expired` (jamais `evicted` ni `expired_overwrite`) ; définir à nouveau un cookie avec la même valeur et les mêmes attributs ne déclenche rien.
- **scripting et tabs :** `scripting.insertCSS`, lorsqu'il reçoit des `frameIds` autres que celui du cadre principal, ajoute une feuille de style aux `document.adoptedStyleSheets` de chaque cadre, où la page la voit et où un `cssOrigin` valant `USER` s'applique comme un style d'auteur ; `tabs.insertCSS` et `removeCSS`, lorsqu'ils reçoivent un `frameId`, ne s'appliquent qu'au cadre principal. `matchOriginAsFallback` n'est pas pris en charge (WebKit n'a aucun moyen d'injecter dans un cadre d'après l'origine de son parent). WebKit injecte les scripts de contenu du manifeste d'une extension dans les pages déjà ouvertes lorsqu'elle est installée ou activée, comme le fait Safari ; Chrome attend leur prochain chargement, si bien qu'une extension dont le script de contenu interroge son worker au sujet d'une telle page peut ne rien trouver (Avira Browser Safety consigne une erreur). Un script qu'enregistre `scripting.registerContentScripts` s'exécute lui aussi aussitôt dans les pages correspondantes déjà ouvertes, et de nouveau lorsque `updateContentScripts` le modifie, là où Chrome attend leur prochain chargement.
- **contextMenus :** les `documentUrlPatterns` et `targetUrlPatterns` que WebKit n'accepte pas (schémas `ftp:`, `urn:`, `data:`, `about:`, `blob:`, `javascript:`, `mailto:`, `ws:` et `chrome:`, ports explicites, et motifs non valides que Chrome signale comme `Invalid url pattern`) sont abandonnés, si bien qu'un élément ne s'affiche jamais pour une telle page ou un tel lien ; un élément auquel il n'en reste aucun ne s'affiche nulle part. Le rappel d'un `create` sans titre (et d'un `scripting.registerContentScripts` sans motifs de correspondance) s'exécute sans `runtime.lastError`. Un `update` qui déplace un élément sous l'un de ses ancêtres autre que son parent le laisse où il est. Lorsque le seul élément d'une extension pour un lien, un élément multimédia, une sélection ou un champ modifiable n'est pas aussi affiché pour la page entière sous le même titre, Lilo ne peut pas savoir à quelle extension il appartient : le choisir n'est pas un geste de l'utilisateur pour `sidePanel.open`, et un espace privé le masque tant qu'une extension qui n'y est pas autorisée est activée.
- **storage :** les pages d'extension et les workers reçoivent les réponses dans l'ordre où ils font leurs appels, comme dans Chrome. Un `get` peut tout de même manquer un `set` pas encore terminé lorsqu'un autre contexte l'a fait, ou dans un script de contenu, là où Chrome répond aux appels dans l'ordre où ils arrivent ; dans ce cas, plusieurs réponses peuvent aussi se résoudre dans une seule tâche, avant que le code qui attend la première n'ait poursuivi son exécution. Les écritures faites pendant qu'une autre du même contexte est en cours partent ensemble vers WebKit : des `set` de clés distinctes, ou des `remove`, faits l'un après l'autre déclenchent un seul `storage.onChanged` contenant tous leurs changements.
- **devtools :** le `chrome.devtools` de WebKit est partiel : `panels` n'a que `create`, `themeName` et `onThemeChanged` (pas de `elements`, `sources`, `Theme`, `setOpenResourceHandler`, `openResource` ni `setThemeChangeHandler`, donc pas de `createSidebarPane`), `network` seulement `onNavigated` (pas de `onRequestFinished` ni de `getHAR`), `inspectedWindow` seulement `eval`, `reload` et `tabId` (pas de `getResources`, `onResourceAdded` ni `onResourceContentCommitted`), un panneau seulement `onShown` et `onHidden`, et `performance` et `recorder` sont manquants. `inspectedWindow.eval` ignore `contextSecurityOrigin` et `useContentScriptContext` ; `reload` ignore `userAgent` et `injectedScript`. Un inspecteur que Lilo connecte sans l'afficher (pour `chrome.debugger` ou les outils devtools de l'agent) démarre la `devtools_page` de chaque extension qui en a une, comme le fait l'ouverture de l'Inspecteur web, là où Chrome n'ouvre aucun DevTools pour `chrome.debugger`.
- **webRequest :** les détails n'ont pas d'`initiator`, de `frameType` ni de `documentLifecycle`, `statusLine` est le texte d'état seul (`OK`, pas `HTTP/1.1 200 OK`), et l'`error` d'`onErrorOccurred` est la description localisée du système, pas un code `net::ERR_*`. Le `onAuthRequired` d'un proxy, que Lilo signale lui-même, a les `frameType`, `documentLifecycle` et `statusLine` de Chrome.
- **webNavigation :** `onHistoryStateUpdated` et `onReferenceFragmentUpdated` ne se déclenchent que pour les cadres principaux. Lilo ne peut pas savoir comment une navigation a commencé : `onCommitted` signale le `transitionType` `link` pour un cadre principal et `auto_subframe` pour un sous-cadre, et `onCommitted`, `onHistoryStateUpdated` et `onReferenceFragmentUpdated` ne signalent aucun `transitionQualifiers` hormis `forward_back` pour un parcours de l'historique au sein d'un document. `onCreatedNavigationTarget` signale le `sourceFrameId` 0, et rien pour une fenêtre popup de page. `getAllFrames` et `getFrame` signalent le document vide initial d'un onglet, avec une URL vide, jusqu'à la validation de sa première navigation, et une URL vide pour un cadre pour lequel l'extension n'a pas d'autorisation d'hôte. Une page que le préchargement du meilleur résultat charge avant l'appui sur Retour ne déclenche aucun événement `webNavigation` pour ce chargement, ni `onTabReplaced` : elle rejoint l'onglet sous le même id, et `tabs.onUpdated` la signale à partir de là.
- **Panneau latéral :** ancré à droite du contenu (`getLayout` signale toujours `right`). Un panneau que masque un changement d'onglet est libéré et se recharge lorsque son onglet l'affiche de nouveau, là où Chrome conserve la page. `open` accepte un appel depuis une page avec un geste de l'utilisateur, ou un appel qui suit dans les 5 secondes un clic sur l'action de l'extension, sa commande clavier ou son élément de menu contextuel (Chrome : seulement depuis l'écouteur de cet événement, avant tout `await`). Un `setOptions` pour un id d'onglet inconnu de Lilo est ignoré. `onClosed` pour un panneau dont l'onglet ou la fenêtre s'est fermé ne signale aucun `tabId` et un `windowId` de -1.
- **commands :** une commande avec `"global": true` n'agit que lorsque Lilo est l'app active, comme les autres : Lilo n'enregistre aucun raccourci à l'échelle du système.
- **action :** `getUserSettings` signale `isOnToolbar: true` pour une extension épinglée à la barre d'adresse ; les épingles sont partagées par tous les profils, et seules les extensions du profil actif entendent `onUserSettingsChanged`. Une importation depuis Chrome épingle les extensions importées que Chrome avait épinglées, dans l'ordre de sa barre d'outils, après celles qui sont déjà épinglées.
- **permissions :** une demande d'accès à un hôte nomme un onglet (les `documentId` sont inconnus de Lilo) et s'affiche sous la forme d'un bouton Autoriser dans le menu des extensions. Comme dans Chrome, Lilo ne sollicite jamais l'utilisateur pour un site qu'une extension atteint sans y avoir accès (WebKit demanderait l'URL complète de la page) : l'appel échoue, et seul `permissions.request` affiche une invite. Un espace de noms que Lilo ajoute (`pageCapture`, `tabGroups`, `debugger`...) dont l'autorisation n'est que facultative apparaît dans une page ou un worker une fois que celui-ci apprend qu'elle a été accordée, par ses propres `permissions.request`, `contains` ou `getAll` (un worker l'apprend une fois son code de premier niveau exécuté). Accorder ou retirer une telle autorisation, ou une `nativeMessaging` facultative, ne déclenche ni `permissions.onAdded` ni `onRemoved`. Une invite de `permissions.request` est refusée lorsque sa fenêtre se ferme ou affiche un autre profil.
- **pageCapture :** le MHTML est écrit à partir de l'archive web de la page produite par WebKit, si bien que ses parties sont les ressources que WebKit a conservées, chaque URL une seule fois, sans en-têtes `Content-ID`.
- **instanceID :** Lilo n'a pas de backend push : `getToken` et `deleteToken` échouent avec le `Instance ID is currently disabled.` de Chrome et `onTokenRefresh` ne se déclenche jamais (`getID`, `getCreationTime` et `deleteID` fonctionnent comme dans Chrome).
- **cookies.set :** un cookie pour une URL https sans `secure`, venant d'une extension dont les autorisations d'hôte ne nomment que https, est défini comme `secure` : sinon, WebKit vérifie son accès en http.
- **cookies.getPartitionKey :** cadres principaux seulement : un autre `frameId` ou un `documentId` échoue.
- **Sites qu'une page d'extension affiche dans un cadre :** un site auquel l'extension peut accéder reçoit ses cookies, cookies `SameSite` compris, sur chaque requête et chaque WebSocket, et conserve ceux que définissent ses réponses un instant après chaque réponse, comme dans Chrome : pour les WebSockets, Lilo détient des copies de ses cookies `SameSite` partitionnées sous les pages de cette extension tant que l'une d'elles l'affiche dans un cadre, copies que l'API cookies peut lister. Un cadre d'un popup qu'`action.openPopup` a ouvert depuis le worker ne les reçoit qu'une fois le popup chargé. Un site affiché dans un cadre d'un panneau latéral conserve son propre `localStorage`, celui qu'utilisent ses onglets, même s'il s'agit d'un site auquel l'extension ne peut pas accéder, mais son IndexedDB est tenu à l'écart de celui de ses onglets ; affiché dans un cadre des autres pages de l'extension, il reçoit un `localStorage` vidé à chaque chargement. Dans le popup de l'action et la page d'arrière-plan, WebKit filtre la Content Security Policy d'un site affiché dans un cadre comme celle d'une extension, si bien que ses workers `blob:` y sont refusés.
- **omnibox :** une suggestion `deletable` se supprime avec Commande-Suppr.
- **Téléchargements :** les URL `blob:` sont rejetées. Lilo ne marque aucun téléchargement comme dangereux, donc `acceptDanger` échoue toujours ; `setUiOptions` masque le bouton de téléchargement de la barre latérale. Au-delà de 200 téléchargements suivis, ceux qui sont terminés et ceux qui ont été interrompus il y a plus d'un jour cessent d'être suivis pour `onChanged` : la reprise d'un tel téléchargement interrompu ne déclenche aucun `onChanged` pour son premier changement.
- **Notifications :** les `iconUrl` distantes ne sont pas affichées (les URL `data:` en base64 et les fichiers de l'extension elle-même le sont).
- **Identité :** `getProfileUserInfo` est toujours vide : Lilo n'a pas de compte Google au niveau du navigateur. `getAuthToken` exécute le consentement OAuth de Google dans un panneau de connexion où l'utilisateur se connecte (Lilo n'a pas de compte Google au niveau du navigateur), puis met le jeton en cache dans le trousseau ; Google peut refuser un client enregistré uniquement pour Chrome. `launchWebAuthFlow` se termine aussi sur une redirection vers un hôte de bouclage ; un flux non interactif sans `timeoutMsForNonInteractive` abandonne après 30 secondes.
- **publicSuffix :** la Public Suffix List est celle qui est fournie avec Lilo, actualisée à chacune de ses versions. `getDomain` avec `encoding: "display"` affiche un libellé en Unicode lorsqu'il utilise un seul système d'écriture (ou l'un des mélanges chinois, japonais et coréen avec le latin) et qu'il n'est pas composé uniquement de lettres cyrilliques qui se lisent comme des lettres latines ; le vérificateur d'usurpation IDN de Chrome applique davantage de règles, si bien que quelques libellés qu'il conserve en punycode s'affichent ici en Unicode.
- **tts :** `lang` n'est vérifié que pour la forme d'une balise de langue, et le `charIndex` de l'événement `end` compte des unités de code UTF-16 là où Chrome compte des octets UTF-8.
- **i18n.detectLanguage :** utilise le système de reconnaissance de langue du système (jusqu'à trois langues, `isReliable` au-dessus de 80 %), si bien que les résultats peuvent différer de ceux de Chrome.
- **Réglages contrôlés par les extensions** (`proxy`, `fontSettings`, `contentSettings`) : lorsque plusieurs extensions définissent le même, l'extension chargée en dernier l'emporte (Chrome : celle qui a été installée en dernier).
- **browsingData :** WebKit conserve les données de site par site (domaine enregistrable), si bien que `origins` et `excludeOrigins` effacent ou épargnent un site entier, stockage compris, et ignorent alors `since` ; un `since` non nul efface aussi ce que les pages d'extension ont stocké pendant cette période. `formData` efface l'historique des champs de recherche de WebKit. `settings` signale la sélection par défaut de Chrome (Lilo ne conserve aucun choix d'effacement des données). WebKit ne peut pas isoler les données des pages d'extension, si bien que des `originTypes` ne nommant que `extension` ou `protectedWeb` n'effacent aucune donnée de site.
- **contentSettings :** Lilo applique les règles de `camera`, `microphone`, `location`, `notifications`, `popups` et `javascript` ; les autres types conservent et signalent leurs règles, que les pages ne suivent pas. En l'absence de règle, `get` signale l'autorisation de site propre à Lilo, sinon la valeur par défaut de Chrome. Les règles `incognito_session_only` s'appliquent aux espaces privés et durent jusqu'à ce qu'elles soient effacées.
- **fontSettings :** les pages suivent les familles standard et à chasse fixe du système d'écriture par défaut (`Zyyy`) et les trois tailles ; les autres familles et systèmes d'écriture sont seulement conservés et signalés, et les pages d'extension gardent les polices de WebKit.
- **proxy :** appliqué aux onglets et aux extensions du profil une fois l'extension chargée, avec un seul serveur pour tout le trafic (`singleProxy`, sinon `proxyForHttps`, `proxyForHttp`, `fallbackProxy`) ; un proxy `http` reçoit aussi `CONNECT` pour le http simple. `pac_script` (WebKit n'accepte aucun script PAC, par URL ou par données, une fois le profil ouvert), `auto_detect` et `direct`, ainsi que les serveurs `socks4` ou `quic`, laissent la main aux réglages de proxy du système tandis que `get` les signale. `bypassList` conserve les entrées d'hôte et de domaine (`<local>`, `<-loopback>` et les plages d'adresses IP sont abandonnés). Les espaces privés n'utilisent jamais le proxy d'une extension : les portées de navigation privée sont seulement conservées et signalées. La demande d'authentification d'un proxy atteint `webRequest.onAuthRequired` (avec `webRequestAuthProvider`) pour les chargements d'un onglet seulement, jamais pour les requêtes de l'extension elle-même, et sous la forme d'une requête `main_frame` de la page de l'onglet sans `realm` (WebKit ne le signale pas) ; Lilo affiche aussitôt une invite lorsqu'aucun écouteur n'est `blocking` ou `asyncBlocking`, sinon une fois que ceux-ci ne donnent aucun identifiant, ou après 30 secondes. `onProxyError` se déclenche pour une page qui ne peut pas se connecter tant qu'un proxy s'applique (`net::ERR_PROXY_CONNECTION_FAILED` ou `net::ERR_TUNNEL_CONNECTION_FAILED`), jamais pour des erreurs PAC.
- **privacy :** un bouchon : les réglages signalent les valeurs par défaut de Chrome comme `not_controllable`, `set`/`clear` n'ont aucun effet et `onChange` ne se déclenche jamais.
- **storage.managed :** toujours vide (bouchon en lecture seule).
- **storage :** un `set` qui remplace une valeur contenant des caractères hors Latin-1, que WebKit compte mal dans le quota, est appliqué comme une suppression suivie d'une définition : `onChanged` se déclenche pour les deux.
- **gcm :** Lilo n'a pas de backend push Google : `register`, `unregister` et `send` échouent avec le `GCM is currently disabled.` de Chrome et `onMessage`, `onMessagesDeleted` et `onSendError` ne se déclenchent jamais.
- **declarativeContent :** Lilo n'a pas d'actions de page et affiche chaque action sur chaque page : `onPageChanged` conserve ses règles, jusqu'à l'arrêt du worker ou de la page qui les a ajoutées, mais elles n'agissent jamais (`ShowAction`, `SetIcon` et `RequestContentScript` compris).
- **printerProvider :** la zone de dialogue d'impression de macOS ne liste aucune imprimante d'extension, donc ses événements ne se déclenchent jamais.
- **mdns :** donné à toute extension qui déclare l'autorisation `mdns`, là où Chrome n'autorise que des extensions listées. `onServiceList` trouve les types `_tcp` et `_udp` dans le domaine `local` ; une instance qui ne se résout pas en une adresse dans les 5 secondes est omise de la liste. La première exploration peut amener macOS à demander si Lilo peut accéder au réseau local.
- **management :** Lilo n'exécute aucune app : `launchApp`, `createAppShortcut` et `setLaunchType` échouent avec les erreurs de Chrome pour une extension qui n'est pas une app, et `generateAppForLink` échoue après les vérifications de Chrome.
- **Scripts utilisateur :** `runAt: "document_idle"` s'exécute à la fin du document ; `includeGlobs`/`excludeGlobs` sont vérifiés par une garde autour du code, et les sources `js` d'un même enregistrement s'exécutent comme un seul script. Une page prend connaissance de l'interrupteur « Autoriser les scripts utilisateur » à son prochain chargement (un worker redémarre sur place). Les messages des scripts utilisateur portent `frameId`, et le `tab` de leur expéditeur, pour les cadres principaux seulement ; un message auquel personne ne répond échoue après 5 minutes. `userScripts.execute` rejette `documentIds`, et dans un monde `MAIN` dont la CSP de la page refuse `eval`, le résultat d'une promesse vaut null. Le `csp` d'un monde est stocké, pas appliqué : WebKit n'a pas de politique de sécurité du contenu pour les mondes de scripts, et ses mondes isolés ne sont pas non plus liés par celle de la page.
- **tabCapture et desktopCapture :** les flux sont uniquement vidéo, issus de la navette de partage d'onglet (au plus 30 images par seconde et 1920 pixels de large) : WebKit n'expose aucune prise audio par onglet ni pour le système, si bien qu'un `getUserMedia` qui demande l'audio seul avec `chromeMediaSource` est rejeté avec `NotSupportedError`, qu'une demande d'audio à côté de la vidéo n'obtient que la piste vidéo, et que `chooseDesktopMedia` signale toujours `canRequestAudioTrack: false`. Les autres contraintes `mandatory` et `optional` sont ignorées. `desktopCapture` affiche le sélecteur d'onglets de Lilo lorsque `tab` est une source (avec un choix « écran ou fenêtre » lorsque ceux-ci le sont aussi), sinon le sélecteur de partage de contenu du système ; `cancelChooseDesktopMedia` ne peut pas fermer le sélecteur du système une fois affiché, et son choix est alors abandonné. `onStatusChanged` déclenche `pending` et `active` ensemble au démarrage du flux, et `getCapturedTabs` ne liste que les flux déjà démarrés.
- **Cadres d'extension :** une page web affichée dans un cadre à l'intérieur d'une page d'extension ne reçoit aucun des ajouts de Lilo.
- **Localisation et notifications :** les pages d'extension reçoivent les `navigator.geolocation` et `Notification` de Lilo, accordés sans demande lorsque l'extension détient l'autorisation `geolocation` ou `notifications`, requise ou facultative et accordée ; sinon, une page dans un onglet demande comme le fait un site, et un popup, un panneau latéral ou un document hors écran essuie un refus. Les scripts de contenu gardent ceux de WebKit, qui ne délivrent aucune position et n'affichent aucune notification, et le worker garde le `showNotification` de WebKit.
- **Globales du moteur :** les pages d'extension et le worker n'ont pas de `SharedArrayBuffer`, même avec les `cross_origin_embedder_policy` et `cross_origin_opener_policy` du manifeste, que WebKit ignore : Distill Web Monitor ne peut pas ouvrir son magasin de données. Ils reçoivent le `navigator.connection` de Lilo (WebKit n'en a pas), qui se lit toujours comme une connexion rapide (`effectiveType` `4g`, `rtt` 50, `downlink` 10, `saveData` false) et comme une vitesse nulle hors ligne ; son `change` se déclenche lorsque le réseau disparaît ou revient, jamais pour un changement de vitesse. Ils reçoivent aussi, ainsi que les scripts de contenu, `Symbol.dispose` et `Symbol.asyncDispose`, que WebKit omet, si bien que le `using` compilé par TypeScript, esbuild ou Babel s'exécute (le déverrouillage du coffre de Bitwarden) ; la syntaxe `using` native y reste une erreur de syntaxe, et `DisposableStack` et `AsyncDisposableStack` sont manquants ; les scripts du monde principal (`world: 'MAIN'`) ne reçoivent pas `Symbol.dispose`, et les itérateurs n'ont ni `[Symbol.dispose]` ni `[Symbol.asyncDispose]`. Il manque aussi à JavaScriptCore `Temporal`, `Iterator.prototype.join` et `includes`, `Iterator.zip` et `zipKeyed` ; avant macOS 26.3, `Iterator.concat` ; avant 26.1, les `getOrInsert` et `getOrInsertComputed` de `Map` et de `WeakMap` ainsi que `Math.sumPrecise`. `Date.parse` renvoie `NaN` pour des formats que V8 accepte (`01-02-2024`, `2024.01.02`, `02.01.2024`, une heure suivie de `UTC`). Sur macOS 26, un module avec un `await` de premier niveau que plusieurs modules importent en même temps peut rejeter un import avec une `ReferenceError` (webkit.org/b/242740). `Error.prepareStackTrace` n'a aucun effet et les piles gardent le format de JavaScriptCore (`Error.captureStackTrace` existe). Lilo n'ajoute de polyfill pour aucun d'entre eux. Les pages que Lilo ouvre (panneau latéral, document hors écran, onglets d'extension) reçoivent les indicateurs de fonctionnalités WebKit que reçoivent les onglets, remplacements d'`about:config` compris ; le popup et le worker, que WebKit crée, gardent les valeurs par défaut de WebKit, si bien que `requestIdleCallback` et `showPicker()` peuvent y manquer.

### Extensions du store testées [#tested-store-extensions]

Mesuré le 23 octobre 2026 avec `make ext-smoke TOP=30` (les 30 extensions les plus utilisées de `top-extensions.json` tel qu'il se présentait alors, les gestionnaires de mots de passe que propose la configuration initiale et les pages de nouvel onglet de la liste), par rapport à l'enregistrement de Chrome 156 de l'époque (`make chrome-smoke`) : chacune s'installe depuis le store, son worker démarre et son popup s'affiche. Les gestionnaires de mots de passe ont aussi ouvert un formulaire de connexion local, sans être connectés, pour vérifier que leurs scripts de contenu l'atteignent. Les erreurs que Chrome signale aussi ne comptent pas. `ext-smoke` vérifie aussi, sur une page de connexion locale et sans être connecté, que le thème de Dark Reader s'applique et que son raccourci le désactive pour le site, que Grammarly, Quillbot, LastPass, Dashlane, Proton Pass, Keeper, DeepL et Immersive Translate marquent la page, que DeepL et AdBlock ajoutent leurs entrées de menu contextuel, qu'Adblock Plus bloque une requête publicitaire, que les boutons de barre d'outils de WhatFont et de SelectorGadget agissent sur la page et que celui de Claude ouvre son panneau latéral. Depuis le 24 octobre 2026, il vérifie aussi qu'uBlock Origin Lite et AdGuard bloquent cette requête publicitaire ; que React, Vue.js et Redux DevTools s'accrochent à la page dès son premier script ; que JSON Formatter et JSONVue mettent en forme un document JSON ; que Video Speed Controller ajoute son contrôleur à une vidéo en cours de lecture ; que LanguageTool et Wordtune atteignent la page ; que le bouton de barre d'outils de Sider ouvre son panneau latéral, celui de Session Buddy son gestionnaire et celui de GoFullPage une capture de la page entière. Ghostery ne bloque rien tant que les conditions de sa configuration initiale ne sont pas acceptées, ce que le test ne fait pas. `top-extensions.json` liste aussi des extensions choisies à la main en dehors des plus utilisées (`extra`), qu'`ext-smoke` exécute à chaque fois. Rien ici n'était connecté à un compte, donc les fonctionnalités nécessitant une connexion (remplissage automatique, traduction, suggestions grammaticales) ne sont pas vérifiées. La liste et l'enregistrement de Chrome (désormais celui de Chrome 157) ont été mesurés de nouveau depuis, si bien que le tableau ci-dessous ne correspond pas aux 30 extensions les plus utilisées d'aujourd'hui : il liste les extensions mesurées alors, plus Kami, et ses états suivent les listes `knownFailures` et `intermittent` actuelles de `RealExtensionSmokeTests`.

Le 24 octobre 2026, au-delà de leurs popups et sans être connectés : la pipette de ColorZilla échantillonne la page, Wappalyzer liste les technologies d'une page locale, FireShot capture une page entière, Okta et 2FAS Auth ne touchent pas à un formulaire de connexion (comme dans Chrome), et Google Docs Offline et Application Launcher for Drive démarrent leurs workers sans erreur. Le bloqueur de contenu de Lilo et Adblock Plus bloquent côte à côte sans casser la page, une requête que l'un ou l'autre bloque échouant une seule fois ; chacun garde sa propre liste d'autorisation, si bien qu'un site mis en pause dans Adblock Plus est toujours filtré par le bloqueur de Lilo jusqu'à ce qu'il y soit autorisé lui aussi.

`top-extensions.json`, mesuré le 24 octobre 2026, liste les 1000 extensions les plus utilisées. `RealExtensionSmokeTests` liste, avec leur cause, les 36 extensions connues pour ne pas s'exécuter sans problème (`knownFailures`) et les 30 qui échouent lors de certaines exécutions (`intermittent`) ; 26 et 22 d'entre elles figurent parmi les 500 les plus utilisées. Pour 11 de ces 66 causes, Chrome échoue, ou échouerait, de la même manière, ou dispute la même course ; les autres désignent une différence dans WebKit, JavaScriptCore ou Lilo, un élément du test lui-même, ou une cause encore inconnue. Une extension dont la version du store diffère de celle que contient l'enregistrement de Chrome n'est pas comparée au rendu de son popup et de ses pages dans Chrome jusqu'à ce que `make chrome-smoke` l'enregistre de nouveau ; les erreurs que Chrome a enregistrées pour elle ne comptent toujours pas.

| Extension | État | Diffère de Chrome |
| --- | --- | --- |
| Adobe Acrobat, McAfee WebAdvisor, Thunder download support, Chrome Remote Desktop, Google Translate, Microsoft Single Sign On, Cisco Webex, uBlock Origin Lite, 360 Internet Protection, Claude, NordVPN, VeePN, Honey, Adblock for Youtube, Online Security, Teleparty, Authenticator, Foxit PDF Creator, 1clickVPN, Screencastify | fonctionne | rien de mesuré |
| Grammarly | fonctionne avec des limites | certaines exécutions consignent une erreur absente de l'enregistrement de Chrome : à son premier démarrage, elle injecte un script dans sa page de bienvenue à côté de celui qu'elle déclare, et la seconde exécution ne parvient pas à redéclarer un nom : Lilo signale l'adresse de la page de bienvenue pendant son chargement, là où Chrome n'en signale aucune, comme pour 1Password |
| AdBlock | fonctionne avec des limites | sa page de bienvenue consigne une erreur : deux de ses entrées de scripts de contenu listent le même script, que WebKit exécute deux fois dans une même page, là où Chrome ne signale rien |
| Adblock Plus | fonctionne avec des limites | elle bloque les requêtes publicitaires, mais sa page d'options lit les réponses de son worker (sa langue, ses fonctionnalités et ses abonnements) comme undefined, là où celle de Chrome s'affiche sans erreur |
| Malwarebytes Browser Guard | fonctionne avec des limites | ses filtres cosmétiques ne masquent aucune des publicités de la page de test, que ceux de Chrome masquent ; une fois son worker inactif, son popup au-dessus d'un onglet vide affiche moins de la moitié de celui de Chrome ; son blocage des requêtes n'a pas été testé |
| GoFullPage | fonctionne avec des limites | sa page de bienvenue ne trouve aucune fenêtre qui la contient (« No matching window found »), là où celle de Chrome en trouve une |
| Zotero Connector | non vérifié | sur la page de test, son script de contenu ne parvient pas à mettre en place la traduction, qui est sa manière de lire ce que contient une page : son worker crée deux fois son document hors écran (« Only a single offscreen document may be created ») ; l'enregistrement dans Zotero n'a pas été testé |
| Kami | non vérifié | sa page d'options n'affiche rien en 20 secondes, là où celle de Chrome affiche ses réglages ; rien d'autre de ce qu'elle fait n'a été testé |
| Momentum, Bonjourr | fonctionne | rien de mesuré (remplacement du nouvel onglet) |
| NordPass | fonctionne | rien de mesuré : elle marque ou décore le formulaire de connexion |
| Dashlane | fonctionne avec des limites | elle décore le formulaire de connexion comme dans Chrome, mais consigne une erreur au démarrage : tant que ses réglages ne sont pas encore renseignés, ses informations d'appareil se rabattent sur une commande qu'elle n'a pas (`getCommonAppSetting: a is not a function`) ; dans Chrome, ils le sont déjà à ce moment-là |
| Proton Pass | fonctionne | rien de mesuré : elle injecte son interface de formulaire dans le formulaire de connexion |
| Bitwarden | fonctionne | sans connexion, elle ne modifie le formulaire de connexion dans aucun des deux navigateurs ; lors de certaines exécutions, la mise à jour de son badge rencontre un onglet que le test a fermé entre-temps (`No tab with id`) |
| KeePassXC-Browser | fonctionne | elle ajoute son élément au formulaire de connexion, comme dans Chrome ; son app de bureau, atteinte par la messagerie native, n'a pas été testée |
| 1Password | fonctionne avec des limites | un onglet activé pendant le démarrage de son cœur (dans un délai d'environ 150 ms) consigne « WASM is not initialized » : Chrome signale un nouvel onglet en cours de chargement sans url, que 1Password ignore, là où Lilo signale sa cible, car les propres vérifications d'accès de WebKit sur cet onglet en ont besoin ; le formulaire de connexion reçoit son menu à la prise de focus, comme dans Chrome ; son app de bureau (déverrouillage et Touch ID) nécessite l'étape décrite dans [Gestionnaires de mots de passe](#password-managers) |
| AdGuard AdBlocker | fonctionne avec des limites | son popup n'affiche parfois rien : il peut envoyer un message à son worker avant que celui-ci ait mis en place le gestionnaire, une course que Chrome dispute aussi |
| Keeper, Okta Browser Plugin, Wappalyzer, WhatFont, SelectorGadget, Google Docs Offline, Application Launcher for Drive | fonctionne | rien de mesuré |
| ColorZilla | fonctionne | lors de certaines exécutions, elle injecte un script ou envoie un message dans un onglet que le test vient de fermer (`Tab not found`) ; au-dessus d'un onglet vide, WebKit refuse son `tabs.captureVisibleTab` |
| FireShot | fonctionne avec des limites | elle capture une page entière, mais au-dessus d'un onglet vide, son popup propose de capturer cet onglet, là où l'enregistrement de Chrome ouvre le popup dans un onglet à part |
| 2FAS Auth | couvert par des tests unitaires seulement | elle s'installe et démarre, mais l'exécution du test ne peut pas voir le blocage dont il est question dans cette ligne : son worker stocke ses clés dans IndexedDB, ce qui le bloquait jusqu'à ce que Lilo les stocke lui-même (voir Worker d'arrière-plan dans [Différences d'API](#api-differences)), et seuls des tests unitaires vérifient ce stockage ; le jumelage avec son app pour téléphone n'a pas été testé |
| Endpoint Verification | ne fonctionne pas | elle lit `chrome.enterprise.reportingPrivate`, une API privée que Chrome ne donne qu'à cette extension Google, pour signaler l'appareil à Google Workspace ; Lilo ne peut pas la fournir, si bien que les politiques d'accès qui exigent un appareil vérifié refusent Lilo |
| Tampermonkey | fonctionne | rien de mesuré : comme dans Chrome, ses scripts nécessitent « Autoriser les scripts utilisateur » |

### Agent utilisateur et Chrome Web Store [#user-agent-and-the-chrome-web-store]

Lilo se présente comme Chrome là où les extensions s'y attendent (`ChromeMasquerade`), et comme Safari partout ailleurs :

- **Les pages web** reçoivent l'agent utilisateur Safari de WebKit, sans jeton Lilo, si bien que les sites servent leur variante Safari.
- **Le Chrome Web Store** (`chromewebstore.google.com`) reçoit un agent utilisateur Chrome et `navigator.userAgentData`, ainsi qu'un shim `chrome.webstorePrivate`, si bien que son bouton d'installation fonctionne ; Lilo le renomme « Ajouter à Lilo » et masque les invites du store à passer à Chrome. Les paquets et les vérifications de mise à jour que Lilo télécharge depuis `clients2.google.com` envoient le même agent utilisateur Chrome.
- **Le code des extensions** (les pages d'extension, le worker d'arrière-plan et le monde isolé des scripts de contenu) lit les `navigator.userAgent`, `appVersion`, `vendor` et `userAgentData` de Chrome ; la page autour d'un script de contenu garde celui de Safari.
- **Requêtes des extensions :** les workers, les popups et les panneaux que WebKit crée pour une extension envoient l'agent utilisateur de Chrome, si bien que les serveurs qu'atteignent les propres requêtes d'une extension voient Chrome.
- **Ce que cela signifie pour vous :** les extensions s'installent et se mettent à jour depuis le store comme dans Chrome, et les pages que vous consultez reçoivent l'agent utilisateur de Safari. Le store et ses conditions sont faits pour Chrome, et Google ne cautionne pas Lilo : Google pourrait modifier le store ou son service de mise à jour de sorte que les installations ou les mises à jour depuis Lilo cessent de fonctionner.

### Gestionnaires de mots de passe [#password-managers]

- **Approche :** Lilo ne fournit volontairement **aucun gestionnaire de mots de passe intégré** : le stockage des identifiants et le remplissage automatique sont laissés aux extensions et au système. La configuration initiale propose d'en installer un depuis le store. Les gestionnaires testés figurent dans [Extensions du store testées](#tested-store-extensions) ; le remplissage automatique avec un compte connecté n'est pas vérifié.
- **Messagerie native :** `chrome.runtime.connectNative` et `sendNativeMessage` suivent le protocole stdio de Chrome (`ExtensionNativeMessaging`) pour les extensions qui déclarent `nativeMessaging` dans `permissions`, ou dans `optional_permissions` une fois que `permissions.request` l'a fait accorder. Un hôte qui restreint les navigateurs autorisés à le lancer refuse toujours Lilo (voir [Extension Mots de passe iCloud](/fr/reference/web-compatibility#icloud-passwords-extension)).
- **Les manifestes d'hôte** (`<name>.json`, `type: stdio`, `path` absolu) sont lus dans les dossiers `NativeMessagingHosts` dans cet ordre : sous `~/Library/Application Support/`, `Lilo`, `Google/Chrome`, `Google/Chrome Beta`, `Google/Chrome Dev` et `Google/Chrome Canary` ; puis `/Library/Google/Chrome/NativeMessagingHosts` ; `Chromium` sous `~/Library/Application Support/`, puis `/Library/Application Support/Chromium/NativeMessagingHosts` ; `Microsoft Edge` sous `~/Library/Application Support/`, puis `/Library/Microsoft/Edge/NativeMessagingHosts` ; et enfin `BraveSoftware/Brave-Browser`, `Arc/User Data` et `Vivaldi` sous `~/Library/Application Support/`. Le dossier propre à Lilo passe en premier ; le premier manifeste dont les `allowed_origins` listent `chrome-extension://<id>/` l'emporte. Les manifestes de Firefox ne sont pas lus. L'hôte reçoit cette origine comme `argv[1]`.
- **Échecs :** un port `chrome.runtime.connectNative` vers un hôte manquant, refusé ou qui se termine se déconnecte avec l'erreur de Chrome comme `runtime.lastError`. Un port ouvert par le `browser.runtime` de WebKit se déconnecte sans elle, et seulement une fois qu'il envoie son premier message (un port qui n'envoie jamais rien reste ouvert). `sendNativeMessage` échoue avec `Native host has exited.` lorsque son hôte ne répond pas et ne se termine pas dans les 30 s, là où Chrome attend aussi longtemps que vit le worker.
- **1Password** ne se déverrouille avec son app de bureau (et Touch ID) qu'une fois Lilo ajouté dans Réglages > Navigateur > Ajouter un navigateur de l'app : l'app ne se connecte d'elle-même qu'aux navigateurs qu'elle connaît (Chrome, Firefox, Edge, Brave, Arc).
- **Dashlane** consigne `Unable to find "argon2.wasm" in the extension's resources` dans son worker : Emscripten résout le binaire par rapport au répertoire du worker lui-même au lieu d'utiliser les octets que Dashlane récupère depuis `assets/argon2/`. Ce n'est pas une lacune de WebKit, et Lilo ne relance pas un worker pour cela : seul un arrière-plan dont le contenu n'a pas pu se charger est relancé (`errorsWarrantBackgroundRevive`).
- **`.wasm` empaqueté :** WebKit sert les ressources d'extension sans le type `application/wasm`, ce que `WebAssembly.instantiateStreaming` et `compileStreaming` rejettent. Pour les réponses de l'extension elle-même, Lilo compile plutôt à partir des octets (`shared/wasm-mime.ts`), dans les pages comme dans le worker.
