openvibers/openvibe.extensions Commits

The Companion is ready for the Chrome Web Store and addons.mozilla.org: store listing and privacy answers in store/, reproducible Chrome and Firefox store zips (npm run pack:stores), Mozilla's web-ext lint in CI, data disclosures that match the code, and a current STATUS and README (#7)

OP OpenVibers committed 22 days ago · d77a4fcc21 · 10 files, +197 −20

Changes

ddiff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
iindex 5300f33..47d605f 100644
---- a/.github/workflows/ci.yml
++++ b/.github/workflows/ci.yml
@@@ -12,7 +12,16 @@ jobs:
cache: false # and so no lockfile
syntax-dirs: scripts
pin-drift: false # no openvibe-* pins
-- # The coupon helper's zip, kept 14 days for review.
++ # Mozilla's own linter (what addons.mozilla.org runs on upload); a warning fails the build.
++ firefox-lint:
++ runs-on: ubuntu-latest
++ steps:
++ - uses: actions/checkout@v4
++ - uses: actions/setup-node@v4
++ with:
++ node-version: 22.22.1
++ - run: npm run lint:firefox
++ # The release zip and the two store zips, kept 14 days for review.
package:
runs-on: ubuntu-latest
steps:
@@@ -21,6 +30,7 @@ jobs:
with:
node-version: 22.22.1
- run: npm run pack
++ - run: npm run pack:stores
- uses: actions/upload-artifact@v4
with:
name: openvibe-coupons-helper
ddiff --git a/README.md b/README.md
iindex 4436ee5..f4ae3ed 100644
---- a/README.md
++++ b/README.md
@@@ -5,10 +5,12 @@
**Status:** alpha (roadmap Wave 18, client part). The extension builds, its tests pass, and Chromium
loads the unpacked 0.3.0 build with no console or CSP errors (2026-10-15, Playwright's Chromium: branded Chrome no
longer loads unpacked extensions from the command line). A development build of
--the coupon part looked up codes and sent a report against a local OpenVibe.Coupons. It is **not
--published** to any store (install it from a release zip, below), and it hasn't been verified in
--Firefox yet. OpenVibe.Coupons runs on the production host with an empty database, and
--`https://openvibe.coupons` is public since 2026-10-09, so the release build talks to the live
++the coupon part looked up codes and sent a report against a local OpenVibe.Coupons. Mozilla's
++`web-ext lint` (what addons.mozilla.org runs on upload) passes with no errors, warnings or notices
++and runs in CI; it hasn't been loaded in a running Firefox yet. It is **not published** to any
++store yet: the store zips, listing text and privacy answers are ready (`npm run pack:stores`,
++[store/](store/)), and only the owner's developer-account step is left. OpenVibe.Coupons is
++public at `https://openvibe.coupons` since 2026-10-09, so the release build talks to the live
Coupons API.
**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.
**License:** MIT.
@@@ -117,7 +119,7 @@ browser/
icons/ generated by scripts/icons.js
scripts/
audit.js the permission audit (manifest, CSP, pages)
-- pack.js zip for loading or store review (runs the audit first)
++ pack.js zip for loading, or --stores: the Chrome and Firefox store zips (runs the audit first)
check-autoapply.js lib/autoapply.js on test/fixtures/checkout-*.html in headless Chrome (CDP, no dependencies)
icons.js draws the icons (no dependencies)
```
@@@ -150,6 +152,8 @@ That check is not in CI. Firefox hasn't been tried.
```bash
npm run pack # dist/openvibe-companion-<version>.zip (API https://openvibe.coupons)
++npm run pack:stores # dist/openvibe-companion-<version>-chrome.zip and -firefox.zip (store uploads)
++npm run lint:firefox # Mozilla web-ext lint, warnings as errors (CI runs it)
node scripts/pack.js --api http://localhost:4850 # dist/…-dev.zip for a local Coupons (localhost/127.0.0.1 or https only)
```
@@@ -168,10 +172,13 @@ shows "Allow access to openvibe.coupons".
1. **OpenVibe.Coupons is already public** (see its README; live at `https://openvibe.coupons` since
2026-10-09). The release build only talks to `https://openvibe.coupons`.
--2. **Store listings:** submit the zip to the Chrome Web Store and addons.mozilla.org. The privacy
-- disclosure is the table above: the hostname of the active tab, only on open, sent to
-- openvibe.coupons; the address and title only in a hand-off tab the person opens. `browser_specific_settings.gecko.data_collection_permissions` declares
-- `browsingActivity`. Check it against AMO's requirements at submission time.
++2. **Store listings:** `npm run pack:stores` builds `dist/openvibe-companion-<version>-chrome.zip`
++ (no `browser_specific_settings`) and `-firefox.zip` (no `minimum_chrome_version`), byte for byte
++ reproducible. Upload them with the fields in [store/listing.md](store/listing.md), the single
++ purpose, permission justifications and data disclosures in [store/privacy.md](store/privacy.md)
++ (written from the code; `browser_specific_settings.gecko.data_collection_permissions` declares
++ `authenticationInfo`, `browsingActivity` and `websiteContent` to match), and the shots in
++ [store/screenshots.md](store/screenshots.md). Never upload from CI.
3. **CORS (optional):** set `COUPONS_EXTENSION_ORIGINS` on Coupons to the Chrome Web Store id
(`chrome-extension://<id>`) and, if wanted, `moz-extension://*`. The extension doesn't need it,
because its host permission covers the API. CORS only helps when a browser withholds that
ddiff --git a/STATUS.json b/STATUS.json
iindex 6791d7f..371d90a 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -26,21 +26,23 @@
"credential": "optional cpx_ install token (coupons.lookup, coupons.report); no Network session",
"publishedToStores": false,
"loadedInChrome": "yes: headless Chromium loads the 0.3.0 build (popup and options render, no console errors); lib/autoapply.js passes three fixture checkout pages in headless Chrome",
-- "loadedInFirefox": "not yet verified",
++ "loadedInFirefox": "not yet loaded in a browser; Mozilla's web-ext lint (8.10.0, what AMO runs on upload) passes with 0 errors, 0 warnings and 0 notices, and runs in CI with warnings as errors",
"handoffs": [
"https://openvibe.rent/board (keep a listing)",
"https://openvibe.rent/fair (check the rent)",
"https://openvibe.watch/watches/new (watch a page)",
"https://openvibe.food/delivery (compare delivery apps)",
"https://openvibe.reviews/save (a review site's rating, checked and saved on Reviews)"
-- ]
++ ],
++ "storeBuilds": "npm run pack:stores: dist/openvibe-companion-<version>-chrome.zip and -firefox.zip, reproducible, audited",
++ "storeListing": "store/ (listing, privacy and permission answers, screenshot plan); support URL https://openvibe.coupons/companion"
}
},
"pending": [
"OpenVibe.Coupons launched publicly on 2026-10-09 (https://openvibe.coupons); the database is still empty",
"move Live's browser-extension/openvibe-kiosk-companion and hardware/ here (ADR-023), with a redirect README in Live",
-- "store listings (Chrome Web Store, addons.mozilla.org) and the store extension ids in COUPONS_EXTENSION_ORIGINS"
++ "owner: create the Chrome Web Store and AMO developer listings and upload the store zips with store/'s answers and screenshots; then put the Chrome extension id in COUPONS_EXTENSION_ORIGINS"
],
"deployed": false,
-- "updated": "2026-09-28"
++ "updated": "2026-10-22"
}
ddiff --git a/browser/manifest.json b/browser/manifest.json
iindex 4c269ce..011d95f 100644
---- a/browser/manifest.json
++++ b/browser/manifest.json
@@@ -42,7 +42,9 @@
"strict_min_version": "128.0",
"data_collection_permissions": {
"required": [
-- "browsingActivity"
++ "authenticationInfo",
++ "browsingActivity",
++ "websiteContent"
]
}
}
ddiff --git a/package.json b/package.json
iindex 7f00628..9b69a7b 100644
---- a/package.json
++++ b/package.json
@@@ -6,6 +6,8 @@
"scripts": {
"test": "node test/run.js",
"pack": "node scripts/pack.js",
++ "pack:stores": "node scripts/pack.js --stores",
++ "lint:firefox": "npx --yes [email protected] lint --source-dir browser --warnings-as-errors",
"pack:dev": "node scripts/pack.js --api http://localhost:4850",
"icons": "node scripts/icons.js"
},
ddiff --git a/scripts/pack.js b/scripts/pack.js
iindex 237880c..98f5431 100644
---- a/scripts/pack.js
++++ b/scripts/pack.js
@@@ -4,6 +4,10 @@
* Packs browser/ into a zip for loading or store review. It never uploads anything anywhere.
*
* node scripts/pack.js → dist/openvibe-companion-<version>.zip
++ * node scripts/pack.js --stores → dist/openvibe-companion-<version>-chrome.zip and
++ * dist/openvibe-companion-<version>-firefox.zip, the store uploads:
++ * the Chrome zip drops browser_specific_settings (Chrome warns on it), the Firefox zip drops
++ * minimum_chrome_version; nothing else differs from browser/.
* node scripts/pack.js --api http://localhost:4850
* → dist/openvibe-companion-<version>-dev.zip,
* a development build whose API origin (lib/config.js), host permission and CSP connect-src
@@@ -68,6 +72,15 @@ function listFiles(dir, base = dir) {
}
/** Copy of browser/ with the API origin rewritten (dev builds), as { name → Buffer }. */
++/** The store manifest for one browser: only drops the other browser's key (the audit runs on the full one). */
++function forTarget(files, target) {
++ const m = JSON.parse(files.get('manifest.json'));
++ if (target === 'chrome') delete m.browser_specific_settings;
++ else if (target === 'firefox') delete m.minimum_chrome_version;
++ else throw new Error('target must be chrome or firefox');
++ files.set('manifest.json', Buffer.from(JSON.stringify(m, null, 2) + '\n'));
++}
++
function sourceFiles(apiOrigin) {
const files = new Map(listFiles(SRC).map((f) => [f, fs.readFileSync(path.join(SRC, f))]));
if (apiOrigin !== DEFAULT_ORIGIN) {
@@@ -81,10 +94,11 @@ function sourceFiles(apiOrigin) {
return files;
}
--function build({ api = DEFAULT_ORIGIN, outDir = path.join(ROOT, 'dist') } = {}) {
++function build({ api = DEFAULT_ORIGIN, outDir = path.join(ROOT, 'dist'), target = null } = {}) {
if (!/^https:\/\/[a-z0-9.-]+(:\d+)?$/.test(api) && !/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(api)) {
throw new Error('--api must be an https origin, or http://localhost / http://127.0.0.1 for development');
}
++ if (target && api !== DEFAULT_ORIGIN) throw new Error('store builds use the release API origin only');
const files = sourceFiles(api);
const tmp = fs.mkdtempSync(path.join(require('os').tmpdir(), 'ov-ext-pack-'));
try {
@@@ -92,10 +106,11 @@ function build({ api = DEFAULT_ORIGIN, outDir = path.join(ROOT, 'dist') } = {})
const audit = auditDir(tmp);
if (audit.problems.length) throw new Error(`permission audit failed:\n - ${audit.problems.join('\n - ')}`);
const version = audit.manifest.version;
++ if (target) forTarget(files, target);
fs.mkdirSync(outDir, { recursive: true });
-- const out = path.join(outDir, `openvibe-companion-${version}${api === DEFAULT_ORIGIN ? '' : '-dev'}.zip`);
++ const out = path.join(outDir, `openvibe-companion-${version}${api === DEFAULT_ORIGIN ? '' : '-dev'}${target ? '-' + target : ''}.zip`);
fs.writeFileSync(out, zip([...files].map(([name, data]) => ({ name, data }))));
-- return { out, files: [...files.keys()], origin: api };
++ return { out, files: [...files.keys()], origin: api, target };
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
@@@ -104,8 +119,12 @@ function build({ api = DEFAULT_ORIGIN, outDir = path.join(ROOT, 'dist') } = {})
if (require.main === module) {
const i = process.argv.indexOf('--api');
try {
-- const r = build({ api: i > -1 ? String(process.argv[i + 1] || '').replace(/\/+$/, '') : DEFAULT_ORIGIN });
-- console.log(`wrote ${path.relative(process.cwd(), r.out)} (${r.files.length} files, API ${r.origin})`);
++ const runs = process.argv.includes('--stores') ? [{ target: 'chrome' }, { target: 'firefox' }]
++ : [{ api: i > -1 ? String(process.argv[i + 1] || '').replace(/\/+$/, '') : DEFAULT_ORIGIN }];
++ for (const opts of runs) {
++ const r = build(opts);
++ console.log(`wrote ${path.relative(process.cwd(), r.out)} (${r.files.length} files, API ${r.origin}${r.target ? ', ' + r.target : ''})`);
++ }
} catch (err) {
console.error(err.message);
process.exit(1);
ddiff --git a/store/listing.md b/store/listing.md
nnew file mode 100644
iindex 0000000..4904703
---- /dev/null
++++ b/store/listing.md
@@@ -0,0 +1,50 @@
++# Store listing: OpenVibe Companion
++
++Copy these fields into the Chrome Web Store developer dashboard and addons.mozilla.org (AMO).
++The upload files come from `npm run pack:stores`: `dist/openvibe-companion-<version>-chrome.zip`
++and `dist/openvibe-companion-<version>-firefox.zip`. The privacy and permission answers are in
++[privacy.md](privacy.md), the screenshots in [screenshots.md](screenshots.md).
++
++| Field | Value |
++|---|---|
++| Name | OpenVibe Companion |
++| Category | Chrome Web Store: **Shopping**. AMO: **Shopping** |
++| Language | English |
++| Homepage | https://openvibe.coupons/companion |
++| Support URL | https://openvibe.coupons/companion |
++| Source code (AMO "source code" question) | Not needed: no minifier, bundler or generated code. The zip is `browser/` as written, and https://github.com/OpenVibers/OpenVibe.Extensions is public (MIT). |
++| License (AMO) | MIT |
++| Firefox add-on id | `[email protected]` (from `browser_specific_settings.gecko.id`) |
++| Minimum versions | Chrome 111, Firefox 128 |
++
++## Short description
++
++Chrome Web Store "summary" (132 characters at most; this is 125):
++
++> Coupon codes for the shop you are on, tried when you ask, plus one-click hand-offs to OpenVibe Rent, Watch, Food and Reviews.
++
++AMO "summary" (250 at most): the same sentence.
++
++## Long description
++
++> OpenVibe Companion helps on the page you are already looking at, and only when you open it.
++>
++> **Coupon codes for this shop.** Open the Companion on a shop and it asks OpenVibe Coupons (openvibe.coupons) for codes for that site, by its hostname only: never the full address, never the page. Press "Try these codes on this page" and the Companion types them into the shop's coupon box one at a time and tells you which ones lowered the total. It never rewrites links and never adds referral tags.
++>
++> **Tell other shoppers what worked.** If you connect the Companion with an install token from openvibe.coupons, one click reports whether a code worked or failed, so the list stays honest.
++>
++> **The same restaurant on every delivery app.** On a delivery app, "Compare delivery apps" opens OpenVibe Food with the restaurant's name, so you can see its price on the other apps.
++>
++> **Save a review site's rating.** On Glassdoor, Indeed, Yelp, the BBB, Trustpilot, Google Maps or Tripadvisor, "Save its rating to OpenVibe Reviews" reads the rating the page states and opens openvibe.reviews to check and save it.
++>
++> **Keep a listing, watch a page.** "Keep on my Rent board" and "Check the rent" open OpenVibe Rent with the listing's address; "Watch this page" opens OpenVibe Watch to tell you when the page changes.
++>
++> **What it can see.** The Companion asks for no access to your sites until you open it on a tab (the browser's "active tab" permission). It has no background worker, no content scripts, and no access to your history, cookies or other tabs. Every hand-off opens a tab you can see, and nothing is saved until you save it there. The only thing it stores is the optional install token, on this device.
++>
++> Open source (MIT): https://github.com/OpenVibers/OpenVibe.Extensions
++
++## Notes for the reviewer
++
++AMO "notes to reviewer" and the Chrome Web Store's test instructions:
++
++> No account is needed. To see codes, open the Companion on any shop that OpenVibe Coupons lists (https://openvibe.coupons shows them). "Try these codes on this page" runs only on a page with a coupon box. The settings page takes an optional `cpx_` install token from https://openvibe.coupons; without it, everything except reporting works.
ddiff --git a/store/privacy.md b/store/privacy.md
nnew file mode 100644
iindex 0000000..6c35fdb
---- /dev/null
++++ b/store/privacy.md
@@@ -0,0 +1,49 @@
++# Store privacy answers: OpenVibe Companion
++
++Written from the code at this commit (`browser/`), not from intentions. If the code changes what it
++reads, sends or stores, change this file, the README's "What it can see" table and
++`browser_specific_settings.gecko.data_collection_permissions` together.
++
++## Single purpose (Chrome Web Store)
++
++> Help with the page in the active tab, only when the person opens the Companion: coupon codes for the shop they are on (looked up on OpenVibe Coupons by hostname and tried in the page's coupon box on request), and one-press hand-offs of that page to OpenVibe services (Rent, Watch, Food, Reviews) in a tab the person sees.
++
++## Permission justifications (Chrome Web Store)
++
++| Permission | Justification |
++|---|---|
++| `activeTab` | When the person opens the popup, it reads the active tab's address and title, to look up coupon codes for that site by its hostname and to fill the hand-off buttons. No access to any tab before that click, and none to other tabs. |
++| `scripting` | Two buttons inject a script packaged in the extension into the active tab's top frame, only after the press: "Try these codes on this page" runs `lib/autoapply.js` (finds the coupon box, types each code, reads whether the order total changed) and "Save its rating to OpenVibe Reviews" runs `lib/rating.js` (reads the rating the page states). Neither makes a request or reads cookies or storage. |
++| `storage` | Keeps the optional `cpx_` install token the person pastes on the settings page, in `storage.local` (never synced). Nothing else is stored. |
++| Host permission `https://openvibe.coupons/*` | The one API the extension calls: look up a merchant by hostname, list its codes, and report whether a code worked. |
++| Remote code | **No.** All JavaScript is in the package; the CSP is `script-src 'self'` with no `eval`. |
++
++## Data usage (Chrome Web Store "Privacy practices" tab)
++
++Tick these data types, because each leaves the device in some case:
++
++| Type | Ticked | What, when, where |
++|---|---|---|
++| Web history | **Yes** | The active tab's **hostname** goes to openvibe.coupons when the popup opens on a site. On a hand-off press, the page's address (credentials, fragment and tracking parameters removed) and title go to openvibe.rent, openvibe.watch, openvibe.food or openvibe.reviews in the address of a tab the person sees. |
++| Website content | **Yes** | On "Save its rating to OpenVibe Reviews": the business name, rating, scale, count of ratings and city the page states, in the address of the openvibe.reviews tab. On "Compare delivery apps": the restaurant or store name taken from the page title, to openvibe.food. |
++| Authentication information | **Yes** | Only if the person connects the extension: the `cpx_` install token they pasted is sent to openvibe.coupons as `Authorization: Bearer` on its API calls. It is issued by openvibe.coupons and sent nowhere else. |
++| User activity | No | No clicks, keystrokes or scrolls are recorded. A report sends a code id, `worked`/`failed` and an optional reason the person chose, which is the person's own feedback. |
++| Financial and payment information | No | `lib/autoapply.js` reads the order total in the page to tell which codes lowered it; the total stays in the popup and is never sent. |
++| Personally identifiable information, health, personal communications, location | No | Not read. (The city sent with a rating is the business's, from the page's structured data.) |
++
++Certify all three: the data is not sold to third parties; it is not used or transferred for
++purposes unrelated to the single purpose; it is not used or transferred to determine
++creditworthiness or for lending.
++
++**Privacy policy URL:** https://openvibe.coupons/companion (owner: confirm the page carries the
++"what it reads and sends" text above, or link the OpenVibe privacy page that does).
++
++## Firefox (AMO)
++
++`browser_specific_settings.gecko.data_collection_permissions.required` declares, for the same
++reasons as the table above: `authenticationInfo`, `browsingActivity`, `websiteContent`. AMO shows
++these to the person at install. Nothing is optional, because nothing is collected that the person
++didn't press for, and the token is the person's own choice on the settings page.
++
++In Firefox, MV3 host permissions can be withheld at install; the settings page then shows "Allow
++access to openvibe.coupons".
ddiff --git a/store/screenshots.md b/store/screenshots.md
nnew file mode 100644
iindex 0000000..a1b62af
---- /dev/null
++++ b/store/screenshots.md
@@@ -0,0 +1,20 @@
++# Screenshot plan: OpenVibe Companion
++
++Chrome Web Store: 1 to 5 screenshots, 1280×800 (or 640×400), PNG or JPEG, no alpha; a 440×280
++small promo tile. AMO: any size up to 2400×1800, ideally the same 1280×800 shots. Take them from
++the release build (`npm run pack:stores`, loaded unpacked) on real pages, with no personal data
++in the address bar or the page.
++
++| # | Shows | Page | Caption |
++|---|---|---|---|
++| 1 | The popup open on a shop with codes listed | a shop OpenVibe Coupons lists | Coupon codes for the shop you are on |
++| 2 | "Try these codes on this page" finished: which code lowered the total | the same shop's cart, coupon box visible | Tried in the coupon box when you ask |
++| 3 | The popup on a review site, the "Save its rating to OpenVibe Reviews" button | a Trustpilot or Yelp business page | Save a review site's rating |
++| 4 | The popup on a rental listing, "Keep on my Rent board" and "Check the rent" | a rental listing page | Keep a listing, check the rent |
++| 5 | The settings page with the token field empty | `options.html` | No account needed; connect only to report |
++
++Small promo tile (440×280): the 128 px icon on the OpenVibe blue, the name, and "Coupon codes,
++tried when you ask".
++
++Headless capture works for the popup and settings pages only (a popup can't be opened over a
++real tab headless, so shots 1 to 4 are a person's screen captures, cropped to 1280×800).
ddiff --git a/test/pack.test.js b/test/pack.test.js
iindex e8879f7..dc42565 100644
---- a/test/pack.test.js
++++ b/test/pack.test.js
@@@ -66,6 +66,22 @@ function unzip(buf) {
assert.doesNotMatch(files.get('manifest.json').toString() + files.get('lib/config.js').toString(), /openvibe\.coupons\//);
});
++ await check('the store zips differ from browser/ only by the other browser\'s manifest key, reproducibly', async () => {
++ const src = path.join(__dirname, '..', 'browser');
++ const full = JSON.parse(fs.readFileSync(path.join(src, 'manifest.json'), 'utf8'));
++ for (const [target, dropped] of [['chrome', 'browser_specific_settings'], ['firefox', 'minimum_chrome_version']]) {
++ const r = build({ outDir: tmp, target });
++ assert.match(path.basename(r.out), new RegExp(`^openvibe-companion-\\d+\\.\\d+\\.\\d+-${target}\\.zip$`));
++ const files = unzip(fs.readFileSync(r.out));
++ for (const [name, data] of files) if (name !== 'manifest.json') assert.ok(data.equals(fs.readFileSync(path.join(src, name))), name);
++ const want = { ...full };
++ delete want[dropped];
++ assert.deepStrictEqual(JSON.parse(files.get('manifest.json')), want);
++ assert.ok(fs.readFileSync(build({ outDir: tmp, target }).out).equals(fs.readFileSync(r.out)));
++ }
++ assert.throws(() => build({ outDir: tmp, target: 'chrome', api: 'http://localhost:4850' }), /release API origin/);
++ });
++
await check('refuses remote http origins and anything that is not an origin', async () => {
for (const bad of ['http://openvibe.coupons', 'http://evil.example.net', 'https://openvibe.coupons/path', 'ftp://x', '']) {
assert.throws(() => build({ api: bad, outDir: tmp }), /--api/, bad);