openvibers/openvibe.extensions README.md
README.md
| 1 | # OpenVibe.Extensions |
| 2 | |
| 3 | > OpenVibe's first-party client surfaces: today, the **OpenVibe Companion** browser extension. |
| 4 | |
| 5 | **Docs:** https://openvibe.services · **For AI:** https://openvibe.network/llms.txt |
| 6 | **Build and test:** `npm install && npm run pack` (Node 22 or newer) · **Tests:** `npm test` |
| 7 | |
| 8 | **Status:** alpha (roadmap Wave 18, client part). The extension builds, its tests pass, and headless |
| 9 | Chrome loads the unpacked 0.9.2 build with no console or CSP errors (2026-11-07, Chrome 156 via |
| 10 | CDP `Extensions.loadUnpacked`; the side panel's Agents tab draws the Dock's agent view against stubbed first parties, see the credential note below). A development build of |
| 11 | the coupon part looked up codes and sent a report against a local OpenVibe.Coupons. Mozilla's |
| 12 | `web-ext lint` (what addons.mozilla.org runs on upload) passes with no errors, warnings or notices |
| 13 | and runs in CI; it hasn't been loaded in a running Firefox yet. It is **not published** to any |
| 14 | store yet: the store zips, listing text and privacy answers are ready (`npm run pack:stores`, |
| 15 | [store/](store/)), and only the owner's developer-account step is left. OpenVibe.Coupons is |
| 16 | public at `https://openvibe.coupons` since 2026-10-09, so the release build talks to the live |
| 17 | Coupons API. On 2026-10-22 Coupons held 169 codes for 16 of its 37 merchants, all from the DealNews feed through |
| 18 | OpenVibe.Sources; it had issued no install token and received no code report. |
| 19 | **Decision record:** [ADR-023](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-023-client-surfaces.md) (first-party client surfaces and their home). Roadmap §4.2 D. |
| 20 | **License:** MIT. |
| 21 | |
| 22 | ## Purpose |
| 23 | |
| 24 | Client code ships on a different schedule from servers: store review, browser updates and |
| 25 | per-install permissions. ADR-023 gives clients their own repository. |
| 26 | |
| 27 | | Surface | Directory | State | |
| 28 | |---|---|---| |
| 29 | | OpenVibe Companion (Chromium and Firefox, Manifest V3) | `browser/` | alpha, described below (it was the OpenVibe Coupons Helper until 0.2.0) | |
| 30 | | Kiosk companion (Firefox, new-tab and history bridge for the /kiosk page) | `kiosk-companion/` | Moved from OpenVibe.Live `browser-extension/openvibe-kiosk-companion/` (ADR-023, Live 9c5aa64); Live keeps a redirect README | |
| 31 | | Hardware / Raspberry Pi companions (streamer and controller scripts) | `hardware/` | Moved from OpenVibe.Live `hardware/` (ADR-023, Live 9c5aa64); Live keeps a redirect README | |
| 32 | | Desktop client | — | Planned by ADR-023 for the long term. Nothing exists. | |
| 33 | |
| 34 | ## The OpenVibe Companion (`browser/`) |
| 35 | |
| 36 | One button in the browser for the OpenVibe sites that work with the page you are on: |
| 37 | |
| 38 | - **Keep on my Rent board.** On a rental listing (Craigslist, Facebook Marketplace, Zillow, |
| 39 | Apartments.com, Zumper, Airbnb and the rest) or any page, it opens |
| 40 | [openvibe.rent/board](https://openvibe.rent/board) with the listing's address and title filled in, |
| 41 | and the price and city the page states (`lib/listing.js` reads them when you press: the structured data |
| 42 | the site publishes for search engines, else its price tag), so every listing from every site sits in one |
| 43 | list with your notes and how far you got. You check them there before saving. On a rental site this is |
| 44 | the main button. |
| 45 | - **Check the rent.** On a rental listing it opens [openvibe.rent/fair](https://openvibe.rent/fair) with the listing's |
| 46 | address and title, and the monthly rent in dollars, the bedrooms and the city the page states (read the same way; |
| 47 | a sale price or another currency is left out). Rent reads what is missing from the address and title (and says |
| 48 | which it read), and compares the rent with the city's median for that size from the Census Bureau. |
| 49 | - **Watch this page.** It opens a new watch on [OpenVibe.Watch](https://openvibe.watch) for the page, |
| 50 | which tells you (or runs your agent) when it changes. |
| 51 | - **Compare delivery apps.** On DoorDash, Uber Eats, Grubhub, Seamless, Caviar or Instacart it opens |
| 52 | [openvibe.food/delivery](https://openvibe.food/delivery) with the restaurant or store's name (read from the page |
| 53 | title, never the address or the cart), and every app's own search for it opens from there. |
| 54 | - **Save its rating to OpenVibe Reviews.** On Glassdoor, Indeed, Yelp, the BBB, Trustpilot, Google Maps or |
| 55 | Tripadvisor, it reads the rating the page states (the structured data the site publishes for search engines, else |
| 56 | the stars' own label, like "4.5 star rating") and opens [openvibe.reviews/save](https://openvibe.reviews/save) with |
| 57 | the page's address, its name and the numbers. You check them against the page there and save; the rating then shows |
| 58 | beside that site's link on the OpenVibe.Reviews page, with the date. On a review site this is the main button. |
| 59 | - **Try these codes on this page.** At a shop's cart or checkout, one button types the codes OpenVibe.Coupons knows |
| 60 | into the page's own coupon box, presses that box's Apply button, and reads the order total before and after. It |
| 61 | stops at the first code that lowers the total and leaves it applied. Codes reported working go first, codes |
| 62 | reported not working are never tried, and at most ten are tried. It never presses checkout, pay, order, cart or |
| 63 | remove buttons, never submits a form and never presses Enter. With no Apply button next to the box it presses |
| 64 | nothing. If you connected the extension, you can send what happened to OpenVibe.Coupons in one click. |
| 65 | - **Coupon codes.** On a shop's website it shows the codes |
| 66 | [OpenVibe.Coupons](https://github.com/OpenVibers/OpenVibe.Coupons) knows for that shop: |
| 67 | |
| 68 | - the code, with a Copy button |
| 69 | - its status (validity unknown, reported working, reported not working) |
| 70 | - its confidence, or "no reports", and its expiry, or "Expiry unknown" |
| 71 | - its restrictions, or "No restrictions stated" |
| 72 | - how to apply it |
| 73 | |
| 74 | If you connected the extension, you can report whether a code worked. The codes are shown to copy; |
| 75 | the Companion never rewrites links or adds referral tags. |
| 76 | |
| 77 | ### What it can see, and what it sends |
| 78 | |
| 79 | | | | |
| 80 | |---|---| |
| 81 | | **Permissions** | `activeTab`, `identity`, `scripting`, `storage`, `alarms`, `clipboardWrite`, and Chrome-only `offscreen` and `sidePanel` in the Chrome build (Firefox's `sidebar_action` needs no permission of its own). The host permissions are five OpenVibe origins: `https://openvibe.coupons/*` (the coupon API), `https://openvibe.network/*` (the Dock's state), `https://openvibe.codes/*` (the Dock's agent view and its four controls) and `https://openvibe.chat/*` (the Dock's Chats tab), plus `https://openvibe.locker/*` (encrypted password logins). `identity` starts the Dock's one-click OAuth sign-in (`chrome.identity.launchWebAuthFlow` / Firefox's `browser.identity`). `scripting` is used for four buttons: "Try these codes on this page" injects the packaged `lib/autoapply.js`, "Save its rating to OpenVibe Reviews" injects `lib/rating.js`, and "Keep on my Rent board" and "Check the rent" inject `lib/listing.js`, into the active tab (the access `activeTab` gives after you press the button), in its top frame and the extension's isolated world. | |
| 82 | | **Not requested** | `<all_urls>`, `tabs`, `cookies`, `webRequest`, `history`, optional permissions, content scripts, web-accessible resources, `externally_connectable`. | |
| 83 | | **Read from the page** | The active tab's **URL and title**, when you open the popup (that's what `activeTab` grants). The page's content only when you press "Try these codes on this page": `lib/autoapply.js` finds the coupon box, its Apply button and the order total, types the codes in, and returns to the popup which codes changed the total. It makes no request, reads no cookies or storage, and sends nothing anywhere; `test/manifest.test.js` scans it for that. On a review site, "Save its rating to OpenVibe Reviews" runs `lib/rating.js`, which only reads: the page's name, its rating, the scale, how many ratings, and the city from its structured data. "Keep on my Rent board" and "Check the rent" run `lib/listing.js`, which only reads: the price and its currency, the city and region (never the street) and the bedrooms, from the structured data, else the page's price tag. Browser pages, files, IP addresses and local names are "not a site", and nothing is sent or offered for them. | |
| 84 | | **Sent by the extension** | The Coupons part sends to `https://openvibe.coupons` only, and only the **hostname** (`lib/host.js` drops the path, query, fragment, port and credentials first): `GET /api/v1/merchants/resolve?host=`, the merchant id (`GET /api/v1/merchants/:id/coupons`), and, when you report, the code id plus `worked` or `failed` with an optional reason. Every request goes with `credentials: 'omit'` (no cookies), `referrerPolicy: 'no-referrer'` and `redirect: 'error'`. The Dock sends the person's own bearer to `https://openvibe.network` (its state, `/api/v1/me/dock`), `https://openvibe.chat` (Chats), and, when the Agents tab needs it, `https://openvibe.codes` (the agent view and its controls). | |
| 85 | | **Hand-offs (only when you press the button)** | "Keep on my Rent board", "Check the rent", "Watch this page", "Compare delivery apps" and "Save its rating to OpenVibe Reviews" open **a new tab you see** on `https://openvibe.rent/board`, `https://openvibe.rent/fair`, `https://openvibe.watch/watches/new`, `https://openvibe.food/delivery` or `https://openvibe.reviews/save`, with the page's address and title (and, for Reviews, the rating read from it; for Rent, the price, city and bedrooms read from it) in that tab's address. `lib/handoff.js` removes the credentials, the fragment and tracking parameters (`utm_*`, `fbclid`, `gclid`, …) first. The extension makes no request for it, nothing opens on its own, and nothing is saved until you save it on that site. `platform.tabs.open()` refuses any other origin, and the permission audit allows only `https://openvibe.<tld>` hand-off origins. | |
| 86 | | **Stored** | The optional `cpx_` install token in `storage.local` (never synced); for the Dock, the one-click OAuth access token in `storage.session` (memory, cleared when the browser closes) and the rotating refresh token in `storage.local` (this device, not synced). A pasted 0.7.0 `networkToken` is still accepted as a fallback until a later release removes it. | |
| 87 | | **CSP** | `default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob:; connect-src https://openvibe.coupons https://openvibe.network https://openvibe.chat https://openvibe.codes; frame-src https://openvibe.live; object-src 'none'; base-uri 'none'`. The Dock draws its own styles in a `<style>` element (styles only; script stays `'self'`), the agent view shows its still from a local `blob:`, and its Live embed is the only frame. There is no inline script, no remote code, no `eval`, and no `innerHTML` in the extension's own code: the popup builds DOM nodes and sets text. | |
| 88 | |
| 89 | ### The credential: an install token, never a session |
| 90 | |
| 91 | The extension works without any credential, with the API's tighter anonymous rate limits. To |
| 92 | report: |
| 93 | |
| 94 | 1. On [openvibe.coupons/connect-extension](https://openvibe.coupons/connect-extension), sign in and |
| 95 | create a token for this browser. It is shown once. |
| 96 | 2. Paste it into the extension's settings. The extension checks that the API accepts it before |
| 97 | saving it. |
| 98 | |
| 99 | A `cpx_` token has two scopes, `coupons.lookup` and optionally `coupons.report`. It can't submit |
| 100 | codes, moderate, or read anything private, and it isn't an OpenVibe Network session. The extension |
| 101 | refuses to store anything that isn't a `cpx_` token, so a Network JWT can't be pasted in by |
| 102 | mistake. Revoking the token on the site takes effect on its next request, and the popup then says |
| 103 | the browser was disconnected. "Disconnect" in the settings removes the token from the browser. |
| 104 | |
| 105 | Because the extension exposes nothing to web pages (no content scripts, no |
| 106 | `externally_connectable`, no web-accessible resources) and holds no first-party credential, a |
| 107 | malicious shop page can't use it to reach anyone's data. The server side of the same guarantee is |
| 108 | covered in Coupons' own tests: lookups are identical for every caller, and the API ignores cookies. |
| 109 | |
| 110 | ### The Dock in the side panel |
| 111 | |
| 112 | The **OpenVibe Dock** — the pill, the card with its Vibe, Agents, Chats and Activity tabs — sits in |
| 113 | the browser's side panel (Chrome's `side_panel`, Firefox's `sidebar_action`), on every site. It is the |
| 114 | released OpenVibe.Shared Dock 4.212.0, bundled under `browser/shared/` because the extension's CSP allows no |
| 115 | remote script, and it reads and writes the same state on the Network as the Dock on a site, so it |
| 116 | opens where the person left off. It hands that state to the Dock as it opens (the page's tab, chat, agent and |
| 117 | unread counts) and re-reads it every 30 s, so a change on the page reaches the open side panel. |
| 118 | |
| 119 | The Agents tab draws the person's own desk stream the same way a site's Dock does: the Live embed, the |
| 120 | task line, and the owner's **Pause/Resume**, **Curtain now**, **Mute/Unmute** and **End** controls. The |
| 121 | view is read with the person's token from OpenVibe.Codes (`GET /api/v1/me/agent-view`); the extension |
| 122 | allows Codes and Live's embed for exactly that. Codes accepts the Dock's read-only OAuth token (issuer |
| 123 | `<network>/dock`), so the panel shows the stream; its four server controls are links to openvibe.codes, |
| 124 | where the person's own session does the same thing. The fixed Chrome id is in Live's |
| 125 | `AGENT_EMBED_EXTENSION_ORIGINS`, and a browser that cannot frame the embed gets the still and a |
| 126 | **Watch on openvibe.live** link instead. `scripts/check-panel.js` proves both in headless Chrome. |
| 127 | |
| 128 | It needs the person's own OpenVibe sign-in. Press **Sign in** in the panel or the settings page: the |
| 129 | Network's account chooser opens, the extension is a public OAuth client (`companion`, PKCE S256, no |
| 130 | secret), and the Dock gets a read-only token (`dock`, `activity`, `chat.read`, `agent.view`) that refreshes itself. |
| 131 | The access token is kept in `storage.session`; the rotating refresh token in `storage.local`, on this |
| 132 | device. Sign out revokes it at the Network, and the account page (`/my`) lists it under **Signed-in |
| 133 | apps**. The 0.7.0 pasted-token path is still in the settings page as a fallback until a later release |
| 134 | removes it. |
| 135 | |
| 136 | ### Layout |
| 137 | |
| 138 | ``` |
| 139 | browser/ |
| 140 | manifest.json MV3; Chromium (minimum_chrome_version 114) and Firefox (gecko id, strict_min_version 128) |
| 141 | panel.html / panel.js the Dock in the side panel: the Network's state, the same tabs as the Dock on a site |
| 142 | panel.css the Dock fills the panel (no floating card, no pill) |
| 143 | shared/ the released OpenVibe.Shared Dock 4.212.0, bundled: the CSP allows no remote script |
| 144 | (dock.js, dock-*.js, action-center.js, notification-*.js, agent-sheet.js, |
| 145 | agent-session.js, ov-window.js, ov-float.js, ov-composer.js, ov-glyphs.js, |
| 146 | brands.js, theme-loader.js, ov-mark.js, kit.css) |
| 147 | popup.html / popup.js DOM wiring only |
| 148 | options.html / .js connect (save a checked token), disconnect, grant the API host permission |
| 149 | lib/config.js API_ORIGIN (Coupons), NETWORK_ORIGIN and CHAT_ORIGIN (the Dock), CODES_ORIGIN (its agent view); HANDOFF_ORIGINS (Rent, Watch, Food, Reviews) |
| 150 | lib/host.js URL → hostname (everything else dropped) |
| 151 | lib/handoff.js the Rent board, Watch, Food delivery and Reviews addresses, built from a cleaned URL; which sites are rentals and review sites |
| 152 | lib/autoapply.js "Try these codes on this page": injected into the active tab only when you press it |
| 153 | lib/rating.js "Save its rating to OpenVibe Reviews": reads a review site's rating, injected only when you press it |
| 154 | lib/listing.js "Keep on my Rent board" / "Check the rent": reads a listing's price, city and bedrooms, injected only when you press |
| 155 | lib/api.js the only code that makes requests to the Coupons API |
| 156 | lib/network-api.js the Dock's state on the Network (GET/PUT /api/v1/me/dock), with the person's own token |
| 157 | lib/auth.js the Dock's one-click OAuth sign-in: PKCE, state, refresh, sign-out |
| 158 | lib/controller.js popup logic without the DOM (tested in Link) |
| 159 | lib/platform.js chrome.* / browser.*: tabs.query, tabs.create (hand-off pages only), storage.local, permissions |
| 160 | lib/format.js wording ("Expiry unknown", "No restrictions stated", …) |
| 161 | icons/ generated by scripts/icons.js |
| 162 | scripts/ |
| 163 | audit.js the permission audit (manifest, CSP, pages) |
| 164 | pack.js zip for loading, or --stores: the Chrome and Firefox store zips (runs the audit first) |
| 165 | check-autoapply.js lib/autoapply.js on test/fixtures/checkout-*.html in headless Chrome (CDP, no dependencies) |
| 166 | check-panel.js the unpacked extension in headless Chrome: the panel's Agents tab draws the agent view (CDP, no dependencies) |
| 167 | icons.js draws the icons (no dependencies) |
| 168 | ``` |
| 169 | |
| 170 | ## Tests (Link 22, no browser, no dependencies) |
| 171 | |
| 172 | ```bash |
| 173 | npm test |
| 174 | ``` |
| 175 | |
| 176 | | Requirement | Test | |
| 177 | |---|---| |
| 178 | | The manifest permission audit fails if anything widens: extra permissions, `<all_urls>` or broader hosts, content scripts, web-accessible resources, `externally_connectable`, optional permissions, `unsafe-eval`, remote script or connect sources, any style source beyond `'unsafe-inline'`, any image beyond `'self' blob:`, any frame beyond Live, MV2. The first-party origins agree across config, host permissions and CSP. Pages have no inline or remote script. The code never uses cookies, scripting, dynamic code, HTML injection or `storage.sync`, and only `lib/api.js`, `lib/network-api.js`, `lib/auth.js` and the bundled Dock fetch. | `test/manifest.test.js` | |
| 179 | | The side panel in a real headless Chrome: the unpacked extension opens `panel.html` on the Agents tab with a stubbed Network/Chat/Codes, and the agent view draws its stream frame, line, meta and Pause/Resume, Curtain now, Mute/Unmute and End, with its styles applied and no CSP refusal. Skipped when no Chrome is installed. | `test/panel.test.js` (`scripts/check-panel.js`) | |
| 180 | | API client against a stub: routes; no `Authorization` without a token; never cookies, referrers or redirects; errors as codes (revoked, scope, 429, 503); timeouts; refused input. | `test/api-client.test.js` | |
| 181 | | The Coupons part never sends page content or URLs beyond the hostname. The popup controller runs on checkout URLs full of personal data, and every request the stub receives is inspected. Non-shop pages send nothing. Only `tabs.query` and `storage.local` are touched, and only `cpx_` tokens are stored there. | `test/privacy.test.js` | |
| 182 | | The hand-offs: nothing opens when the popup opens; a button opens one tab on openvibe.rent or openvibe.watch with the cleaned address (no credentials, fragment or tracking parameters); the Coupons API still gets the hostname only; `tabs.open` refuses other origins; rental sites lead with the board. | `test/handoff.test.js` | |
| 183 | | `scripts/pack.js`: the zip is exactly `browser/`, audited and reproducible. A development build rewrites the origin in all three places. Remote `http` origins are refused. | `test/pack.test.js` | |
| 184 | |
| 185 | Beyond the automated tests, the coupon flow was checked by hand on 2026-09-22: |
| 186 | |
| 187 | - Headless Chrome 150, loaded through CDP `Extensions.loadUnpacked`, accepted the manifest and |
| 188 | rendered both pages with no console errors or CSP violations. |
| 189 | - A development build then called a local OpenVibe.Coupons (its test harness) with no CORS |
| 190 | configured. It resolved a shop, listed its code, and posted a report with a `cpx_` token. The |
| 191 | report was stored with channel `extension`. |
| 192 | |
| 193 | On 2026-11-07 the same CDP load ran the side panel in headless Chrome 156 (now `test/panel.test.js` |
| 194 | and `scripts/check-panel.js`): with a stubbed Network, Chat and Codes, `panel.html` opened on the |
| 195 | Agents tab, drew the agent view's Live frame, task line and `Paused by @ada` meta, and showed |
| 196 | Pause/Resume, Curtain now, Mute/Unmute and End, with the Dock's styles applied and no CSP refusal. |
| 197 | The 2026-09-22 coupon flow is not in CI; the panel check is, and it skips when no Chrome is installed. |
| 198 | Firefox hasn't been tried. |
| 199 | |
| 200 | ## Packing (never published from here) |
| 201 | |
| 202 | ```bash |
| 203 | npm run pack # dist/openvibe-companion-<version>.zip (API https://openvibe.coupons) |
| 204 | npm run pack:stores # dist/openvibe-companion-<version>-chrome.zip and -firefox.zip (store uploads) |
| 205 | npm run lint:firefox # Mozilla web-ext lint, warnings as errors (CI runs it) |
| 206 | node scripts/pack.js --api http://localhost:4850 # dist/…-dev.zip for a local Coupons (localhost/127.0.0.1 or https only) |
| 207 | ``` |
| 208 | |
| 209 | Each version's zip is attached to its [GitHub release](https://github.com/OpenVibers/OpenVibe.Extensions/releases/latest). |
| 210 | To load it: |
| 211 | |
| 212 | - **Chromium:** `chrome://extensions`, turn on Developer mode, choose "Load unpacked" and pick the |
| 213 | unzipped folder. |
| 214 | - **Firefox:** `about:debugging`, "This Firefox", "Load Temporary Add-on", and pick |
| 215 | `manifest.json`. |
| 216 | |
| 217 | In Firefox, host permissions for MV3 extensions can be withheld. If they are, the settings page |
| 218 | shows "Allow access to openvibe.coupons". |
| 219 | |
| 220 | ## Before a store release (for the lead) |
| 221 | |
| 222 | 1. **OpenVibe.Coupons is already public** (see its README; live at `https://openvibe.coupons` since |
| 223 | 2026-10-09). The release build talks to `https://openvibe.coupons` for codes and to |
| 224 | `https://openvibe.network`, `https://openvibe.chat` and `https://openvibe.codes` for the Dock, |
| 225 | each named in the manifest and the CSP and nothing else. |
| 226 | 2. **Store listings:** `npm run pack:stores` builds `dist/openvibe-companion-<version>-chrome.zip` |
| 227 | (no `browser_specific_settings`) and `-firefox.zip` (no `minimum_chrome_version`), byte for byte |
| 228 | reproducible. Upload them with the fields in [store/listing.md](store/listing.md), the single |
| 229 | purpose, permission justifications and data disclosures in [store/privacy.md](store/privacy.md) |
| 230 | (written from the code; `browser_specific_settings.gecko.data_collection_permissions` declares |
| 231 | `authenticationInfo`, `browsingActivity` and `websiteContent` to match), and the shots in |
| 232 | [store/screenshots.md](store/screenshots.md). Never upload from CI. |
| 233 | 3. **CORS (optional):** set `COUPONS_EXTENSION_ORIGINS` on Coupons to the Chrome Web Store id |
| 234 | (`chrome-extension://<id>`) and, if wanted, `moz-extension://*`. The extension doesn't need it, |
| 235 | because its host permission covers the API. CORS only helps when a browser withholds that |
| 236 | permission, and only for lookups. |
| 237 | 4. **Version:** bump `browser/manifest.json` for every store upload. Stores refuse a version |
| 238 | they've already seen. |
| 239 | |
| 240 | ## Depends on |
| 241 | |
| 242 | - [OpenVibe.Rent](https://github.com/OpenVibers/OpenVibe.Rent) `/board?url=&title=` and |
| 243 | [OpenVibe.Watch](https://github.com/OpenVibers/OpenVibe.Watch) `/watches/new?template=page&url=&name=` |
| 244 | (pages, not APIs: the person signs in there as usual). |
| 245 | - [OpenVibe.Coupons](https://github.com/OpenVibers/OpenVibe.Coupons) public API: |
| 246 | - `GET /api/v1/merchants/resolve?host=` |
| 247 | - `GET /api/v1/merchants/:id/coupons` |
| 248 | - `POST /api/v1/coupons/:id/report` |
| 249 | - `cpx_` install tokens from `/connect-extension` |
| 250 | - [OpenVibe.Codes](https://github.com/OpenVibers/OpenVibe.Codes) `/api/v1/me/agent-view` (the |
| 251 | Dock's agent view: the state, the still, and the pause, curtain, mute and end controls). Its |
| 252 | session check does not accept the Dock's `<network>/dock`-issued OAuth token yet, so the view is |
| 253 | bundled and verified against a stub but stays hidden in production until Codes or Network accepts |
| 254 | that credential. |
| 255 | - [OpenVibe.Live](https://github.com/OpenVibers/OpenVibe.Live) `/embed/agent/<ags_id>?vt=…`, the |
| 256 | agent view's frame. Its `frame-ancestors` list is first-party site origins; an extension origin is |
| 257 | not on it yet. |
| 258 | - No npm dependencies. Tests and scripts use Link's standard library. |
| 259 | |
| 260 | --- |
| 261 | |
| 262 | Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers). |
| 263 | |
| 264 | ## Locker passwords |
| 265 | |
| 266 | The popup unlocks Locker with its passphrase or recovery kit. A passkey belongs to |
| 267 | openvibe.locker, so the popup explains this and offers **Open Locker**. Unlock keys |
| 268 | live only in `storage.session` (or background memory where unavailable), never local |
| 269 | or sync storage. Lock now, account changes, browser restart and 15 idle minutes |
| 270 | clear them. Worker suspension does not reset the deadline. |
| 271 | |
| 272 | Saved logins match the current origin exactly, including scheme, hostname and port. |
| 273 | **Fill** writes visible username/password inputs in the active tab's top frame; |
| 274 | **Fill and sign in** also submits. Neither runs on page load. Ambiguous forms with |
| 275 | more than one password input are refused. The injected function checks the origin |
| 276 | again immediately before writing each input. Copy password/code clears the clipboard |
| 277 | 30 seconds after the latest copy, even when the popup closes; it does not read the |
| 278 | clipboard and may clear something copied elsewhere during those 30 seconds. |
| 279 | |
| 280 | **Save this login** encrypts `{version:1,site,username,password,totp,notes}` using the |
| 281 | same SDK envelope and private payload framing as Locker. Site and login details |
| 282 | stay encrypted on the server. TOTP codes (including otpauth links) are computed |
| 283 | locally. No password telemetry or breach lookup runs in Companion. |
| 284 | |
| 285 | Deployment requires Network's `locker.login.manage` scope and |
| 286 | `GET /api/v1/me/keys/companion`, then Locker's `/api/v1/companion` routes. Existing |
| 287 | OAuth grants need **Sign in to Companion** again to grant the new scope. The scope |
| 288 | allows only own wrapped-key reads and own encrypted-login reads/creates, with live |
| 289 | Network revocation checks. It grants no key-write, general-file or agent blind-fill |
| 290 | capability. Chrome uses an offscreen clipboard document; Firefox uses its |
| 291 | background event page with an alarm to clear even after suspension. Only the Locker host permission is added. |
| 292 | |
| 293 | Bundled crypto: `browser/shared/ov-locker-core.js` is the unchanged SDK 0.101.0 |
| 294 | browser bundle from Shared 4.156.0. `locker-payload.js` and `locker-passwords.js` |
| 295 | come from Locker commit `9c2744e`; keep their framing/TOTP semantics compatible. |
| 296 | Run `npm test -- locker`, `npm run pack:stores`, and `npm run lint:firefox`. |
| 297 | |
| 298 | Optional browser verification: install Playwright outside the package (or set |
| 299 | `PLAYWRIGHT_MODULE` to an existing installation), install its Chromium build, and run |
| 300 | `node test/locker-browser.js`. It checks trusted popup clicks, 12 responsive/theme |
| 301 | screenshots and actual clipboard clearing with the packaged worker and no popup open. |
| 302 | |
| 303 | Firefox MV3 background pages are [non-persistent](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts), so a clipboard alarm backs up its in-memory timer. |
| 304 | Chrome uses the documented [offscreen clipboard context](https://developer.chrome.com/docs/extensions/reference/api/offscreen). |
| 305 |