From 8cf2a475cb3d48f9a62ea632832ae876f611876e Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Tue, 3 Dec 2024 14:25:49 +0530 Subject: [PATCH 1/7] Add proposal for runtime.onEnabled and runtime.onExtensionLoaded events --- .../runtime_on_load_on_enabled_events.md | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 proposals/runtime_on_load_on_enabled_events.md diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md new file mode 100644 index 00000000..390339c4 --- /dev/null +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -0,0 +1,165 @@ +# Proposal: New runtime.onEnabled and runtime.onExtensionLoaded events + +**Summary** + +Creates two new extension state notification events that will help developers +take action on the full “lifecycle” of their extensions. + +**Document Metadata** + +**Author:** jlulejian@chromium.org + +**Sponsoring Browser:** Google Chrome and Microsoft Edge + +**Contributors:** sorajdev@microsoft.com + +**Created:** 2024-12-03 + +**Related Issues:** https://github.com/w3c/webextensions/issues/353 + +## Motivation + +### Objective + +Enable two new extension lifecycle events for the developer to add listeners for: + +#### chrome.runtime.onEnabled +Enables a developer to listen to when an extension is re-enabled from a disabled +state. There is currently no way for a developer to listen for the extension +being disabled and then enabled. + +### chrome.runtime.onExtensionLoaded +Enables a developer to listen to when an extension is added to the set of active +extensions. This can occur from being installed, enabled, or when the browser / +profile first starts. The listener will be given the cause so they can decide to +take different actions based on the reason it was added to the set. There is +currently no generic event that encapsulates all instances of extension install +(and update), startup, and being enabled from a disabled state. + +#### Use Cases `chrome.runtime.onEnabled` +A developer wants to run code once in an extension’s "lifecycle", without storing +a flag to keep track of this. + +Before this API the developer would need to manually keep track of this state +via a storage flag in persistent storage. + +With this new API a single listener can be registered and reduce the extension’s +complexity. + +More use cases can be found [here](https://github.com/w3c/webextensions/issues/353#issuecomment-1490078217). + +#### Use Cases `chrome.runtime.onExtensionLoaded` +An extension automatically does initial bootstrapping, such as creating an +initial state used throughout the browser session, creating a WebSocket +connection, checking for things you've missed while the browser was closed, and +putting something in chrome.storage.session. + +Before this API the developer would have to create multiple listeners for +`chrome.runtime.onInstalled()`, `onStartup()`, and (above) `onEnabled()`. + +With this new API method the developer could write one method that performs the +same action for all three, potentially with small differences based on the +loaded cause. + +More use cases can be found [here](https://github.com/w3c/webextensions/issues/353#issuecomment-1582536300). + +### Known Consumers + +This is a broadly applicable API method that will be helpful to any developer +wanting to execute code as the extension changes running states. But there +appears to be strong interest per the [WECG discussion](https://github.com/w3c/webextensions/issues/353). + +## Specification + +### Schema +#### `chrome.runtime.onEnabled` +```typescript +namespace runtime { + // Fired when an extension goes from being in a disabled state to an enabled + // state. + export interface onEnabled { + addListener(callback: () => void): void; + removeListener(callback: () => void): void; + } +} +``` + +#### `chrome.runtime.onExtensionLoaded` +```typescript +namespace runtime { + export type OnLoadedReason = 'enabled' | 'installed' | 'updated' | 'profileStart'; + + export interface ExtensionLoadDetails { + // The reason that this event is being dispatched. + onLoadReason: OnLoadedReason; + + // Indicates the previous version of the extension, which has just been + // updated. This is present only if 'reason' is 'updated' or ‘enabled’. + previousVersion?: string; + } + + // Fired when the extension is added to the set of active extensions. This can + // occur in multiple scenarios: when an extension is first installed, updated + // to a new version, and enabled after being disabled. + export interface onExtensionLoaded { + addListener(callback: (details: ExtensionLoadDetails) => void): void; + removeListener(callback: (details: ExtensionLoadDetails) => void): void; + } +} +``` + +### Behavior + +Described as code comments in schema description. + +### New Permissions + +No new permissions are needed as extensions can access the `browser.runtime` +API without any permissions. + +### Manifest File Changes + +No new manifest changes are added. + +## Security and Privacy + +### Exposed Sensitive Data + +The new events expose some information that wasn’t present before. It does +expose that an extension was (explicitly) disabled, which is something we don't +expose today. Today, an extension cannot necessarily distinguish between being +disabled and the user simply not opening the browser for <n> amount of +time. + +### Abuse Mitigations + +These APIs would be difficult to use in an abusive way since they do not give +information or access to other extensions, and don’t give control into the +extensions lifecycle – only knowledge as it transitions to different states. + +### Additional Security Considerations + +N/A. + +## Alternatives + +### Existing Workarounds + +There is currently `chrome.runtime.onInstalled`, and `chrome.runtime.onStartup`. +These API have gaps in listening that are addressed by these two events. In +short, `chrome.runtime.onEnabled`, covers the remaining status that developers +care to know, and `chrome.runtime.onExtensionLoaded` encapsulates them all +together with a single listener. + +### Open Web API + +These API methods are specific to extension lifetime / lifecycle and would not +be appropriate to be in the open web. + +## Implementation Notes + +N/A. + +## Future Work + +N/A. From 74d937a0e150519a22a4ffd8526732e49d3d4452 Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Tue, 3 Dec 2024 16:21:09 +0530 Subject: [PATCH 2/7] Add "reload" reason --- proposals/runtime_on_load_on_enabled_events.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md index 390339c4..8251e891 100644 --- a/proposals/runtime_on_load_on_enabled_events.md +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -87,7 +87,23 @@ namespace runtime { #### `chrome.runtime.onExtensionLoaded` ```typescript namespace runtime { - export type OnLoadedReason = 'enabled' | 'installed' | 'updated' | 'profileStart'; + // The reason for which the event is being dispatched. + // + // 'enabled': The extension was re-enabled from a disabled state. + // + // 'installed': The extension was newly installed. + // + // 'updated': The extension was reloaded after an update. + // + // 'profileStart': The extension is being loaded during a profile startup. + // + // 'reload': The extension was reloaded (e.g. via `chrome.runtime.reload() or` + // the user manually reloaded the extension). + export type OnLoadedReason = 'enabled' | + 'installed' | + 'updated' | + 'profileStart' | + 'reload'; export interface ExtensionLoadDetails { // The reason that this event is being dispatched. From 0b97ba91dae754a4ad5ce428e7ab47d25e168fa4 Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Thu, 12 Dec 2024 15:30:35 +0530 Subject: [PATCH 3/7] Rename event arguments --- proposals/runtime_on_load_on_enabled_events.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md index 8251e891..55132dda 100644 --- a/proposals/runtime_on_load_on_enabled_events.md +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -95,19 +95,19 @@ namespace runtime { // // 'updated': The extension was reloaded after an update. // - // 'profileStart': The extension is being loaded during a profile startup. + // 'started': The extension is being loaded during browser startup. // // 'reload': The extension was reloaded (e.g. via `chrome.runtime.reload() or` // the user manually reloaded the extension). - export type OnLoadedReason = 'enabled' | + export type OnLoadedReason = 'enabled' | 'installed' | 'updated' | - 'profileStart' | + 'started' | 'reload'; export interface ExtensionLoadDetails { // The reason that this event is being dispatched. - onLoadReason: OnLoadedReason; + reason: OnLoadedReason; // Indicates the previous version of the extension, which has just been // updated. This is present only if 'reason' is 'updated' or ‘enabled’. From 917c53169c4245ffdf918b105101bfb8f0be28a7 Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Wed, 18 Dec 2024 12:57:01 +0530 Subject: [PATCH 4/7] Rename 'started' load reason to 'startup' --- proposals/runtime_on_load_on_enabled_events.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md index 55132dda..b3e78976 100644 --- a/proposals/runtime_on_load_on_enabled_events.md +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -95,14 +95,14 @@ namespace runtime { // // 'updated': The extension was reloaded after an update. // - // 'started': The extension is being loaded during browser startup. + // 'startup': The extension is being loaded during browser startup. // // 'reload': The extension was reloaded (e.g. via `chrome.runtime.reload() or` // the user manually reloaded the extension). export type OnLoadedReason = 'enabled' | 'installed' | 'updated' | - 'started' | + 'startup' | 'reload'; export interface ExtensionLoadDetails { From 2f8891e86d80e26aab2b3f9ae7181040ee760821 Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Fri, 20 Dec 2024 10:56:18 +0530 Subject: [PATCH 5/7] Add hasListener() to event interface --- proposals/runtime_on_load_on_enabled_events.md | 1 + 1 file changed, 1 insertion(+) diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md index b3e78976..5968c514 100644 --- a/proposals/runtime_on_load_on_enabled_events.md +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -119,6 +119,7 @@ namespace runtime { // to a new version, and enabled after being disabled. export interface onExtensionLoaded { addListener(callback: (details: ExtensionLoadDetails) => void): void; + hasListener(callback: (details: ExtensionLoadDetails) => void): boolean; removeListener(callback: (details: ExtensionLoadDetails) => void): void; } } From 1cb6cbd0af87a5071aa76296ebbc24e9883bc4d9 Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Wed, 22 Jan 2025 13:35:31 +0530 Subject: [PATCH 6/7] Remove a confusing example and add information about split mode incognito contexts --- proposals/runtime_on_load_on_enabled_events.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md index 5968c514..324309e0 100644 --- a/proposals/runtime_on_load_on_enabled_events.md +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -51,8 +51,7 @@ More use cases can be found [here](https://github.com/w3c/webextensions/issues/3 #### Use Cases `chrome.runtime.onExtensionLoaded` An extension automatically does initial bootstrapping, such as creating an initial state used throughout the browser session, creating a WebSocket -connection, checking for things you've missed while the browser was closed, and -putting something in chrome.storage.session. +connection, checking for things you've missed while the browser was closed etc. Before this API the developer would have to create multiple listeners for `chrome.runtime.onInstalled()`, `onStartup()`, and (above) `onEnabled()`. @@ -61,6 +60,10 @@ With this new API method the developer could write one method that performs the same action for all three, potentially with small differences based on the loaded cause. +Some browsers (e.g. Chromium) support running a separate instance of the background +script in incognito mode, by setting `incognito: "split"` in the extension manifest. +This event will be fired for such instances separately. + More use cases can be found [here](https://github.com/w3c/webextensions/issues/353#issuecomment-1582536300). ### Known Consumers From c7f3da620a9f08aa0495eafa809358facd751d15 Mon Sep 17 00:00:00 2001 From: Sohail Rajdev Date: Fri, 31 Jan 2025 08:20:18 +0530 Subject: [PATCH 7/7] Clarify repeated firing in split mode incognito contexts Co-authored-by: Rob Wu --- proposals/runtime_on_load_on_enabled_events.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/proposals/runtime_on_load_on_enabled_events.md b/proposals/runtime_on_load_on_enabled_events.md index 324309e0..92d8404e 100644 --- a/proposals/runtime_on_load_on_enabled_events.md +++ b/proposals/runtime_on_load_on_enabled_events.md @@ -62,7 +62,10 @@ loaded cause. Some browsers (e.g. Chromium) support running a separate instance of the background script in incognito mode, by setting `incognito: "split"` in the extension manifest. -This event will be fired for such instances separately. +This event will be fired for such instances separately. The event may fire +repeatedly during the same browser session if the incognito session ends and +(re)starts, for example by closing the last incognito window and opening a new +incognito window. More use cases can be found [here](https://github.com/w3c/webextensions/issues/353#issuecomment-1582536300).