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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 39 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pnpm add @addon-core/browser
- [extension](docs/extension.md)
- [history](docs/history.md)
- [i18n](docs/i18n.md)
- [identity](docs/identity.md) — Chrome full support; Firefox, Edge, and Opera support the portable web auth flow; Safari is not supported.
- [idle](docs/idle.md)
- [management](docs/management.md)
- [notifications](docs/notifications.md)
Expand Down Expand Up @@ -77,49 +78,67 @@ pnpm add @addon-core/browser

## Usage examples

- setActionPopup (works in MV2 & MV3):
### Action API across MV2 and MV3

```ts
import { setActionPopup } from "@addon-core/browser";
import {setActionPopup, setBadgeText} from "@addon-core/browser";

await setActionPopup("popup.html");
// Optional per-tab usage (when you have a tab id):
// await setActionPopup("popup.html", someTabId);
await setBadgeText("ON");
```

- getCurrentTab:
`setActionPopup()` works with `chrome.action` in Manifest V3 and `chrome.browserAction` in Manifest V2.

### Tabs and events

```ts
import { getCurrentTab } from "@addon-core/browser";
import {getActiveTab, onTabUpdated} from "@addon-core/browser";

const tab = await getActiveTab();

const tab = await getCurrentTab();
if (tab?.id) {
console.log("Current tab id:", tab.id);
}
const off = onTabUpdated((tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.status === "complete") {
off();
}
});
```

- onTabUpdated (with unsubscribe):
Event helpers return an unsubscribe function, making listener lifecycles easy to control.

### Context menus and tab messaging

```ts
import { onTabUpdated } from "@addon-core/browser";
import {createOrUpdateContextMenu, onContextMenusClicked, sendTabMessage} from "@addon-core/browser";

const off = onTabUpdated((tabId, changeInfo, tab) => {
if (changeInfo.status === "complete") {
console.log("Tab finished loading:", tabId, tab.url);
}
await createOrUpdateContextMenu("save-selection", {
title: "Save selection",
contexts: ["selection"],
});

// Later, to stop listening:
off();
const off = onContextMenusClicked(async (info, tab) => {
if (info.menuItemId !== "save-selection" || !tab?.id) {
return;
}

await sendTabMessage(tab.id, {
type: "save-selection",
text: info.selectionText,
});
});
```

`createOrUpdateContextMenu()` avoids duplicate-menu errors during extension reloads, and `sendTabMessage()` keeps the background-to-content-script path promise-based.

## Helpers

- [browserDetection](docs/browserDetection.md) — Best-effort browser detection with `BrowserName`, `BrowserFamily`, `guessBrowser()`, `isBrowser()`, and `isBrowserFamily()`.

## Utilities

In addition to Chrome API wrappers, this package provides a set of low-level utilities for error handling, promise management, and listener safety. While these are primarily used internally, they are also exported via the `@addon-core/browser/utils` subpath for advanced usage.

For a complete list of utility functions and examples, see the [Utilities Documentation](docs/utils.md).


## Not yet covered

These commonly used WebExtensions/Chrome Extension APIs are not wrapped here yet (Chrome OS–only APIs are intentionally omitted). If you’d like to contribute, please see [CONTRIBUTING.md](CONTRIBUTING.md) and open an issue/PR.
Expand All @@ -132,8 +151,6 @@ These commonly used WebExtensions/Chrome Extension APIs are not wrapped here yet
- devtools.* (inspectedWindow, network, panels)
- dns
- fontSettings
- identity
- identityProvider
- omnibox
- pageCapture
- platformKeys
Expand All @@ -148,4 +165,4 @@ These commonly used WebExtensions/Chrome Extension APIs are not wrapped here yet
- tabGroups
- topSites
- tts
- ttsEngine
- ttsEngine
114 changes: 114 additions & 0 deletions docs/browserDetection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# browserDetection

Best-effort browser detection for extension contexts.

Chrome does not expose a native `chrome.runtime.getBrowserInfo()` API. This helper combines available browser signals and returns where the guess came from, so callers can decide how much they trust the result.

## Methods

