openvibers/openvibe.shared Commits
README (consuming, releasing, compatibility), vendored-copy divergence audit, consumer migration plan; alpha
Changes
@@@ -2,72 +2,177 @@> Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders.--**Status:** placeholder — planning only, no runnable code yet.--**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.3 and §16.6.++**Status:** alpha. The `openvibe-shared` package lives here, v1.0.0 (Wave 2). Consumers still++vendor their own copies until they apply [docs/migration-plan.md](docs/migration-plan.md).++**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.3 and §16.6.**License:** AGPL-3.0 (same as every OpenVibe service).--## Purpose----The canonical home of what is today `OpenVibe.Network/packages/openvibe-shared` (navbar, footer, themes, icons, legal pages, SEO helpers, notifications UI, panels), published as immutable versioned artifacts with a CDN-compatible mirror instead of being vendored/rsynced from Network.----## Owns--++This repository **is** the npm package `openvibe-shared`. `package.json` sits at the repository++root, so GitHub's tarball of a release tag installs as the package with no build step. It was++imported history-free from `OpenVibe.Network/packages/openvibe-shared` at Network `f337eb6`.++[docs/divergence-2026-09-22.md](docs/divergence-2026-09-22.md) records how the vendored copies++were reconciled first.++++## What's in it++++| Kind | Files |++|---|---|++| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `openvibe-sw.js` |++| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics`, `app-icon`, `auth-client`, `brand`, `builtin-themes`, `chrome-ssr`, `legal`, `middleware`, `notifications`, `seo`, `theme-sync`, `url-resolver`, `files`; `footer` and `icons` (= `ov-icons.js`) work on both sides |++| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js` |++++Every module file has an `exports` entry, so `require('openvibe-shared/navbar')`,++`require.resolve('openvibe-shared/package.json')` and the rest all resolve.++++## Consuming it++++### Server side: pin a release tarball++++```json++"dependencies": {++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"++}++```++++`npm install` writes the resolved URL and its `integrity` into `package-lock.json`, and `npm ci`++reproduces it exactly. Upgrade by changing the tag in the URL and running `npm install`. Never++hand-copy files into `node_modules/` or a `vendor/` directory.++++```js++const legal = require('openvibe-shared/legal');++const seo = require('openvibe-shared/seo');++const { AnalyticsTracker } = require('openvibe-shared/analytics');++```++++### Browser side: two options++++1. **From the Network** (what most pages do today):++ `<script src="https://openvibe.network/shared/navbar.js" defer></script>`. Network serves the++ browser files of the version it pins, from its own `node_modules/openvibe-shared`.++2. **From the site's own origin**: the page's first paint doesn't depend on the Network being++ up, and the files match the version the site's server pins. Serve exactly the browser files++ from `node_modules`:++++ ```js++ const shared = require('openvibe-shared/files');++ app.use('/shared', (req, res, next) => {++ const name = path.basename(req.path);++ if (!shared.isBrowserFile(name)) return next();++ res.setHeader('Access-Control-Allow-Origin', '*');++ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');++ res.sendFile(shared.path(name));++ });++ // Web Push worker: same origin, scope "/"++ app.get('/openvibe-sw.js', (req, res) => { res.setHeader('Service-Worker-Allowed', '/'); res.sendFile(shared.path('openvibe-sw.js')); });++ ```++++ Never `express.static` the whole package directory. It also holds server code, tests and++ scripts.++++**navbar.js and `nav-icons.js`.** navbar.js draws the icons for its own rows inline. The rest++of the Font Awesome glyph set sites name in `links`, `chips` and menu sections lives in++`nav-icons.js`. navbar.js fetches that file once, after first paint, from **the same directory++navbar.js was loaded from**, as `nav-icons.js?v=<first 12 hex of its sha256>`. It falls back to++`https://openvibe.network/shared/` when navbar.js is bundled or inlined.++++So wherever you serve navbar.js, serve `nav-icons.js` beside it. The `?v=` matches the file's++content hash, so it can be cached as immutable.++++Until the glyphs arrive, each icon's slot has the right width and nothing moves. If the file++can't load, those icons fall back to `<i class="fa-solid …">`. Two options change this:++++- `OpenVibeNavbar.init({ iconsUrl })` points navbar.js at another URL.++- Loading `nav-icons.js` *before* navbar.js puts every glyph in the first frame.++++## Development++++```bash++npm ci++npm test # every test/*.test.js (fnm exec --using=22.22.1 npm test)++npm test -- navbar # only matching files++npm run size # raw + brotli (q11) size of every browser file++python3 scripts/build-nav-icons.py --font …/fa-solid-900.woff2 --css …/fontawesome.min.css++node scripts/build-theme-loader.js++```++++Tests are plain Node with stubbed browser globals. Some tests guard the release:++++- **navbar.js size budget**: 29 KB brotli (`test/nav-icons.test.js`).++- **Built-in icons**: every icon the default navbar renders must be built in and drawn with no++ fetch.++- **Generated blocks**: the generated navbar and theme-loader blocks must be up to date.++- **Browser bundles**: browser files must be free of `require()` and Node globals++ (`test/browser-bundles.test.js`).++- **Exports**: every export must resolve.++++CI runs on Node 22.22.1 and installs a tag-style tarball into an empty project.++++## Releasing++++1. Land the change on `main` with tests green. Add an entry under a new version heading in++ `CHANGELOG.md`, and set the same version in `package.json`.++2. Commit (`Release vX.Y.Z`), then tag: `git tag -a vX.Y.Z -m "vX.Y.Z"`. Push `main` and the tag.++ CI runs on the tag.++3. **Tags are immutable.** Never move or delete a released tag. Consumer lockfiles pin its++ tarball's integrity hash. A fix is a new patch release.++4. Upgrade Network first, because it serves `https://openvibe.network/shared/*` to every site.++ Then upgrade Live, Community, Media, Tools and Sites: change the tag in the URL, run++ `npm install`, commit the lockfile and deploy.++++GitHub builds tag tarballs on demand. In practice they're byte-stable, but GitHub doesn't++promise it. If `npm ci` ever reports `EINTEGRITY` for this package on an unchanged tag, check++that the tag wasn't moved, then run `npm install` to refresh the lockfile. The content is the++tag's tree either way.++++## Compatibility policy++++- **Semver.** Major releases are for breaking changes: removing or renaming an exported++ subpath, an export, a browser global (`OpenVibeNavbar`, `OpenVibeFooter`, …) or a served file++ name, removing an init option, or changing markup, classes or defaults that sites style or++ script against. Minor releases add modules, options or files. Patch releases are fixes with no++ API change.++- **The previous major stays servable.** Network serves each major it supports at++ `/shared/v<major>/<file>`. It keeps the older major installed side by side++ (`"openvibe-shared-v1": "<v1 tarball URL>"`) for at least 90 days after the next major ships,++ and for as long as any consumer or published HTML still references it. That includes the++ pre-rendered pages in `OpenVibe.Sites/dist/`.++- **Unversioned paths never jump a major silently.** `/shared/<file>` keeps serving the major it++ served before. It moves to a new major only after every consumer has either moved to it or++ switched to an explicit `/shared/v<major>/` path. Because navbar.js loads `nav-icons.js` from++ its own directory, a versioned navbar always gets the icons of its own release.++- A new browser file is added to `files.js` in the same release, so sites that serve from++ `node_modules` pick it up.++++## Charter++++The canonical home of what was `OpenVibe.Network/packages/openvibe-shared` (navbar, footer,++themes, icons, legal pages, SEO helpers, notifications UI, panels). It is published as++immutable versioned artifacts with a CDN-compatible mirror, instead of being vendored or++rsynced from Network.++++**Owns**- `@openvibe/tokens|ui|chrome|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`++ (today still one package, `openvibe-shared`)- design tokens and theme rendering- release manifest client (active-tab update coordinator)--## Does not own--++**Does not own**- theme *authority* (OpenVibe.Network owns the theme API and preferences)- identity--## Planned surfaces----- vanilla-JS-compatible build kept; no framework rewrite required--- pinned versions, selective entry points, content-addressed browser assets--- local/mirrored serving so a Network outage does not blank every page----## Data (authority tables / families)----- none (artifact repository)----## Capabilities and events----- package APIs only++**Planned surfaces**++- a vanilla-JS-compatible build (no framework rewrite required)++- pinned versions, selective entry points and content-addressed browser assets++- local/mirrored serving, so a Network outage doesn't blank every page--Events: `release manifest notifications (client side)`----## Depends on----- OpenVibe.Contracts (release manifest contract)----## Acceptance (must be true before "done")++**Data:** none (artifact repository). **Capabilities and events:** package APIs only; release++manifest notifications (client side). **Depends on:** OpenVibe.Contracts (release manifest++contract).++**Acceptance (must be true before "done")**- Live, Community, Media, Tools and Sites consume pinned versions with no manual copies- old `openvibe.network/shared/*` URLs keep serving version-pinned mirrors during migration--## Bootstrap / extraction source----Extract `packages/openvibe-shared` from OpenVibers/OpenVibe.Network; Network then consumes it like every other product.----## Launch rule----This repository does not make the product real, and the domain keeps its placeholder page on--[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the--following exist here (plan §12.12):----1. an owning runtime with health/readiness endpoints and observability;--2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals);--3. server-rendered or static public routes that are useful without JavaScript;--4. real persistence and end-to-end workflows;--5. capability and event registration against `OpenVibe.Contracts`;--6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour;--7. acceptance tests proving the advertised functionality.----The launch release removes the domain from `OpenVibe.Sites/sites.json`, switches routing and--registers maturity in the ecosystem registry atomically. A placeholder is never counted as an--implemented service.-----Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).@@@ -1,7 +1,10 @@{"repository": "OpenVibers/OpenVibe.Shared",-- "stage": "placeholder",-- "code": false,++ "stage": "alpha",++ "code": true,++ "wave": "W2",++ "package": "openvibe-shared",++ "version": "1.0.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",-- "updated": "2026-09-21"++ "updated": "2026-09-22"}nnew file mode 100644@@@ -0,0 +1,95 @@++# Vendored-copy divergence audit — 2026-09-22++++Before `openvibe-shared` moved into this repository, every vendored copy was compared with the++canonical package, `OpenVibe.Network/packages/openvibe-shared` at Network commit **`f337eb6`**.++Comparisons use the **committed** trees (`git archive HEAD`), not working trees, because some++working trees carry CRLF line endings that git normalises away (see Live below).++++**Result: no consumer holds a fix that canonical lacks.** Every difference is a copy that fell++behind canonical, or a line-ending artefact. Canonical was imported unchanged (except navbar.js,++per CHANGELOG 1.0.0) and nothing needed folding back in.++++## Method++++1. `diff -r` of each consumer's committed `vendor/openvibe-shared` against canonical.++2. For every divergent file, `git log -p -- vendor/openvibe-shared/<file>` in the consumer, and++ `git log -- packages/openvibe-shared/<file>` in Network, to date both sides.++3. To catch a local fix that a later re-sync might have overwritten: every blob ever committed++ under `vendor/openvibe-shared` in Live, Media, Tools, Community and Sites was looked up among++ the 116 blobs ever committed under `packages/openvibe-shared` in Network. A blob that never++ existed in Network is a local edit.++++## Consumers at audit time++++| Repo | HEAD | Copy | Differences from canonical |++|---|---|---|---|++| OpenVibe.Live (seadragon worktree = main) | `e266060` | full, no tests | none in committed content (CRLF artefact below) |++| OpenVibe.Media | `ddfefcf` | full, no tests | `ov-icons.js` (stale) |++| OpenVibe.Tools | `fd5d318` | full, no tests | `ov-icons.js` (stale) |++| OpenVibe.Community | `d7c6e60` | full, partial tests | `ov-icons.js` (stale), `test/navbar-brand.test.js` (stale) |++| OpenVibe.Sites | `185014f` | 5 files | none: `app-icon`, `chrome-ssr`, `footer`, `legal`, `seo` are byte-identical |++++The vendored copies that lack `test/` and `scripts/build-nav-icons.py` are simply packaged++differently. None of those files ships to a browser or is required at runtime.++++## Decisions++++### Live: `vendor/openvibe-shared/user-card.js`: canonical (no content change)++++- The reported divergence is **line endings only**. `diff --strip-trailing-cr` is empty, and the++ committed blob in Live equals canonical's.++- Network's working tree has CRLF in five files (`analytics.js`, `auth-client.js`, `index.js`,++ `middleware.js`, `user-card.js`), and the seadragon worktree has CRLF in `user-card.js`. Their++ index copies are LF (`git ls-files --eol` shows `i/lf w/crlf`, `core.autocrlf=input`). A++ working-tree diff between a CRLF checkout and an LF checkout therefore flags the file.++- History: Live `e371ed1` (2026-09-18, "…vendor sync") re-synced it. It changed the banner++ fallback accent from `#8b5cf6` to `#3b82f6`, which canonical already had. Before that, Live had++ only the initial import `ac0d6a4`.++- Prevention: this repository ships `.gitattributes` with `* text=auto eol=lf`, and the package++ is installed from a tarball, so no working-tree line endings are involved.++++### Media, Tools, Community: `vendor/openvibe-shared/ov-icons.js`: canonical++++- The three copies are identical to each other (md5 `8c50f1d5…`). Each equals canonical as of++ Network `6ce1ef4` (2026-09-21 09:57, "Ring icons: solid Font Awesome glyphs inside the OV++ ring"). They were synced one minute later: Media `6114bc6`, Tools `0346edc`, Community `de947ea`.++- Canonical then got Network **`71be248`** (2026-09-21 11:57, "Ring icons: fix solid glyphs++ rendering off-centre (transform-origin pivot bug)"). That commit moves the position and scale++ `transform` from the classed `.ovi-glyph` element onto a plain inner `<g>`. The consumers++ never re-synced after it.++- None of the three consumer histories edits the file outside those syncs. `git log -p` shows++ only `vendor sync` or `…; vendor sync` commits.++- Canonical is the fix, and the consumers carry the bug. It matters where they++ `require('openvibe-shared/icons')` on the server. Tools does this in++ `apps/gateway/server/pages/site.js`, so Tools' server-rendered solid ring glyphs++ (`icons.use()` / `icons.sprite()`) can render off-centre until Tools moves to v1.0.0.++ Browsers load `ov-icons.js` from `openvibe.network/shared/`, which is already fixed.++++### Community: `vendor/openvibe-shared/test/navbar-brand.test.js`: canonical++++- Community's copy comes from `ff4a6b6` (2026-09-17 20:10, "Re-sync vendored openvibe-shared").++ Canonical changed afterwards in `68597ec` and `959bcb3` (2026-09-18): links became++ `class="ovnav-link …"`, the mobile drawer arrived and icons became inline SVG.++- Its old assertions (`class="active"`, `fa-plus icon`) would fail against the navbar Community++ actually ships. It never runs, because Community's `test/run.js` reads only `test/`. Its copy++ also lacks `panels.test.js` and `shared-modules.test.js`.++- The copy is stale, with nothing to fold in. Tests now live here only.++++### Local-edit scan (method step 3)++++The scan found one local-only blob. Community `66c11ab` (2026-09-17 20:09, "Scaffold the++Community app…") vendored a `navbar.js` that never existed in Network. It is 7 lines away from++Network `d8b7f65`, committed at 20:08 the same evening. Community copied the Network working++tree about a minute before that commit. `d8b7f65` then added `apiBase` to the history calls and++guarded `nav.setAttribute`.++++That snapshot is therefore an older draft of canonical, not a fix. Community's next syncs++(`f4533f4`, `ee2f49d`, `b9cb474`, `de947ea`) replaced it with Network blobs. No other vendored++blob in any consumer's history is missing from Network's history.++++## Follow-ups++++- Tools' server-rendered solid ring glyphs can stay off-centre until Tools installs v1.0.0++ (`docs/migration-plan.md`).++- Once every consumer pins this package, the `vendor/openvibe-shared` directories are deleted,++ and this class of drift can't recur.nnew file mode 100644@@@ -0,0 +1,627 @@++# Migration plan: consumers move to the pinned OpenVibe.Shared release++++Status: **prepared, not applied** (2026-09-22). Nothing here has been committed in the consumer++repos. The lead applies it after tagging `v1.0.0` in this repo.++++Every consumer replaces its `file:` dependency on a vendored copy with the release tarball:++++```json++"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"++```++++It also deletes its `vendor/openvibe-shared` copy. Code that read that directory by path now++goes through `require('openvibe-shared/files')`, which lists the package's browser files and++where they are.++++## Order and prerequisites++++1. **Tag `v1.0.0`** in OpenVibe.Shared and push it. The repo must be public, because codeload++ serves private repos only with a token. Check that++ `curl -fsSLI https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0`++ returns 200.++2. **Network first.** It serves `https://openvibe.network/shared/*` to Media, Tools, Community++ and Sites pages. Once it deploys, those pages get the v1.0.0 navbar and footer, plus the new++ lazily fetched `nav-icons.js` next to them, with no change in those repos.++3. **Live.** It serves `/shared/*` from its own origin, and its navbar needs `nav-icons.js`++ served beside `navbar.js`. Until Live's allowlist includes that file, lazy icons 404 and fall++ back to `<i class="fa-solid">`. Live loads Font Awesome, so the icons still show.++4. **Community, Media, Tools** in any order. Their browsers already load from the Network. Their++ servers only `require()` Node modules: legal, footer SSR, app-icon, auth-client, analytics++ and icons.++5. **Sites**, optionally. It has no `package.json` today.++++Common steps for each npm consumer:++++```bash++rm -rf node_modules/openvibe-shared # today a symlink into vendor/++git rm -r -q vendor/openvibe-shared # Network: packages/openvibe-shared++# edit package.json (diff below), then:++npm install --no-audit --no-fund # rewrites package-lock.json++npm test++git grep -n 'vendor/openvibe-shared' # must print nothing but docs you chose to keep++```++++The lockfile then holds a single entry:++++```json++"node_modules/openvibe-shared": {++ "version": "1.0.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",++ "integrity": "sha512-…",++ "license": "MIT",++ "dependencies": { "jsonwebtoken": "^9.0.0" }++}++```++++The `"vendor/openvibe-shared": {…}` workspace entry and the `"link": true` entry disappear.++++On the hosts, `npm ci --omit=dev` (or `npm install` where there is no lockfile) replaces any++stale real-directory copy in `node_modules/openvibe-shared`. The "node_modules is a stale COPY"++comments stop applying.++++The references below come from `git grep -n -E "vendor/openvibe-shared|packages/openvibe-shared|'vendor', 'openvibe-shared'|'packages', 'openvibe-shared'"`++over tracked files, run at the HEADs listed in `docs/divergence-2026-09-22.md`. Network's++generated `docs/roadmap-baseline/*` is left out (270 hits in `inventory.json`), because it is++regenerated.++++---++++## OpenVibe.Network++++### References++++```++README.md:265 docs: "The `packages/openvibe-shared/` directory provides…"++docs/platform-plan.md:34-35 docs++docs/shared-contracts.md:3,5,177 docs++package.json:9 test script runs 7 packages/openvibe-shared/test/* files++package.json:27 "openvibe-shared": "file:./packages/openvibe-shared",++package-lock.json:24,1483,2099 lockfile entries++scripts/roadmap-baseline/generate.js:30,37,97,133,500++server/index.js:607 res.sendFile(path.resolve(__dirname, '..', 'packages', 'openvibe-shared', 'openvibe-sw.js'))++server/index.js:612 const sharedPath = path.resolve(__dirname, '..', 'packages', 'openvibe-shared');++deploy/nginx/openvibe.network.conf:80-86 location /shared/ { … add_header Cache-Control "public, max-age=300, …"; }++server/deploy/nginx-generator.js:33-36 { match: '/shared/', cacheTime: '1h' }++```++++The `server/…` requires that use `require('openvibe-shared/...')` need no change:++`server/index.js:22,28,426`, `server/admin/routes.js:16`, `server/db/database.js:698`,++`server/deploy/routes.js:12`, `server/home/render.js:8,9,80`,++`server/notifications/notification-service.js:10`, `server/setup/routes.js:6` and++`server/url-registry.js:3`.++++### Steps++++1. `git rm -r packages/openvibe-shared`. From now on, changes land in OpenVibe.Shared and ship++ as a release.++2. Edit `package.json`:++++```diff++- "test": "node test/registry-bootstrap.test.js && node packages/openvibe-shared/test/theme-parity.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node packages/openvibe-shared/test/url-resolver.test.js && node packages/openvibe-shared/test/url-resolver-deploy.test.js && node packages/openvibe-shared/test/navbar-auth.test.js && node packages/openvibe-shared/test/navbar-brand.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node packages/openvibe-shared/test/shared-modules.test.js && node test/chrome.test.js && node packages/openvibe-shared/test/panels.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/registry-ecosystem.test.js && node test/service-principal.test.js",+++ "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/chrome.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/registry-ecosystem.test.js && node test/service-principal.test.js",++@@++- "openvibe-shared": "file:./packages/openvibe-shared",+++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",++```++++ The package's own tests now run in OpenVibe.Shared's CI.++++3. Run `npm install`. It regenerates `package-lock.json`.++4. Update `server/index.js`, currently lines 601–621. `/shared/*` now serves from++ `node_modules/openvibe-shared`, through an allowlist, with a versioned alias:++++```diff++-// Web-push service worker: must be served from THIS origin (scope /), so each site++-// exposes the shared worker at /openvibe-sw.js rather than loading it from Network.++-app.get('/openvibe-sw.js', (req, res) => {++- res.setHeader('Content-Type', 'application/javascript; charset=utf-8');++- res.setHeader('Service-Worker-Allowed', '/');++- res.setHeader('Cache-Control', 'no-cache');++- res.sendFile(path.resolve(__dirname, '..', 'packages', 'openvibe-shared', 'openvibe-sw.js'));++-});++-++-// Serve openvibe-shared client-side libs (notification-ui.js, navbar.js, etc.)++-// Accessible at https://openvibe.network/shared/notification-ui.js etc.++-const sharedPath = path.resolve(__dirname, '..', 'packages', 'openvibe-shared');++-app.use('/shared', express.static(sharedPath, {++- setHeaders(res, filePath) {++- res.setHeader('Access-Control-Allow-Origin', '*');++- // Override helmet's same-origin policies so other domains can load these scripts++- res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');++- res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none');++- res.setHeader('Cache-Control', 'public, max-age=300');++- },++-}));+++// openvibe-shared: the OpenVibe.Shared release package.json pins, from node_modules.+++const sharedFiles = require('openvibe-shared/files');++++++// Web-push service worker: must be served from THIS origin (scope /), so each site+++// exposes the shared worker at /openvibe-sw.js rather than loading it from Network.+++app.get('/openvibe-sw.js', (req, res) => {+++ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');+++ res.setHeader('Service-Worker-Allowed', '/');+++ res.setHeader('Cache-Control', 'no-cache');+++ res.sendFile(sharedFiles.path('openvibe-sw.js'));+++});++++++// Serve openvibe-shared client-side libs (notification-ui.js, navbar.js, etc.) to every site at+++// https://openvibe.network/shared/<file>, and the same release at /shared/v1/<file> (OpenVibe.Shared+++// README, compatibility policy). Only the package's browser files: its server modules, tests and+++// scripts are not served. A ?v= equal to the file's content hash (navbar.js asks for nav-icons.js+++// that way) is cacheable forever; anything else gets five minutes.+++const sharedRev = new Map();+++function serveShared(req, res, next) {+++ const name = req.path.slice(1);+++ if (!sharedFiles.isBrowserFile(name)) return next();+++ if (!sharedRev.has(name)) sharedRev.set(name, require('crypto').createHash('sha256').update(fs.readFileSync(sharedFiles.path(name))).digest('hex').slice(0, 12));+++ res.setHeader('Access-Control-Allow-Origin', '*');+++ // Override helmet's same-origin policies so other domains can load these scripts+++ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');+++ res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none');+++ res.setHeader('Cache-Control', req.query.v === sharedRev.get(name) ? 'public, max-age=31536000, immutable' : 'public, max-age=300, stale-while-revalidate=60');+++ res.sendFile(sharedFiles.path(name));+++}+++app.use('/shared/v1', serveShared);+++app.use('/shared', serveShared);++```++++ Behaviour changes here, all intended. `/shared/package.json`, `/shared/test/*`,++ `/shared/scripts/*` and the Node modules (`/shared/auth-client.js`, `/shared/middleware.js`, …)++ stop being public. Every file any site loads is in the allowlist: `navbar`, `theme-loader`,++ `footer`, `notification-ui`, `account-switcher`, `ov-mark`, `ov-icons`, `history`,++ `sso-client`, `panels`, `ui`, `island`, and `user-card` from the README example.++++ The hash map is per process. The package only changes through a deploy, which restarts the++ process.++++5. Update `deploy/nginx/openvibe.network.conf`. Remove the proxy's own `Cache-Control`. With++ `add_header`, nginx sends a second `Cache-Control` next to the app's, and the immutable one++ would be ignored:++++```diff++ location /shared/ {++ proxy_pass http://127.0.0.1:4000;++ proxy_http_version 1.1;++ proxy_set_header Host $host;++- # Shared browser modules are loaded by every OpenVibe site from here. Five minutes at the++- # edge: a navbar/footer change reaches the whole network within minutes instead of an hour.++- add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";+++ # Shared browser modules are loaded by every OpenVibe site from here. The app sets+++ # Cache-Control: five minutes (+60s stale) by default, a year for a ?v= content-hash URL.++ }++```++++ Also update `server/deploy/nginx-generator.js:33-36`: drop `cacheTime: '1h'` from++ `{ match: '/shared/' }`, because `expires 1h` adds a conflicting header as well. Re-run++ `node test/nginx-generator-live.test.js`, and adjust it if it asserts that `cacheTime`.++++6. Update `scripts/roadmap-baseline/generate.js`. The canonical package now lives in the++ sibling repo, and consumers carry it in `node_modules`:++++```diff++- { name: 'OpenVibe.Network', service: 'network', scan: ['server', 'packages/openvibe-shared'] },+++ { name: 'OpenVibe.Network', service: 'network', scan: ['server'] },+++ { name: 'OpenVibe.Shared', service: 'shared', scan: ['.'] },++@@++-const CANONICAL_SHARED = path.join(NETWORK_DIR, 'packages/openvibe-shared');+++const CANONICAL_SHARED = path.join(ROOT, 'OpenVibe.Shared');++```++++ At line 97, "Tables created by the vendored shared package", read++ `path.join(dir, 'node_modules/openvibe-shared')` when `vendor/openvibe-shared` is absent. At++ line 133, "Vendored shared-package drift", report each repo's pinned tag, read from++ `package.json` `dependencies['openvibe-shared']`, instead of hashing vendored files. Then++ regenerate `docs/roadmap-baseline/` and run `node test/roadmap-baseline.test.js`.++7. Update the docs. In `README.md:263-282` ("Shared Client Libraries"), change "The++ `packages/openvibe-shared/` directory provides…" to "The `openvibe-shared` package++ ([OpenVibers/OpenVibe.Shared](https://github.com/OpenVibers/OpenVibe.Shared), pinned in++ package.json) provides… served at `/shared/` (and `/shared/v1/`)". Add `nav-icons.js` to the++ table ("navbar glyphs, loaded by navbar.js"). In `docs/platform-plan.md:34-35` and++ `docs/shared-contracts.md:3,5`, replace "lives in `OpenVibe.Network/packages/openvibe-shared`++ … vendored (rsync, never edited)" with "lives in OpenVibe.Shared, consumed as a pinned++ release tarball". In `docs/shared-contracts.md:177`, the path becomes++ `node scripts/measure-icons.js` (in OpenVibe.Shared).++8. Verify:++ - `curl -sI https://openvibe.network/shared/navbar.js` returns 200 with `max-age=300`.++ - `curl -sI "https://openvibe.network/shared/nav-icons.js?v=$(node -e "console.log(require('crypto').createHash('sha256').update(require('fs').readFileSync(require('openvibe-shared/files').path('nav-icons.js'))).digest('hex').slice(0,12))")"`++ returns 200 with `immutable`.++ - `curl -sI https://openvibe.network/shared/v1/footer.js` returns 200.++ - `curl -sI https://openvibe.network/shared/auth-client.js` returns 404, or the SPA fallback.++ - A Media, Tools or Community page shows the navbar icons, and DevTools shows++ `nav-icons.js?v=…` loaded after first paint.++++---++++## OpenVibe.Live++++### References++++```++package.json:47 "openvibe-shared": "file:./vendor/openvibe-shared",++package-lock.json:29,2832,3779 lockfile entries++server/index.js:446 () => path.resolve(__dirname, '../vendor/openvibe-shared'),++server/index.js:449-450 OpenVibeApp/packages fallbacks++scripts/perf/check-budgets.js:32,36 budget check reads vendor/ by path++test/features-registry.test.js:24 fileFor() reads vendor/ by path++public/js/ov-navbar-live.js:1 comment++public/js/app.js:1483 comment++AGENTS.md:59 README.md:21 SETUP.md:14,30 docs/architecture.md:12 SECURITY_AUDIT.md:98 docs++.gitignore:31-32 leftover "public/shared/" (nothing writes it any more)++```++++`server/index.js:124` (`require('openvibe-shared/analytics')`) and `server/config.js:9`++(`require('openvibe-shared/url-resolver')`) need no change.++++### Steps++++1. Run `rm -rf node_modules/openvibe-shared && git rm -r vendor/openvibe-shared`.++2. Edit `package.json`, then run `npm install`:++++```diff++- "openvibe-shared": "file:./vendor/openvibe-shared",+++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",++```++++3. Update `server/index.js`, currently lines 434–505. The package lists its own browser files,++ and that list includes the new `nav-icons.js`. The server serves them from++ `node_modules/openvibe-shared` with no candidate search:++++```diff++ // ── Static Files ─────────────────────────────────────────────++-// Serve openvibe-shared browser assets at /shared/ — serve directly from the++-// resolved source directory to avoid write operations (the public/ directory++-// is read-only under systemd ProtectSystem=strict on production).++-const SHARED_BROWSER_FILES = ['theme-loader.js', 'notification-ui.js', 'account-switcher.js', 'openvibe-sw.js', 'footer.js', 'ov-mark.js', 'tooltip.js', 'history.js', 'navbar.js', 'sso-client.js', 'ov-icons.js', 'island.js', 'ui.js', 'panels.js'];++-let sharedServePath = null;++-++-(function resolveSharedAssets() {++- const candidates = [++- // The vendored copy is the source of truth (synced from OpenVibe.Network). On the++- // server node_modules/openvibe-shared is a stale COPY, not a symlink, so it must++- // never win over vendor/.++- () => path.resolve(__dirname, '../vendor/openvibe-shared'),++- () => path.dirname(require.resolve('openvibe-shared/package.json')),++- () => path.resolve(__dirname, '../node_modules/openvibe-shared'),++- () => path.resolve(__dirname, '..', '..', 'OpenVibeApp', 'packages', 'openvibe-shared'),++- () => path.resolve(__dirname, '..', '..', 'packages', 'openvibe-shared'),++- ];++- for (const getPath of candidates) {++- try {++- const p = getPath();++- if (p && fs.existsSync(p)) {++- // Verify at least one required browser file exists++- const found = SHARED_BROWSER_FILES.filter(f => fs.existsSync(path.join(p, f)));++- if (found.length > 0) {++- sharedServePath = p;++- assets.setSharedDir(p);++- console.log(`[Server] /shared: serving ${found.length}/${SHARED_BROWSER_FILES.length} browser file(s) from ${p}`);++- const missing = SHARED_BROWSER_FILES.filter(f => !found.includes(f));++- if (missing.length) console.warn(`[Server] /shared: missing files: ${missing.join(', ')}`);++- return;++- }++- }++- } catch (_) {}++- }++- console.error('[Server] /shared: openvibe-shared source not found — shared browser assets will return 404');++-})();++-++-if (sharedServePath) {+++// Serve openvibe-shared browser assets at /shared/ — straight from node_modules/openvibe-shared,+++// the OpenVibe.Shared release package.json pins (the public/ directory is read-only under+++// systemd ProtectSystem=strict on production). The package lists its own browser files.+++const sharedFiles = require('openvibe-shared/files');+++const SHARED_BROWSER_FILES = sharedFiles.BROWSER;+++const sharedServePath = sharedFiles.dir;+++assets.setSharedDir(sharedServePath);+++console.log(`[Server] /shared: serving ${SHARED_BROWSER_FILES.length} browser file(s) from ${sharedServePath}`);++++++{++ // Web-push service worker must be same-origin with scope "/" → expose it at the root.++ app.get('/openvibe-sw.js', (req, res) => {++@@ (lines 474-499 unchanged: /openvibe-sw.js, whitelist, ?v= immutable, sendFile)++-} else {++- console.error('[Server] /shared: serving unavailable — shared browser assets will return 404');++- app.use('/shared', (req, res) => {++- res.status(404).type('text/plain').send('Shared assets unavailable');++- });++ }++```++++ The unchanged middle already makes `nav-icons.js?v=<hash>` immutable, because++ `req.query.v === assets.hashOf('/shared/nav-icons.js')`. navbar.js asks for exactly that++ hash (`NAV_ICONS_REV`, sha256 truncated to 12 hex characters, the same as `assets.js`).++ `user-card.js` is now served too. It is public on the Network already.++++4. Update `scripts/perf/check-budgets.js`:++++```diff++ const assets = require(path.join(ROOT, 'server/web/assets'));++-try { assets.setSharedDir(path.join(ROOT, 'vendor/openvibe-shared')); } catch { /* */ }+++const SHARED_DIR = require('openvibe-shared/files').dir;+++try { assets.setSharedDir(SHARED_DIR); } catch { /* */ }++ const html = assets.renderRoute(assets.document('index.html').html, '/');++ const fileFor = (url) => {++ const p = url.split('?')[0];++- return p.startsWith('/shared/') ? path.join(ROOT, 'vendor/openvibe-shared', path.basename(p)) : path.join(ROOT, 'public', p);+++ return p.startsWith('/shared/') ? path.join(SHARED_DIR, path.basename(p)) : path.join(ROOT, 'public', p);++ };++```++++ Expected after v1.0.0: `eagerJsBrotliKB` goes from 263.8 (failing) to **252.4 / 260**, and++ `eagerJsRawKB` from 1147.1 to 1113.9. This was measured with the v1.0.0 `navbar.js`++ substituted into the budget script.++++5. Update `test/features-registry.test.js:24`:++++```diff++-const fileFor = (url) => url.startsWith('/shared/') ? path.join(ROOT, 'vendor/openvibe-shared', path.basename(url)) : path.join(PUB, url);+++const fileFor = (url) => url.startsWith('/shared/') ? require('openvibe-shared/files').path(path.basename(url)) : path.join(PUB, url);++```++++ `files.path()` throws for a non-browser file. That is stricter than before, and correct,++ because a feature must not reference one.++++6. Update comments and docs:++ - `public/js/ov-navbar-live.js:1`: "(vendor/openvibe-shared/navbar.js, served at++ /shared/navbar.js)" becomes "(openvibe-shared/navbar.js from OpenVibe.Shared, served at++ /shared/navbar.js)".++ - `public/js/app.js:1483`: "(vendor/openvibe-shared/panels.js" becomes++ "(openvibe-shared/panels.js".++ - `AGENTS.md:59`: "**openvibe-shared:** Pinned release of OpenVibers/OpenVibe.Shared++ (`"openvibe-shared": "https://codeload…/refs/tags/vX.Y.Z"`), served at `/shared/*` from++ `node_modules`. Change it there and bump the tag; never edit `node_modules`."++ - `README.md:21`, `SETUP.md:14,30` and `docs/architecture.md:12`: same wording.++ - `SECURITY_AUDIT.md:98`: `vendor/openvibe-shared/analytics.js` becomes++ `openvibe-shared/analytics.js`, with remediation "change in OpenVibe.Shared, release, bump++ the pin".++ - `.gitignore:31-32`: delete the leftover `public/shared/` lines. This is optional.++7. Deploy. `deploy/scripts/deploy.sh` treats `package.json` and `package-lock.json` as++ "deps + server", so the release runs `npm ci --omit=dev`, which installs the tarball, and++ then restarts. A restart drops live RTMP publishers, so check `/api/streams` first and use++ `deploy.sh --wait-idle`.++8. Verify:++ - `npm test` passes, including `perf:budget`.++ - `curl -sI https://openvibe.live/shared/nav-icons.js?v=<hash>` returns an immutable 200.++ - The home page's navbar icons render. `nav-icons.js` loads after first paint.++++---++++## OpenVibe.Community++++### References++++```++package.json:25 "openvibe-shared": "file:./vendor/openvibe-shared"++package-lock.json:21,1075,1567 lockfile entries++README.md:203 "vendor/openvibe-shared unmodified copy of OpenVibe.Network/packages/openvibe-shared"++```++++Browsers load `https://openvibe.network/shared/{theme-loader,navbar,footer}.js` absolutely (the++`NETWORK_URL` constant at `server/render/layout.js:16`, used at lines 120–125). Server requires++are `server/app.js:82` (legal), `server/auth/routes.js:37` (auth-client) and++`server/render/layout.js:118,133` (app-icon, footer). Neither needs a change.++++The vendored copy's tracked `test/` files never ran and are deleted with it.++++### Steps++++1. Run `rm -rf node_modules/openvibe-shared && git rm -r vendor/openvibe-shared`.++2. Edit `package.json`, then run `npm install`:++++```diff++- "openvibe-shared": "file:./vendor/openvibe-shared"+++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"++```++++3. `README.md:203`: the line becomes++ `node_modules/openvibe-shared pinned OpenVibe.Shared release (package.json); never edit`.++ README line 173 ("serve the current `openvibe-shared`…") still reads correctly.++4. `npm test`. `test/ssr.test.js:19-21` asserts the absolute Network URLs, which are unchanged.++ Community's CI (`.github/workflows/ci.yml`, `npm ci`) fetches the tarball from codeload.++5. There is no deploy script. On the host, `git pull && npm ci --omit=dev && systemctl restart openvibe-community`.++++---++++## OpenVibe.Media++++### References++++```++package.json:22 "openvibe-shared": "file:./vendor/openvibe-shared",++package-lock.json:20,1973,2587 lockfile entries++README.md:42 "vendor/openvibe-shared/ vendored shared helpers (do not edit; re-sync from canonical)"++```++++Browsers load `${NETWORK_URL}/shared/{theme-loader,navbar,footer}.js`++(`server/public/page-chrome.js:14,82,146-147`) and++`https://openvibe.network/shared/theme-loader.js` (`server/public/browse.js:240`). Server++requires are `server/index.js:42`, `server/user-auth.js:37`,++`server/public/browse.js:269,271,276` and `server/public/page-chrome.js:81,187,191`. None of them++needs a change.++++### Steps++++1. Run `rm -rf node_modules/openvibe-shared && git rm -r vendor/openvibe-shared`.++2. Edit `package.json`, then run `npm install`:++++```diff++- "openvibe-shared": "file:./vendor/openvibe-shared",+++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",++```++++3. `README.md:42`: the line becomes++ `node_modules/openvibe-shared/ pinned OpenVibe.Shared release (package.json); never edit`.++4. `npm test`. `test/public-pages.test.js:40,42` asserts the absolute Network URLs, which are++ unchanged.++5. Deploy: pull, `npm ci --omit=dev`, restart. This picks up the ov-icons centring fix from++ `docs/divergence-2026-09-22.md` for server-side icons.++++---++++## OpenVibe.Tools++++Tools has no root dependency. Each app under `apps/*` has its own `package.json`, and++`deploy/scripts/deploy.sh` rsyncs `vendor/openvibe-shared` into every app's++`node_modules/openvibe-shared`, because npm copies `file:` dependencies on the host. After the++change, each app installs its own copy of the tarball, and npm's cache means it is downloaded++once. There is no rsync and no symlink.++++### References++++```++package.json:7 "install:all": "npm --prefix vendor/openvibe-shared install && npm --prefix apps/gateway install && …"++deploy/scripts/deploy.sh:2-3,11 header comment + rsync of vendor/openvibe-shared into each app++apps/gateway/server/index.js:282 res.sendFile(path.resolve(__dirname, '..', '..', '..', 'vendor', 'openvibe-shared', 'openvibe-sw.js'));++apps/audio/package.json:21 "openvibe-shared": "file:../../vendor/openvibe-shared",++apps/docs/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared",++apps/food/package.json:19 "openvibe-shared": "file:../../vendor/openvibe-shared"++apps/gateway/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared"++apps/img/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared",++apps/maps/package.json:14 "openvibe-shared": "file:../../vendor/openvibe-shared",++apps/text/package.json:19 "openvibe-shared": "file:../../vendor/openvibe-shared"++apps/yt/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared",++apps/gateway/package-lock.json:19,22,752 lockfile entries++apps/maps/package-lock.json:22,25,1287 lockfile entries++apps/text/package-lock.json:19,22,873 lockfile entries++README.md:26 "└── openvibe-shared # vendored shared package (canonical copy lives in OpenVibe.Network)"++apps/audio/README.md:34,52 "openvibe-shared package (vendored at `vendor/openvibe-shared`)"++```++++Browser pages load `https://openvibe.network/shared/*` absolutely: `apps/*/public/index.html`,++gateway `public/{audio,img,dev,net}.html`, `server/pages/site.js:120`, and 31++`apps/text/public/*.html` pages. They need no change. Server requires such as++`openvibe-shared/analytics` in every app, and gateway's brand, legal, auth-client, app-icon,++icons, seo and footer, need no change either.++++### Steps++++1. `git rm -r vendor/openvibe-shared`. The untracked `vendor/openvibe-shared/node_modules` left++ by `install:all` goes too: `rm -rf vendor`.++2. In each of the 8 app `package.json` files, keep the trailing comma where the file has one:++++```diff++- "openvibe-shared": "file:../../vendor/openvibe-shared",+++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",++```++++ Then run++ `for a in apps/*/; do [ -f "$a/package.json" ] && (cd "$a" && rm -rf node_modules/openvibe-shared && npm install --no-audit --no-fund); done`.++ That regenerates the three tracked lockfiles (gateway, maps and text).++3. Update the root `package.json:7`:++++```diff++- "install:all": "npm --prefix vendor/openvibe-shared install && npm --prefix apps/gateway install && npm --prefix apps/maps install && …",+++ "install:all": "npm --prefix apps/gateway install && npm --prefix apps/maps install && …",++```++++ Only the leading `npm --prefix vendor/openvibe-shared install && ` is removed.++++4. Update `deploy/scripts/deploy.sh`:++++```diff++ #!/usr/bin/env bash++-# Deploy OpenVibe.Tools on the host: pull, refresh the vendored shared package inside each app's++-# node_modules (npm copies file: dependencies on this host, so a pull alone leaves them stale),++-# install new dependencies when a package.json changed, restart the units, check health.+++# Deploy OpenVibe.Tools on the host: pull, install dependencies in each app whose package.json+++# changed (that includes a new openvibe-shared release tag), restart the units, check health.++ set -euo pipefail++ cd "$(dirname "$0")/../.."++ BEFORE=$(git rev-parse HEAD); git pull -q --ff-only; AFTER=$(git rev-parse HEAD)++ for app in apps/*/; do++ [ -f "$app/package.json" ] || continue++ if git diff --name-only "$BEFORE" "$AFTER" -- "$app/package.json" | grep -q . || [ ! -d "$app/node_modules" ]; then (cd "$app" && npm install --omit=dev --no-audit --no-fund --silent); fi++- d="$app/node_modules/openvibe-shared"; [ -L "$d" ] || { mkdir -p "$d"; rsync -a --delete --exclude node_modules vendor/openvibe-shared/ "$d/"; }++ done++```++++ The first deploy after this change touches every app's `package.json`, so every app runs++ `npm install`. That replaces the rsynced real-directory copy with the tarball. If an app's++ `node_modules/openvibe-shared` is a leftover symlink into `vendor/`, delete it before that++ deploy.++++5. Update `apps/gateway/server/index.js:282`:++++```diff++- res.sendFile(path.resolve(__dirname, '..', '..', '..', 'vendor', 'openvibe-shared', 'openvibe-sw.js'));+++ res.sendFile(require('openvibe-shared/files').path('openvibe-sw.js'));++```++++6. Update the docs. `README.md:26` becomes `(openvibe-shared is a pinned OpenVibe.Shared++ release in each app's package.json; no vendor/ copy)`, and `apps/audio/README.md:34,52`++ becomes "openvibe-shared (pinned OpenVibe.Shared release)". `README.md:116` ("Browser pages++ load shared JS absolutely from `https://openvibe.network/shared/`") stays true.++7. Verify with `npm --prefix apps/gateway test` and `npm --prefix apps/yt test`. Then run++ `node -e "require('openvibe-shared/icons')"` in each app directory. After deploy, check that++ `/api/health` is healthy and that gateway ring icons are centred. That comes from the++ ov-icons fix Tools lacked.++++---++++## OpenVibe.Sites (optional, no package.json today)++++### References++++```++build.js:16 comment "Vendored from OpenVibe.Network/packages/openvibe-shared (copy, never edit)"++build.js:17 const legal = require('./vendor/openvibe-shared/legal');++build.js:63-64 require('./vendor/openvibe-shared/app-icon').headTags(…) / .CRITICAL++build.js:193 require('./vendor/openvibe-shared/footer').ssr(…)++build.js:210 require('./vendor/openvibe-shared/app-icon').manifest(…)++vendor/openvibe-shared/{app-icon,chrome-ssr,footer,legal,seo}.js (5-file subset, byte-identical to v1.0.0)++```++++### Steps++++1. Add `package.json`:++++```json++{++ "name": "openvibe-sites",++ "private": true,++ "description": "Placeholder and hub pages for the planned OpenVibe domains (static build)",++ "scripts": { "build": "node build.js" },++ "dependencies": {++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"++ }++}++```++++ Run `npm install` and commit `package-lock.json`. `.gitignore` already lists `node_modules/`.++++2. Update `build.js`:++++```diff++-// Vendored from OpenVibe.Network/packages/openvibe-shared (copy, never edit): the network's legal documents.++-const legal = require('./vendor/openvibe-shared/legal');+++// The network's legal documents, from the pinned OpenVibe.Shared release (package.json).+++const legal = require('openvibe-shared/legal');++@@++-${require('./vendor/openvibe-shared/app-icon').headTags({ site: 'network' }).split('\n')[0]}++-${require('./vendor/openvibe-shared/app-icon').CRITICAL}+++${require('openvibe-shared/app-icon').headTags({ site: 'network' }).split('\n')[0]}+++${require('openvibe-shared/app-icon').CRITICAL}++@@++-${require('./vendor/openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })}+++${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })}++@@++-function manifest(site) { return JSON.stringify(require('./vendor/openvibe-shared/app-icon').manifest({ …+++function manifest(site) { return JSON.stringify(require('openvibe-shared/app-icon').manifest({ …++```++++3. Run `git rm -r vendor/openvibe-shared`, then `node build.js`. `git status dist/` must show no++ changes, because the five modules are byte-identical to v1.0.0.++4. Update `deploy/scripts/deploy.sh`: add `npm ci --omit=dev --no-audit --no-fund` before++ `node build.js`. The host needs outbound HTTPS to codeload.github.com.++++The published `dist/` HTML hard-codes `https://openvibe.network/shared/navbar.js`. That is one++reason the Network keeps `/shared/<file>` on the v1 line (README, compatibility policy).++++---++++## After all consumers moved++++- `git grep -n 'vendor/openvibe-shared'` is empty in every repo, apart from historical docs.++ `find ~/OpenVibers -path '*/vendor/openvibe-shared' -type d` finds nothing.++- Every future shared change follows the same path: a PR to OpenVibe.Shared, a `vX.Y.Z` tag,++ then a pin bump in Network first and in the other consumers after.
@@ -2,72 +2,177 @@
@@ -2,72 +2,177 @@
> Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** placeholder — planning only, no runnable code yet.
-**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.3 and §16.6.
+**Status:** alpha. The `openvibe-shared` package lives here, v1.0.0 (Wave 2). Consumers still
+vendor their own copies until they apply [docs/migration-plan.md](docs/migration-plan.md).
+**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.3 and §16.6.
**License:** AGPL-3.0 (same as every OpenVibe service).
**License:** AGPL-3.0 (same as every OpenVibe service).
-## Purpose
-
-The canonical home of what is today `OpenVibe.Network/packages/openvibe-shared` (navbar, footer, themes, icons, legal pages, SEO helpers, notifications UI, panels), published as immutable versioned artifacts with a CDN-compatible mirror instead of being vendored/rsynced from Network.
-
-## Owns
-
+This repository **is** the npm package `openvibe-shared`. `package.json` sits at the repository
+root, so GitHub's tarball of a release tag installs as the package with no build step. It was
+imported history-free from `OpenVibe.Network/packages/openvibe-shared` at Network `f337eb6`.
+[docs/divergence-2026-09-22.md](docs/divergence-2026-09-22.md) records how the vendored copies
+were reconciled first.
+
+## What's in it
+
+| Kind | Files |
+|---|---|
+| Browser scripts, served at `/shared/<file>` (listed in `files.js`) | `navbar.js`, `nav-icons.js`, `theme-loader.js`, `footer.js`, `notification-ui.js`, `account-switcher.js`, `user-card.js`, `ov-mark.js`, `ov-icons.js`, `history.js`, `sso-client.js`, `panels.js`, `ui.js`, `island.js`, `tooltip.js`, `openvibe-sw.js` |
+| Node modules (`require('openvibe-shared/<name>')`) | `index` (`.`), `analytics`, `app-icon`, `auth-client`, `brand`, `builtin-themes`, `chrome-ssr`, `legal`, `middleware`, `notifications`, `seo`, `theme-sync`, `url-resolver`, `files`; `footer` and `icons` (= `ov-icons.js`) work on both sides |
+| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js` |
+
+Every module file has an `exports` entry, so `require('openvibe-shared/navbar')`,
+`require.resolve('openvibe-shared/package.json')` and the rest all resolve.
+
+## Consuming it
+
+### Server side: pin a release tarball
+
+```json
+"dependencies": {
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"
+}
+```
+
+`npm install` writes the resolved URL and its `integrity` into `package-lock.json`, and `npm ci`
+reproduces it exactly. Upgrade by changing the tag in the URL and running `npm install`. Never
+hand-copy files into `node_modules/` or a `vendor/` directory.
+
+```js
+const legal = require('openvibe-shared/legal');
+const seo = require('openvibe-shared/seo');
+const { AnalyticsTracker } = require('openvibe-shared/analytics');
+```
+
+### Browser side: two options
+
+1. **From the Network** (what most pages do today):
+ `<script src="https://openvibe.network/shared/navbar.js" defer></script>`. Network serves the
+ browser files of the version it pins, from its own `node_modules/openvibe-shared`.
+2. **From the site's own origin**: the page's first paint doesn't depend on the Network being
+ up, and the files match the version the site's server pins. Serve exactly the browser files
+ from `node_modules`:
+
+ ```js
+ const shared = require('openvibe-shared/files');
+ app.use('/shared', (req, res, next) => {
+ const name = path.basename(req.path);
+ if (!shared.isBrowserFile(name)) return next();
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
+ res.sendFile(shared.path(name));
+ });
+ // Web Push worker: same origin, scope "/"
+ app.get('/openvibe-sw.js', (req, res) => { res.setHeader('Service-Worker-Allowed', '/'); res.sendFile(shared.path('openvibe-sw.js')); });
+ ```
+
+ Never `express.static` the whole package directory. It also holds server code, tests and
+ scripts.
+
+**navbar.js and `nav-icons.js`.** navbar.js draws the icons for its own rows inline. The rest
+of the Font Awesome glyph set sites name in `links`, `chips` and menu sections lives in
+`nav-icons.js`. navbar.js fetches that file once, after first paint, from **the same directory
+navbar.js was loaded from**, as `nav-icons.js?v=<first 12 hex of its sha256>`. It falls back to
+`https://openvibe.network/shared/` when navbar.js is bundled or inlined.
+
+So wherever you serve navbar.js, serve `nav-icons.js` beside it. The `?v=` matches the file's
+content hash, so it can be cached as immutable.
+
+Until the glyphs arrive, each icon's slot has the right width and nothing moves. If the file
+can't load, those icons fall back to `<i class="fa-solid …">`. Two options change this:
+
+- `OpenVibeNavbar.init({ iconsUrl })` points navbar.js at another URL.
+- Loading `nav-icons.js` *before* navbar.js puts every glyph in the first frame.
+
+## Development
+
+```bash
+npm ci
+npm test # every test/*.test.js (fnm exec --using=22.22.1 npm test)
+npm test -- navbar # only matching files
+npm run size # raw + brotli (q11) size of every browser file
+python3 scripts/build-nav-icons.py --font …/fa-solid-900.woff2 --css …/fontawesome.min.css
+node scripts/build-theme-loader.js
+```
+
+Tests are plain Node with stubbed browser globals. Some tests guard the release:
+
+- **navbar.js size budget**: 29 KB brotli (`test/nav-icons.test.js`).
+- **Built-in icons**: every icon the default navbar renders must be built in and drawn with no
+ fetch.
+- **Generated blocks**: the generated navbar and theme-loader blocks must be up to date.
+- **Browser bundles**: browser files must be free of `require()` and Node globals
+ (`test/browser-bundles.test.js`).
+- **Exports**: every export must resolve.
+
+CI runs on Node 22.22.1 and installs a tag-style tarball into an empty project.
+
+## Releasing
+
+1. Land the change on `main` with tests green. Add an entry under a new version heading in
+ `CHANGELOG.md`, and set the same version in `package.json`.
+2. Commit (`Release vX.Y.Z`), then tag: `git tag -a vX.Y.Z -m "vX.Y.Z"`. Push `main` and the tag.
+ CI runs on the tag.
+3. **Tags are immutable.** Never move or delete a released tag. Consumer lockfiles pin its
+ tarball's integrity hash. A fix is a new patch release.
+4. Upgrade Network first, because it serves `https://openvibe.network/shared/*` to every site.
+ Then upgrade Live, Community, Media, Tools and Sites: change the tag in the URL, run
+ `npm install`, commit the lockfile and deploy.
+
+GitHub builds tag tarballs on demand. In practice they're byte-stable, but GitHub doesn't
+promise it. If `npm ci` ever reports `EINTEGRITY` for this package on an unchanged tag, check
+that the tag wasn't moved, then run `npm install` to refresh the lockfile. The content is the
+tag's tree either way.
+
+## Compatibility policy
+
+- **Semver.** Major releases are for breaking changes: removing or renaming an exported
+ subpath, an export, a browser global (`OpenVibeNavbar`, `OpenVibeFooter`, …) or a served file
+ name, removing an init option, or changing markup, classes or defaults that sites style or
+ script against. Minor releases add modules, options or files. Patch releases are fixes with no
+ API change.
+- **The previous major stays servable.** Network serves each major it supports at
+ `/shared/v<major>/<file>`. It keeps the older major installed side by side
+ (`"openvibe-shared-v1": "<v1 tarball URL>"`) for at least 90 days after the next major ships,
+ and for as long as any consumer or published HTML still references it. That includes the
+ pre-rendered pages in `OpenVibe.Sites/dist/`.
+- **Unversioned paths never jump a major silently.** `/shared/<file>` keeps serving the major it
+ served before. It moves to a new major only after every consumer has either moved to it or
+ switched to an explicit `/shared/v<major>/` path. Because navbar.js loads `nav-icons.js` from
+ its own directory, a versioned navbar always gets the icons of its own release.
+- A new browser file is added to `files.js` in the same release, so sites that serve from
+ `node_modules` pick it up.
+
+## Charter
+
+The canonical home of what was `OpenVibe.Network/packages/openvibe-shared` (navbar, footer,
+themes, icons, legal pages, SEO helpers, notifications UI, panels). It is published as
+immutable versioned artifacts with a CDN-compatible mirror, instead of being vendored or
+rsynced from Network.
+
+**Owns**
- `@openvibe/tokens|ui|chrome|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`
- `@openvibe/tokens|ui|chrome|auth-ui|seo|legal|icons|release-client|web-runtime|server-web|testing-web`
+ (today still one package, `openvibe-shared`)
- design tokens and theme rendering
- design tokens and theme rendering
- release manifest client (active-tab update coordinator)
- release manifest client (active-tab update coordinator)
-## Does not own
-
+**Does not own**
- theme *authority* (OpenVibe.Network owns the theme API and preferences)
- theme *authority* (OpenVibe.Network owns the theme API and preferences)
- identity
- identity
-## Planned surfaces
-
-- vanilla-JS-compatible build kept; no framework rewrite required
-- pinned versions, selective entry points, content-addressed browser assets
-- local/mirrored serving so a Network outage does not blank every page
-
-## Data (authority tables / families)
-
-- none (artifact repository)
-
-## Capabilities and events
-
-- package APIs only
+**Planned surfaces**
+- a vanilla-JS-compatible build (no framework rewrite required)
+- pinned versions, selective entry points and content-addressed browser assets
+- local/mirrored serving, so a Network outage doesn't blank every page
-Events: `release manifest notifications (client side)`
-
-## Depends on
-
-- OpenVibe.Contracts (release manifest contract)
-
-## Acceptance (must be true before "done")
+**Data:** none (artifact repository). **Capabilities and events:** package APIs only; release
+manifest notifications (client side). **Depends on:** OpenVibe.Contracts (release manifest
+contract).
+**Acceptance (must be true before "done")**
- Live, Community, Media, Tools and Sites consume pinned versions with no manual copies
- Live, Community, Media, Tools and Sites consume pinned versions with no manual copies
- old `openvibe.network/shared/*` URLs keep serving version-pinned mirrors during migration
- old `openvibe.network/shared/*` URLs keep serving version-pinned mirrors during migration
-## Bootstrap / extraction source
-
-Extract `packages/openvibe-shared` from OpenVibers/OpenVibe.Network; Network then consumes it like every other product.
-
-## Launch rule
-
-This repository does not make the product real, and the domain keeps its placeholder page on
-[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until all of the
-following exist here (plan §12.12):
-
-1. an owning runtime with health/readiness endpoints and observability;
-2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals);
-3. server-rendered or static public routes that are useful without JavaScript;
-4. real persistence and end-to-end workflows;
-5. capability and event registration against `OpenVibe.Contracts`;
-6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour;
-7. acceptance tests proving the advertised functionality.
-
-The launch release removes the domain from `OpenVibe.Sites/sites.json`, switches routing and
-registers maturity in the ecosystem registry atomically. A placeholder is never counted as an
-implemented service.
-
---
---
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers).
@@ -1,7 +1,10 @@
@@ -1,7 +1,10 @@
{
{
"repository": "OpenVibers/OpenVibe.Shared",
"repository": "OpenVibers/OpenVibe.Shared",
- "stage": "placeholder",
- "code": false,
+ "stage": "alpha",
+ "code": true,
+ "wave": "W2",
+ "package": "openvibe-shared",
+ "version": "1.0.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
- "updated": "2026-09-21"
+ "updated": "2026-09-22"
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,95 @@
@@ -0,0 +1,95 @@
+# Vendored-copy divergence audit — 2026-09-22
+
+Before `openvibe-shared` moved into this repository, every vendored copy was compared with the
+canonical package, `OpenVibe.Network/packages/openvibe-shared` at Network commit **`f337eb6`**.
+Comparisons use the **committed** trees (`git archive HEAD`), not working trees, because some
+working trees carry CRLF line endings that git normalises away (see Live below).
+
+**Result: no consumer holds a fix that canonical lacks.** Every difference is a copy that fell
+behind canonical, or a line-ending artefact. Canonical was imported unchanged (except navbar.js,
+per CHANGELOG 1.0.0) and nothing needed folding back in.
+
+## Method
+
+1. `diff -r` of each consumer's committed `vendor/openvibe-shared` against canonical.
+2. For every divergent file, `git log -p -- vendor/openvibe-shared/<file>` in the consumer, and
+ `git log -- packages/openvibe-shared/<file>` in Network, to date both sides.
+3. To catch a local fix that a later re-sync might have overwritten: every blob ever committed
+ under `vendor/openvibe-shared` in Live, Media, Tools, Community and Sites was looked up among
+ the 116 blobs ever committed under `packages/openvibe-shared` in Network. A blob that never
+ existed in Network is a local edit.
+
+## Consumers at audit time
+
+| Repo | HEAD | Copy | Differences from canonical |
+|---|---|---|---|
+| OpenVibe.Live (seadragon worktree = main) | `e266060` | full, no tests | none in committed content (CRLF artefact below) |
+| OpenVibe.Media | `ddfefcf` | full, no tests | `ov-icons.js` (stale) |
+| OpenVibe.Tools | `fd5d318` | full, no tests | `ov-icons.js` (stale) |
+| OpenVibe.Community | `d7c6e60` | full, partial tests | `ov-icons.js` (stale), `test/navbar-brand.test.js` (stale) |
+| OpenVibe.Sites | `185014f` | 5 files | none: `app-icon`, `chrome-ssr`, `footer`, `legal`, `seo` are byte-identical |
+
+The vendored copies that lack `test/` and `scripts/build-nav-icons.py` are simply packaged
+differently. None of those files ships to a browser or is required at runtime.
+
+## Decisions
+
+### Live: `vendor/openvibe-shared/user-card.js`: canonical (no content change)
+
+- The reported divergence is **line endings only**. `diff --strip-trailing-cr` is empty, and the
+ committed blob in Live equals canonical's.
+- Network's working tree has CRLF in five files (`analytics.js`, `auth-client.js`, `index.js`,
+ `middleware.js`, `user-card.js`), and the seadragon worktree has CRLF in `user-card.js`. Their
+ index copies are LF (`git ls-files --eol` shows `i/lf w/crlf`, `core.autocrlf=input`). A
+ working-tree diff between a CRLF checkout and an LF checkout therefore flags the file.
+- History: Live `e371ed1` (2026-09-18, "…vendor sync") re-synced it. It changed the banner
+ fallback accent from `#8b5cf6` to `#3b82f6`, which canonical already had. Before that, Live had
+ only the initial import `ac0d6a4`.
+- Prevention: this repository ships `.gitattributes` with `* text=auto eol=lf`, and the package
+ is installed from a tarball, so no working-tree line endings are involved.
+
+### Media, Tools, Community: `vendor/openvibe-shared/ov-icons.js`: canonical
+
+- The three copies are identical to each other (md5 `8c50f1d5…`). Each equals canonical as of
+ Network `6ce1ef4` (2026-09-21 09:57, "Ring icons: solid Font Awesome glyphs inside the OV
+ ring"). They were synced one minute later: Media `6114bc6`, Tools `0346edc`, Community `de947ea`.
+- Canonical then got Network **`71be248`** (2026-09-21 11:57, "Ring icons: fix solid glyphs
+ rendering off-centre (transform-origin pivot bug)"). That commit moves the position and scale
+ `transform` from the classed `.ovi-glyph` element onto a plain inner `<g>`. The consumers
+ never re-synced after it.
+- None of the three consumer histories edits the file outside those syncs. `git log -p` shows
+ only `vendor sync` or `…; vendor sync` commits.
+- Canonical is the fix, and the consumers carry the bug. It matters where they
+ `require('openvibe-shared/icons')` on the server. Tools does this in
+ `apps/gateway/server/pages/site.js`, so Tools' server-rendered solid ring glyphs
+ (`icons.use()` / `icons.sprite()`) can render off-centre until Tools moves to v1.0.0.
+ Browsers load `ov-icons.js` from `openvibe.network/shared/`, which is already fixed.
+
+### Community: `vendor/openvibe-shared/test/navbar-brand.test.js`: canonical
+
+- Community's copy comes from `ff4a6b6` (2026-09-17 20:10, "Re-sync vendored openvibe-shared").
+ Canonical changed afterwards in `68597ec` and `959bcb3` (2026-09-18): links became
+ `class="ovnav-link …"`, the mobile drawer arrived and icons became inline SVG.
+- Its old assertions (`class="active"`, `fa-plus icon`) would fail against the navbar Community
+ actually ships. It never runs, because Community's `test/run.js` reads only `test/`. Its copy
+ also lacks `panels.test.js` and `shared-modules.test.js`.
+- The copy is stale, with nothing to fold in. Tests now live here only.
+
+### Local-edit scan (method step 3)
+
+The scan found one local-only blob. Community `66c11ab` (2026-09-17 20:09, "Scaffold the
+Community app…") vendored a `navbar.js` that never existed in Network. It is 7 lines away from
+Network `d8b7f65`, committed at 20:08 the same evening. Community copied the Network working
+tree about a minute before that commit. `d8b7f65` then added `apiBase` to the history calls and
+guarded `nav.setAttribute`.
+
+That snapshot is therefore an older draft of canonical, not a fix. Community's next syncs
+(`f4533f4`, `ee2f49d`, `b9cb474`, `de947ea`) replaced it with Network blobs. No other vendored
+blob in any consumer's history is missing from Network's history.
+
+## Follow-ups
+
+- Tools' server-rendered solid ring glyphs can stay off-centre until Tools installs v1.0.0
+ (`docs/migration-plan.md`).
+- Once every consumer pins this package, the `vendor/openvibe-shared` directories are deleted,
+ and this class of drift can't recur.
new file mode 100644
new file mode 100644
@@ -0,0 +1,627 @@
@@ -0,0 +1,627 @@
+# Migration plan: consumers move to the pinned OpenVibe.Shared release
+
+Status: **prepared, not applied** (2026-09-22). Nothing here has been committed in the consumer
+repos. The lead applies it after tagging `v1.0.0` in this repo.
+
+Every consumer replaces its `file:` dependency on a vendored copy with the release tarball:
+
+```json
+"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"
+```
+
+It also deletes its `vendor/openvibe-shared` copy. Code that read that directory by path now
+goes through `require('openvibe-shared/files')`, which lists the package's browser files and
+where they are.
+
+## Order and prerequisites
+
+1. **Tag `v1.0.0`** in OpenVibe.Shared and push it. The repo must be public, because codeload
+ serves private repos only with a token. Check that
+ `curl -fsSLI https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0`
+ returns 200.
+2. **Network first.** It serves `https://openvibe.network/shared/*` to Media, Tools, Community
+ and Sites pages. Once it deploys, those pages get the v1.0.0 navbar and footer, plus the new
+ lazily fetched `nav-icons.js` next to them, with no change in those repos.
+3. **Live.** It serves `/shared/*` from its own origin, and its navbar needs `nav-icons.js`
+ served beside `navbar.js`. Until Live's allowlist includes that file, lazy icons 404 and fall
+ back to `<i class="fa-solid">`. Live loads Font Awesome, so the icons still show.
+4. **Community, Media, Tools** in any order. Their browsers already load from the Network. Their
+ servers only `require()` Node modules: legal, footer SSR, app-icon, auth-client, analytics
+ and icons.
+5. **Sites**, optionally. It has no `package.json` today.
+
+Common steps for each npm consumer:
+
+```bash
+rm -rf node_modules/openvibe-shared # today a symlink into vendor/
+git rm -r -q vendor/openvibe-shared # Network: packages/openvibe-shared
+# edit package.json (diff below), then:
+npm install --no-audit --no-fund # rewrites package-lock.json
+npm test
+git grep -n 'vendor/openvibe-shared' # must print nothing but docs you chose to keep
+```
+
+The lockfile then holds a single entry:
+
+```json
+"node_modules/openvibe-shared": {
+ "version": "1.0.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",
+ "integrity": "sha512-…",
+ "license": "MIT",
+ "dependencies": { "jsonwebtoken": "^9.0.0" }
+}
+```
+
+The `"vendor/openvibe-shared": {…}` workspace entry and the `"link": true` entry disappear.
+
+On the hosts, `npm ci --omit=dev` (or `npm install` where there is no lockfile) replaces any
+stale real-directory copy in `node_modules/openvibe-shared`. The "node_modules is a stale COPY"
+comments stop applying.
+
+The references below come from `git grep -n -E "vendor/openvibe-shared|packages/openvibe-shared|'vendor', 'openvibe-shared'|'packages', 'openvibe-shared'"`
+over tracked files, run at the HEADs listed in `docs/divergence-2026-09-22.md`. Network's
+generated `docs/roadmap-baseline/*` is left out (270 hits in `inventory.json`), because it is
+regenerated.
+
+---
+
+## OpenVibe.Network
+
+### References
+
+```
+README.md:265 docs: "The `packages/openvibe-shared/` directory provides…"
+docs/platform-plan.md:34-35 docs
+docs/shared-contracts.md:3,5,177 docs
+package.json:9 test script runs 7 packages/openvibe-shared/test/* files
+package.json:27 "openvibe-shared": "file:./packages/openvibe-shared",
+package-lock.json:24,1483,2099 lockfile entries
+scripts/roadmap-baseline/generate.js:30,37,97,133,500
+server/index.js:607 res.sendFile(path.resolve(__dirname, '..', 'packages', 'openvibe-shared', 'openvibe-sw.js'))
+server/index.js:612 const sharedPath = path.resolve(__dirname, '..', 'packages', 'openvibe-shared');
+deploy/nginx/openvibe.network.conf:80-86 location /shared/ { … add_header Cache-Control "public, max-age=300, …"; }
+server/deploy/nginx-generator.js:33-36 { match: '/shared/', cacheTime: '1h' }
+```
+
+The `server/…` requires that use `require('openvibe-shared/...')` need no change:
+`server/index.js:22,28,426`, `server/admin/routes.js:16`, `server/db/database.js:698`,
+`server/deploy/routes.js:12`, `server/home/render.js:8,9,80`,
+`server/notifications/notification-service.js:10`, `server/setup/routes.js:6` and
+`server/url-registry.js:3`.
+
+### Steps
+
+1. `git rm -r packages/openvibe-shared`. From now on, changes land in OpenVibe.Shared and ship
+ as a release.
+2. Edit `package.json`:
+
+```diff
+- "test": "node test/registry-bootstrap.test.js && node packages/openvibe-shared/test/theme-parity.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node packages/openvibe-shared/test/url-resolver.test.js && node packages/openvibe-shared/test/url-resolver-deploy.test.js && node packages/openvibe-shared/test/navbar-auth.test.js && node packages/openvibe-shared/test/navbar-brand.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node packages/openvibe-shared/test/shared-modules.test.js && node test/chrome.test.js && node packages/openvibe-shared/test/panels.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/registry-ecosystem.test.js && node test/service-principal.test.js",
++ "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/chrome.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/registry-ecosystem.test.js && node test/service-principal.test.js",
+@@
+- "openvibe-shared": "file:./packages/openvibe-shared",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",
+```
+
+ The package's own tests now run in OpenVibe.Shared's CI.
+
+3. Run `npm install`. It regenerates `package-lock.json`.
+4. Update `server/index.js`, currently lines 601–621. `/shared/*` now serves from
+ `node_modules/openvibe-shared`, through an allowlist, with a versioned alias:
+
+```diff
+-// Web-push service worker: must be served from THIS origin (scope /), so each site
+-// exposes the shared worker at /openvibe-sw.js rather than loading it from Network.
+-app.get('/openvibe-sw.js', (req, res) => {
+- res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
+- res.setHeader('Service-Worker-Allowed', '/');
+- res.setHeader('Cache-Control', 'no-cache');
+- res.sendFile(path.resolve(__dirname, '..', 'packages', 'openvibe-shared', 'openvibe-sw.js'));
+-});
+-
+-// Serve openvibe-shared client-side libs (notification-ui.js, navbar.js, etc.)
+-// Accessible at https://openvibe.network/shared/notification-ui.js etc.
+-const sharedPath = path.resolve(__dirname, '..', 'packages', 'openvibe-shared');
+-app.use('/shared', express.static(sharedPath, {
+- setHeaders(res, filePath) {
+- res.setHeader('Access-Control-Allow-Origin', '*');
+- // Override helmet's same-origin policies so other domains can load these scripts
+- res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
+- res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none');
+- res.setHeader('Cache-Control', 'public, max-age=300');
+- },
+-}));
++// openvibe-shared: the OpenVibe.Shared release package.json pins, from node_modules.
++const sharedFiles = require('openvibe-shared/files');
++
++// Web-push service worker: must be served from THIS origin (scope /), so each site
++// exposes the shared worker at /openvibe-sw.js rather than loading it from Network.
++app.get('/openvibe-sw.js', (req, res) => {
++ res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
++ res.setHeader('Service-Worker-Allowed', '/');
++ res.setHeader('Cache-Control', 'no-cache');
++ res.sendFile(sharedFiles.path('openvibe-sw.js'));
++});
++
++// Serve openvibe-shared client-side libs (notification-ui.js, navbar.js, etc.) to every site at
++// https://openvibe.network/shared/<file>, and the same release at /shared/v1/<file> (OpenVibe.Shared
++// README, compatibility policy). Only the package's browser files: its server modules, tests and
++// scripts are not served. A ?v= equal to the file's content hash (navbar.js asks for nav-icons.js
++// that way) is cacheable forever; anything else gets five minutes.
++const sharedRev = new Map();
++function serveShared(req, res, next) {
++ const name = req.path.slice(1);
++ if (!sharedFiles.isBrowserFile(name)) return next();
++ if (!sharedRev.has(name)) sharedRev.set(name, require('crypto').createHash('sha256').update(fs.readFileSync(sharedFiles.path(name))).digest('hex').slice(0, 12));
++ res.setHeader('Access-Control-Allow-Origin', '*');
++ // Override helmet's same-origin policies so other domains can load these scripts
++ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
++ res.setHeader('Cross-Origin-Opener-Policy', 'unsafe-none');
++ res.setHeader('Cache-Control', req.query.v === sharedRev.get(name) ? 'public, max-age=31536000, immutable' : 'public, max-age=300, stale-while-revalidate=60');
++ res.sendFile(sharedFiles.path(name));
++}
++app.use('/shared/v1', serveShared);
++app.use('/shared', serveShared);
+```
+
+ Behaviour changes here, all intended. `/shared/package.json`, `/shared/test/*`,
+ `/shared/scripts/*` and the Node modules (`/shared/auth-client.js`, `/shared/middleware.js`, …)
+ stop being public. Every file any site loads is in the allowlist: `navbar`, `theme-loader`,
+ `footer`, `notification-ui`, `account-switcher`, `ov-mark`, `ov-icons`, `history`,
+ `sso-client`, `panels`, `ui`, `island`, and `user-card` from the README example.
+
+ The hash map is per process. The package only changes through a deploy, which restarts the
+ process.
+
+5. Update `deploy/nginx/openvibe.network.conf`. Remove the proxy's own `Cache-Control`. With
+ `add_header`, nginx sends a second `Cache-Control` next to the app's, and the immutable one
+ would be ignored:
+
+```diff
+ location /shared/ {
+ proxy_pass http://127.0.0.1:4000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+- # Shared browser modules are loaded by every OpenVibe site from here. Five minutes at the
+- # edge: a navbar/footer change reaches the whole network within minutes instead of an hour.
+- add_header Cache-Control "public, max-age=300, stale-while-revalidate=60";
++ # Shared browser modules are loaded by every OpenVibe site from here. The app sets
++ # Cache-Control: five minutes (+60s stale) by default, a year for a ?v= content-hash URL.
+ }
+```
+
+ Also update `server/deploy/nginx-generator.js:33-36`: drop `cacheTime: '1h'` from
+ `{ match: '/shared/' }`, because `expires 1h` adds a conflicting header as well. Re-run
+ `node test/nginx-generator-live.test.js`, and adjust it if it asserts that `cacheTime`.
+
+6. Update `scripts/roadmap-baseline/generate.js`. The canonical package now lives in the
+ sibling repo, and consumers carry it in `node_modules`:
+
+```diff
+- { name: 'OpenVibe.Network', service: 'network', scan: ['server', 'packages/openvibe-shared'] },
++ { name: 'OpenVibe.Network', service: 'network', scan: ['server'] },
++ { name: 'OpenVibe.Shared', service: 'shared', scan: ['.'] },
+@@
+-const CANONICAL_SHARED = path.join(NETWORK_DIR, 'packages/openvibe-shared');
++const CANONICAL_SHARED = path.join(ROOT, 'OpenVibe.Shared');
+```
+
+ At line 97, "Tables created by the vendored shared package", read
+ `path.join(dir, 'node_modules/openvibe-shared')` when `vendor/openvibe-shared` is absent. At
+ line 133, "Vendored shared-package drift", report each repo's pinned tag, read from
+ `package.json` `dependencies['openvibe-shared']`, instead of hashing vendored files. Then
+ regenerate `docs/roadmap-baseline/` and run `node test/roadmap-baseline.test.js`.
+7. Update the docs. In `README.md:263-282` ("Shared Client Libraries"), change "The
+ `packages/openvibe-shared/` directory provides…" to "The `openvibe-shared` package
+ ([OpenVibers/OpenVibe.Shared](https://github.com/OpenVibers/OpenVibe.Shared), pinned in
+ package.json) provides… served at `/shared/` (and `/shared/v1/`)". Add `nav-icons.js` to the
+ table ("navbar glyphs, loaded by navbar.js"). In `docs/platform-plan.md:34-35` and
+ `docs/shared-contracts.md:3,5`, replace "lives in `OpenVibe.Network/packages/openvibe-shared`
+ … vendored (rsync, never edited)" with "lives in OpenVibe.Shared, consumed as a pinned
+ release tarball". In `docs/shared-contracts.md:177`, the path becomes
+ `node scripts/measure-icons.js` (in OpenVibe.Shared).
+8. Verify:
+ - `curl -sI https://openvibe.network/shared/navbar.js` returns 200 with `max-age=300`.
+ - `curl -sI "https://openvibe.network/shared/nav-icons.js?v=$(node -e "console.log(require('crypto').createHash('sha256').update(require('fs').readFileSync(require('openvibe-shared/files').path('nav-icons.js'))).digest('hex').slice(0,12))")"`
+ returns 200 with `immutable`.
+ - `curl -sI https://openvibe.network/shared/v1/footer.js` returns 200.
+ - `curl -sI https://openvibe.network/shared/auth-client.js` returns 404, or the SPA fallback.
+ - A Media, Tools or Community page shows the navbar icons, and DevTools shows
+ `nav-icons.js?v=…` loaded after first paint.
+
+---
+
+## OpenVibe.Live
+
+### References
+
+```
+package.json:47 "openvibe-shared": "file:./vendor/openvibe-shared",
+package-lock.json:29,2832,3779 lockfile entries
+server/index.js:446 () => path.resolve(__dirname, '../vendor/openvibe-shared'),
+server/index.js:449-450 OpenVibeApp/packages fallbacks
+scripts/perf/check-budgets.js:32,36 budget check reads vendor/ by path
+test/features-registry.test.js:24 fileFor() reads vendor/ by path
+public/js/ov-navbar-live.js:1 comment
+public/js/app.js:1483 comment
+AGENTS.md:59 README.md:21 SETUP.md:14,30 docs/architecture.md:12 SECURITY_AUDIT.md:98 docs
+.gitignore:31-32 leftover "public/shared/" (nothing writes it any more)
+```
+
+`server/index.js:124` (`require('openvibe-shared/analytics')`) and `server/config.js:9`
+(`require('openvibe-shared/url-resolver')`) need no change.
+
+### Steps
+
+1. Run `rm -rf node_modules/openvibe-shared && git rm -r vendor/openvibe-shared`.
+2. Edit `package.json`, then run `npm install`:
+
+```diff
+- "openvibe-shared": "file:./vendor/openvibe-shared",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",
+```
+
+3. Update `server/index.js`, currently lines 434–505. The package lists its own browser files,
+ and that list includes the new `nav-icons.js`. The server serves them from
+ `node_modules/openvibe-shared` with no candidate search:
+
+```diff
+ // ── Static Files ─────────────────────────────────────────────
+-// Serve openvibe-shared browser assets at /shared/ — serve directly from the
+-// resolved source directory to avoid write operations (the public/ directory
+-// is read-only under systemd ProtectSystem=strict on production).
+-const SHARED_BROWSER_FILES = ['theme-loader.js', 'notification-ui.js', 'account-switcher.js', 'openvibe-sw.js', 'footer.js', 'ov-mark.js', 'tooltip.js', 'history.js', 'navbar.js', 'sso-client.js', 'ov-icons.js', 'island.js', 'ui.js', 'panels.js'];
+-let sharedServePath = null;
+-
+-(function resolveSharedAssets() {
+- const candidates = [
+- // The vendored copy is the source of truth (synced from OpenVibe.Network). On the
+- // server node_modules/openvibe-shared is a stale COPY, not a symlink, so it must
+- // never win over vendor/.
+- () => path.resolve(__dirname, '../vendor/openvibe-shared'),
+- () => path.dirname(require.resolve('openvibe-shared/package.json')),
+- () => path.resolve(__dirname, '../node_modules/openvibe-shared'),
+- () => path.resolve(__dirname, '..', '..', 'OpenVibeApp', 'packages', 'openvibe-shared'),
+- () => path.resolve(__dirname, '..', '..', 'packages', 'openvibe-shared'),
+- ];
+- for (const getPath of candidates) {
+- try {
+- const p = getPath();
+- if (p && fs.existsSync(p)) {
+- // Verify at least one required browser file exists
+- const found = SHARED_BROWSER_FILES.filter(f => fs.existsSync(path.join(p, f)));
+- if (found.length > 0) {
+- sharedServePath = p;
+- assets.setSharedDir(p);
+- console.log(`[Server] /shared: serving ${found.length}/${SHARED_BROWSER_FILES.length} browser file(s) from ${p}`);
+- const missing = SHARED_BROWSER_FILES.filter(f => !found.includes(f));
+- if (missing.length) console.warn(`[Server] /shared: missing files: ${missing.join(', ')}`);
+- return;
+- }
+- }
+- } catch (_) {}
+- }
+- console.error('[Server] /shared: openvibe-shared source not found — shared browser assets will return 404');
+-})();
+-
+-if (sharedServePath) {
++// Serve openvibe-shared browser assets at /shared/ — straight from node_modules/openvibe-shared,
++// the OpenVibe.Shared release package.json pins (the public/ directory is read-only under
++// systemd ProtectSystem=strict on production). The package lists its own browser files.
++const sharedFiles = require('openvibe-shared/files');
++const SHARED_BROWSER_FILES = sharedFiles.BROWSER;
++const sharedServePath = sharedFiles.dir;
++assets.setSharedDir(sharedServePath);
++console.log(`[Server] /shared: serving ${SHARED_BROWSER_FILES.length} browser file(s) from ${sharedServePath}`);
++
++{
+ // Web-push service worker must be same-origin with scope "/" → expose it at the root.
+ app.get('/openvibe-sw.js', (req, res) => {
+@@ (lines 474-499 unchanged: /openvibe-sw.js, whitelist, ?v= immutable, sendFile)
+-} else {
+- console.error('[Server] /shared: serving unavailable — shared browser assets will return 404');
+- app.use('/shared', (req, res) => {
+- res.status(404).type('text/plain').send('Shared assets unavailable');
+- });
+ }
+```
+
+ The unchanged middle already makes `nav-icons.js?v=<hash>` immutable, because
+ `req.query.v === assets.hashOf('/shared/nav-icons.js')`. navbar.js asks for exactly that
+ hash (`NAV_ICONS_REV`, sha256 truncated to 12 hex characters, the same as `assets.js`).
+ `user-card.js` is now served too. It is public on the Network already.
+
+4. Update `scripts/perf/check-budgets.js`:
+
+```diff
+ const assets = require(path.join(ROOT, 'server/web/assets'));
+-try { assets.setSharedDir(path.join(ROOT, 'vendor/openvibe-shared')); } catch { /* */ }
++const SHARED_DIR = require('openvibe-shared/files').dir;
++try { assets.setSharedDir(SHARED_DIR); } catch { /* */ }
+ const html = assets.renderRoute(assets.document('index.html').html, '/');
+ const fileFor = (url) => {
+ const p = url.split('?')[0];
+- return p.startsWith('/shared/') ? path.join(ROOT, 'vendor/openvibe-shared', path.basename(p)) : path.join(ROOT, 'public', p);
++ return p.startsWith('/shared/') ? path.join(SHARED_DIR, path.basename(p)) : path.join(ROOT, 'public', p);
+ };
+```
+
+ Expected after v1.0.0: `eagerJsBrotliKB` goes from 263.8 (failing) to **252.4 / 260**, and
+ `eagerJsRawKB` from 1147.1 to 1113.9. This was measured with the v1.0.0 `navbar.js`
+ substituted into the budget script.
+
+5. Update `test/features-registry.test.js:24`:
+
+```diff
+-const fileFor = (url) => url.startsWith('/shared/') ? path.join(ROOT, 'vendor/openvibe-shared', path.basename(url)) : path.join(PUB, url);
++const fileFor = (url) => url.startsWith('/shared/') ? require('openvibe-shared/files').path(path.basename(url)) : path.join(PUB, url);
+```
+
+ `files.path()` throws for a non-browser file. That is stricter than before, and correct,
+ because a feature must not reference one.
+
+6. Update comments and docs:
+ - `public/js/ov-navbar-live.js:1`: "(vendor/openvibe-shared/navbar.js, served at
+ /shared/navbar.js)" becomes "(openvibe-shared/navbar.js from OpenVibe.Shared, served at
+ /shared/navbar.js)".
+ - `public/js/app.js:1483`: "(vendor/openvibe-shared/panels.js" becomes
+ "(openvibe-shared/panels.js".
+ - `AGENTS.md:59`: "**openvibe-shared:** Pinned release of OpenVibers/OpenVibe.Shared
+ (`"openvibe-shared": "https://codeload…/refs/tags/vX.Y.Z"`), served at `/shared/*` from
+ `node_modules`. Change it there and bump the tag; never edit `node_modules`."
+ - `README.md:21`, `SETUP.md:14,30` and `docs/architecture.md:12`: same wording.
+ - `SECURITY_AUDIT.md:98`: `vendor/openvibe-shared/analytics.js` becomes
+ `openvibe-shared/analytics.js`, with remediation "change in OpenVibe.Shared, release, bump
+ the pin".
+ - `.gitignore:31-32`: delete the leftover `public/shared/` lines. This is optional.
+7. Deploy. `deploy/scripts/deploy.sh` treats `package.json` and `package-lock.json` as
+ "deps + server", so the release runs `npm ci --omit=dev`, which installs the tarball, and
+ then restarts. A restart drops live RTMP publishers, so check `/api/streams` first and use
+ `deploy.sh --wait-idle`.
+8. Verify:
+ - `npm test` passes, including `perf:budget`.
+ - `curl -sI https://openvibe.live/shared/nav-icons.js?v=<hash>` returns an immutable 200.
+ - The home page's navbar icons render. `nav-icons.js` loads after first paint.
+
+---
+
+## OpenVibe.Community
+
+### References
+
+```
+package.json:25 "openvibe-shared": "file:./vendor/openvibe-shared"
+package-lock.json:21,1075,1567 lockfile entries
+README.md:203 "vendor/openvibe-shared unmodified copy of OpenVibe.Network/packages/openvibe-shared"
+```
+
+Browsers load `https://openvibe.network/shared/{theme-loader,navbar,footer}.js` absolutely (the
+`NETWORK_URL` constant at `server/render/layout.js:16`, used at lines 120–125). Server requires
+are `server/app.js:82` (legal), `server/auth/routes.js:37` (auth-client) and
+`server/render/layout.js:118,133` (app-icon, footer). Neither needs a change.
+
+The vendored copy's tracked `test/` files never ran and are deleted with it.
+
+### Steps
+
+1. Run `rm -rf node_modules/openvibe-shared && git rm -r vendor/openvibe-shared`.
+2. Edit `package.json`, then run `npm install`:
+
+```diff
+- "openvibe-shared": "file:./vendor/openvibe-shared"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"
+```
+
+3. `README.md:203`: the line becomes
+ `node_modules/openvibe-shared pinned OpenVibe.Shared release (package.json); never edit`.
+ README line 173 ("serve the current `openvibe-shared`…") still reads correctly.
+4. `npm test`. `test/ssr.test.js:19-21` asserts the absolute Network URLs, which are unchanged.
+ Community's CI (`.github/workflows/ci.yml`, `npm ci`) fetches the tarball from codeload.
+5. There is no deploy script. On the host, `git pull && npm ci --omit=dev && systemctl restart openvibe-community`.
+
+---
+
+## OpenVibe.Media
+
+### References
+
+```
+package.json:22 "openvibe-shared": "file:./vendor/openvibe-shared",
+package-lock.json:20,1973,2587 lockfile entries
+README.md:42 "vendor/openvibe-shared/ vendored shared helpers (do not edit; re-sync from canonical)"
+```
+
+Browsers load `${NETWORK_URL}/shared/{theme-loader,navbar,footer}.js`
+(`server/public/page-chrome.js:14,82,146-147`) and
+`https://openvibe.network/shared/theme-loader.js` (`server/public/browse.js:240`). Server
+requires are `server/index.js:42`, `server/user-auth.js:37`,
+`server/public/browse.js:269,271,276` and `server/public/page-chrome.js:81,187,191`. None of them
+needs a change.
+
+### Steps
+
+1. Run `rm -rf node_modules/openvibe-shared && git rm -r vendor/openvibe-shared`.
+2. Edit `package.json`, then run `npm install`:
+
+```diff
+- "openvibe-shared": "file:./vendor/openvibe-shared",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",
+```
+
+3. `README.md:42`: the line becomes
+ `node_modules/openvibe-shared/ pinned OpenVibe.Shared release (package.json); never edit`.
+4. `npm test`. `test/public-pages.test.js:40,42` asserts the absolute Network URLs, which are
+ unchanged.
+5. Deploy: pull, `npm ci --omit=dev`, restart. This picks up the ov-icons centring fix from
+ `docs/divergence-2026-09-22.md` for server-side icons.
+
+---
+
+## OpenVibe.Tools
+
+Tools has no root dependency. Each app under `apps/*` has its own `package.json`, and
+`deploy/scripts/deploy.sh` rsyncs `vendor/openvibe-shared` into every app's
+`node_modules/openvibe-shared`, because npm copies `file:` dependencies on the host. After the
+change, each app installs its own copy of the tarball, and npm's cache means it is downloaded
+once. There is no rsync and no symlink.
+
+### References
+
+```
+package.json:7 "install:all": "npm --prefix vendor/openvibe-shared install && npm --prefix apps/gateway install && …"
+deploy/scripts/deploy.sh:2-3,11 header comment + rsync of vendor/openvibe-shared into each app
+apps/gateway/server/index.js:282 res.sendFile(path.resolve(__dirname, '..', '..', '..', 'vendor', 'openvibe-shared', 'openvibe-sw.js'));
+apps/audio/package.json:21 "openvibe-shared": "file:../../vendor/openvibe-shared",
+apps/docs/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared",
+apps/food/package.json:19 "openvibe-shared": "file:../../vendor/openvibe-shared"
+apps/gateway/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared"
+apps/img/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared",
+apps/maps/package.json:14 "openvibe-shared": "file:../../vendor/openvibe-shared",
+apps/text/package.json:19 "openvibe-shared": "file:../../vendor/openvibe-shared"
+apps/yt/package.json:20 "openvibe-shared": "file:../../vendor/openvibe-shared",
+apps/gateway/package-lock.json:19,22,752 lockfile entries
+apps/maps/package-lock.json:22,25,1287 lockfile entries
+apps/text/package-lock.json:19,22,873 lockfile entries
+README.md:26 "└── openvibe-shared # vendored shared package (canonical copy lives in OpenVibe.Network)"
+apps/audio/README.md:34,52 "openvibe-shared package (vendored at `vendor/openvibe-shared`)"
+```
+
+Browser pages load `https://openvibe.network/shared/*` absolutely: `apps/*/public/index.html`,
+gateway `public/{audio,img,dev,net}.html`, `server/pages/site.js:120`, and 31
+`apps/text/public/*.html` pages. They need no change. Server requires such as
+`openvibe-shared/analytics` in every app, and gateway's brand, legal, auth-client, app-icon,
+icons, seo and footer, need no change either.
+
+### Steps
+
+1. `git rm -r vendor/openvibe-shared`. The untracked `vendor/openvibe-shared/node_modules` left
+ by `install:all` goes too: `rm -rf vendor`.
+2. In each of the 8 app `package.json` files, keep the trailing comma where the file has one:
+
+```diff
+- "openvibe-shared": "file:../../vendor/openvibe-shared",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0",
+```
+
+ Then run
+ `for a in apps/*/; do [ -f "$a/package.json" ] && (cd "$a" && rm -rf node_modules/openvibe-shared && npm install --no-audit --no-fund); done`.
+ That regenerates the three tracked lockfiles (gateway, maps and text).
+3. Update the root `package.json:7`:
+
+```diff
+- "install:all": "npm --prefix vendor/openvibe-shared install && npm --prefix apps/gateway install && npm --prefix apps/maps install && …",
++ "install:all": "npm --prefix apps/gateway install && npm --prefix apps/maps install && …",
+```
+
+ Only the leading `npm --prefix vendor/openvibe-shared install && ` is removed.
+
+4. Update `deploy/scripts/deploy.sh`:
+
+```diff
+ #!/usr/bin/env bash
+-# Deploy OpenVibe.Tools on the host: pull, refresh the vendored shared package inside each app's
+-# node_modules (npm copies file: dependencies on this host, so a pull alone leaves them stale),
+-# install new dependencies when a package.json changed, restart the units, check health.
++# Deploy OpenVibe.Tools on the host: pull, install dependencies in each app whose package.json
++# changed (that includes a new openvibe-shared release tag), restart the units, check health.
+ set -euo pipefail
+ cd "$(dirname "$0")/../.."
+ BEFORE=$(git rev-parse HEAD); git pull -q --ff-only; AFTER=$(git rev-parse HEAD)
+ for app in apps/*/; do
+ [ -f "$app/package.json" ] || continue
+ if git diff --name-only "$BEFORE" "$AFTER" -- "$app/package.json" | grep -q . || [ ! -d "$app/node_modules" ]; then (cd "$app" && npm install --omit=dev --no-audit --no-fund --silent); fi
+- d="$app/node_modules/openvibe-shared"; [ -L "$d" ] || { mkdir -p "$d"; rsync -a --delete --exclude node_modules vendor/openvibe-shared/ "$d/"; }
+ done
+```
+
+ The first deploy after this change touches every app's `package.json`, so every app runs
+ `npm install`. That replaces the rsynced real-directory copy with the tarball. If an app's
+ `node_modules/openvibe-shared` is a leftover symlink into `vendor/`, delete it before that
+ deploy.
+
+5. Update `apps/gateway/server/index.js:282`:
+
+```diff
+- res.sendFile(path.resolve(__dirname, '..', '..', '..', 'vendor', 'openvibe-shared', 'openvibe-sw.js'));
++ res.sendFile(require('openvibe-shared/files').path('openvibe-sw.js'));
+```
+
+6. Update the docs. `README.md:26` becomes `(openvibe-shared is a pinned OpenVibe.Shared
+ release in each app's package.json; no vendor/ copy)`, and `apps/audio/README.md:34,52`
+ becomes "openvibe-shared (pinned OpenVibe.Shared release)". `README.md:116` ("Browser pages
+ load shared JS absolutely from `https://openvibe.network/shared/`") stays true.
+7. Verify with `npm --prefix apps/gateway test` and `npm --prefix apps/yt test`. Then run
+ `node -e "require('openvibe-shared/icons')"` in each app directory. After deploy, check that
+ `/api/health` is healthy and that gateway ring icons are centred. That comes from the
+ ov-icons fix Tools lacked.
+
+---
+
+## OpenVibe.Sites (optional, no package.json today)
+
+### References
+
+```
+build.js:16 comment "Vendored from OpenVibe.Network/packages/openvibe-shared (copy, never edit)"
+build.js:17 const legal = require('./vendor/openvibe-shared/legal');
+build.js:63-64 require('./vendor/openvibe-shared/app-icon').headTags(…) / .CRITICAL
+build.js:193 require('./vendor/openvibe-shared/footer').ssr(…)
+build.js:210 require('./vendor/openvibe-shared/app-icon').manifest(…)
+vendor/openvibe-shared/{app-icon,chrome-ssr,footer,legal,seo}.js (5-file subset, byte-identical to v1.0.0)
+```
+
+### Steps
+
+1. Add `package.json`:
+
+```json
+{
+ "name": "openvibe-sites",
+ "private": true,
+ "description": "Placeholder and hub pages for the planned OpenVibe domains (static build)",
+ "scripts": { "build": "node build.js" },
+ "dependencies": {
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.0.0"
+ }
+}
+```
+
+ Run `npm install` and commit `package-lock.json`. `.gitignore` already lists `node_modules/`.
+
+2. Update `build.js`:
+
+```diff
+-// Vendored from OpenVibe.Network/packages/openvibe-shared (copy, never edit): the network's legal documents.
+-const legal = require('./vendor/openvibe-shared/legal');
++// The network's legal documents, from the pinned OpenVibe.Shared release (package.json).
++const legal = require('openvibe-shared/legal');
+@@
+-${require('./vendor/openvibe-shared/app-icon').headTags({ site: 'network' }).split('\n')[0]}
+-${require('./vendor/openvibe-shared/app-icon').CRITICAL}
++${require('openvibe-shared/app-icon').headTags({ site: 'network' }).split('\n')[0]}
++${require('openvibe-shared/app-icon').CRITICAL}
+@@
+-${require('./vendor/openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })}
++${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'full' })}
+@@
+-function manifest(site) { return JSON.stringify(require('./vendor/openvibe-shared/app-icon').manifest({ …
++function manifest(site) { return JSON.stringify(require('openvibe-shared/app-icon').manifest({ …
+```
+
+3. Run `git rm -r vendor/openvibe-shared`, then `node build.js`. `git status dist/` must show no
+ changes, because the five modules are byte-identical to v1.0.0.
+4. Update `deploy/scripts/deploy.sh`: add `npm ci --omit=dev --no-audit --no-fund` before
+ `node build.js`. The host needs outbound HTTPS to codeload.github.com.
+
+The published `dist/` HTML hard-codes `https://openvibe.network/shared/navbar.js`. That is one
+reason the Network keeps `/shared/<file>` on the v1 line (README, compatibility policy).
+
+---
+
+## After all consumers moved
+
+- `git grep -n 'vendor/openvibe-shared'` is empty in every repo, apart from historical docs.
+ `find ~/OpenVibers -path '*/vendor/openvibe-shared' -type d` finds nothing.
+- Every future shared change follows the same path: a PR to OpenVibe.Shared, a `vX.Y.Z` tag,
+ then a pin bump in Network first and in the other consumers after.