- [guessBrowser()](#guessBrowser)
- [isBrowser(guess, ...names)](#isBrowser)
- [isBrowserFamily(guess, family)](#isBrowserFamily)

## Enums

- `BrowserName`
- `BrowserFamily`
- `BrowserGuessSource`

---

<a name="guessBrowser"></a>

### guessBrowser

```ts
guessBrowser(): Promise<BrowserGuess>
```

Guesses the current browser using the best signal available in the current context.

```ts
import {BrowserName, guessBrowser, isBrowser} from "@addon-core/browser";

const guess = await guessBrowser();

if (isBrowser(guess, BrowserName.Firefox)) {
// Firefox-specific path
}
```

The result contains the guessed browser name, browser family, source, and optional raw metadata:

```ts
interface BrowserGuess {
name: BrowserName;
family: BrowserFamily;
source: BrowserGuessSource;
rawName?: string;
vendor?: string;
version?: string;
}
```

Detection order:

1. Firefox `runtime.getBrowserInfo()`.
2. Chromium User-Agent Client Hints via `navigator.userAgentData`.
3. Brave-specific `navigator.brave`.
4. Browser-specific globals such as Opera or Safari globals.
5. Generic Chromium User-Agent Client Hints.
6. `navigator.userAgent`.
7. Extension URL protocol from `runtime.getURL("")`.

When no signal is available, the helper returns:

```ts
{
name: BrowserName.Unknown,
family: BrowserFamily.Unknown,
source: BrowserGuessSource.Unknown
}
```

For capability checks, prefer feature detection. Use `guessBrowser()` when product logic, diagnostics, or analytics truly need a browser label.

<a name="isBrowser"></a>

### isBrowser

```ts
isBrowser(guess: BrowserGuess, ...names: BrowserName[]): boolean
```

Checks whether a browser guess matches one of the provided names.

```ts
import {BrowserName, guessBrowser, isBrowser} from "@addon-core/browser";

const guess = await guessBrowser();

if (isBrowser(guess, BrowserName.Edge, BrowserName.Chrome)) {
// Chromium browser branch for Chrome or Edge
}
```

<a name="isBrowserFamily"></a>

### isBrowserFamily

```ts
isBrowserFamily(guess: BrowserGuess, family: BrowserFamily): boolean
```

Checks whether a browser guess belongs to a browser family.

```ts
import {BrowserFamily, guessBrowser, isBrowserFamily} from "@addon-core/browser";

const guess = await guessBrowser();

if (isBrowserFamily(guess, BrowserFamily.Chromium)) {
// Chromium-family branch
}
```
170 changes: 170 additions & 0 deletions docs/identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# identity

Documentation:

- [Chrome Identity API](https://developer.chrome.com/docs/extensions/reference/api/identity)
- [Firefox identity API](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/identity)
- [Microsoft Edge extension API support](https://learn.microsoft.com/en-us/microsoft-edge/extensions/developer-guide/api-support)

A promise-based wrapper for the WebExtension `identity` API. Chrome exposes the full API surface. Firefox, Edge, and Opera should use the portable OAuth flow with `getIdentityRedirectUrl()` and `launchWebAuthFlow()`. Safari does not support this API.

## Browser support notes

- Chrome: supports the full `chrome.identity` API, except the underlying `getAccounts()` API is Chrome Dev channel only. This wrapper uses the callback form for Chrome-style runtimes because it works across Manifest V2 and Manifest V3.
- Firefox: supports the portable `browser.identity.launchWebAuthFlow()` promise API and `getRedirectURL()`.
- Edge: use `launchWebAuthFlow()` for portable OAuth. `getAuthToken()` and the underlying `getAccounts()` API are officially unsupported even if feature detection sees the methods.
- Opera: supports the portable web auth flow in Chromium-based builds.
- Safari: Identity API is not supported.

## Manifest

For Chrome OAuth token helpers, add `identity` and the `oauth2` manifest section:

```json
{
"permissions": ["identity"],
"oauth2": {
"client_id": "client-id.apps.googleusercontent.com",
"scopes": ["profile", "email"]
}
}
```

For profile email data, add `identity.email`:

```json
{
"permissions": ["identity", "identity.email"]
}
```

Interactive authorization flows should be started from a user action, such as a button click.

## Methods

- [getIdentityRedirectUrl(path?)](#getIdentityRedirectUrl)
- [launchWebAuthFlow(details)](#launchWebAuthFlow)
- [getAuthToken(details?)](#getAuthToken)
- [removeCachedAuthToken(details)](#removeCachedAuthToken)
- [clearAllCachedAuthTokens()](#clearAllCachedAuthTokens)
- [getProfileUserInfo(details?)](#getProfileUserInfo)
- [getIdentityAccounts()](#getIdentityAccounts)

## Events

- [onIdentitySignInChanged(callback)](#onIdentitySignInChanged)

---

<a name="getIdentityRedirectUrl"></a>

### getIdentityRedirectUrl

```
getIdentityRedirectUrl(path?: string): string
```

Generates the extension redirect URL for an OAuth flow.

```ts
import {getIdentityRedirectUrl} from "@addon-core/browser";

const redirectUrl = getIdentityRedirectUrl("oauth");
```

In Chromium browsers this usually returns a URL like `https://<extension-id>.chromiumapp.org/oauth`.

<a name="launchWebAuthFlow"></a>

### launchWebAuthFlow

```
launchWebAuthFlow(details: LaunchWebAuthFlowDetails): Promise<string | undefined>
```

Starts a browser-managed OAuth flow and resolves with the final redirect URL. This is the recommended portable API for Firefox, Edge, Opera, and non-Google providers.

```ts
import {getIdentityRedirectUrl, launchWebAuthFlow} from "@addon-core/browser";

const redirectUrl = getIdentityRedirectUrl("oauth");
const url = new URL("https://accounts.example.com/oauth/authorize");
url.searchParams.set("redirect_uri", redirectUrl);

const responseUrl = await launchWebAuthFlow({
interactive: true,
url: url.toString(),
});
```

This wrapper uses the callback form in Chrome-style runtimes to avoid callbackless Manifest V2 flows hanging, and the Promise form in Firefox where callbacks are not accepted.

`LaunchWebAuthFlowDetails` also accepts `redirect_uri` for Firefox. This option is Firefox-only, supported since Firefox 63; loopback redirect URIs are supported since Firefox 86.

<a name="getAuthToken"></a>

### getAuthToken

```
getAuthToken(details?: chrome.identity.TokenDetails): Promise<chrome.identity.GetAuthTokenResult>
```

Gets a Chrome OAuth2 access token using the manifest `oauth2` configuration or the provided scopes. The wrapper always resolves to `{ token, grantedScopes }`, including callback-based Chrome runtimes that return those values as separate callback arguments.

```ts
import {getAuthToken} from "@addon-core/browser";

const {token} = await getAuthToken({interactive: true});
```

This method is Chrome-focused. In Edge, it is officially unsupported even if present.

<a name="removeCachedAuthToken"></a>

### removeCachedAuthToken

```
removeCachedAuthToken(details: chrome.identity.InvalidTokenDetails): Promise<void>
```

Removes an OAuth2 access token from Chrome's token cache.

<a name="clearAllCachedAuthTokens"></a>

### clearAllCachedAuthTokens

```
clearAllCachedAuthTokens(): Promise<void>
```

Clears all cached auth tokens and authorization state managed by the Identity API.

<a name="getProfileUserInfo"></a>

### getProfileUserInfo

```
getProfileUserInfo(details?: chrome.identity.ProfileDetails): Promise<chrome.identity.ProfileUserInfo>
```

Returns profile email and ID information. Requires the `identity.email` permission; otherwise Chrome returns empty fields.

<a name="getIdentityAccounts"></a>

### getIdentityAccounts

```
getIdentityAccounts(): Promise<chrome.identity.AccountInfo[]>
```

Returns accounts present in the Chrome profile. This API is Chrome Dev channel only and should not be used for stable product logic.

<a name="onIdentitySignInChanged"></a>

### onIdentitySignInChanged

```
onIdentitySignInChanged(callback: (account: chrome.identity.AccountInfo, signedIn: boolean) => void): () => void
```

Fires when the sign-in state changes for an account in the user's profile. Returns an unsubscribe function.
Loading
Loading