openvibers/openvibe.games Commits

The game pages run their own pinned OpenVibe Frame (roadmap WS-P task 4, D42: the Games client). The server gains openvibe-shared 1.23.0 and serves its browser files at /shared (apps/server/src/net/sharedAssets.ts: only the files openvibe-shared/files lists, content-addressed with ?v= for a year, else five minutes, ETag and 304, the same rules as openvibe-shared/serve, which is Express-shaped while this server is plain node:http), next to /release.json. index.html, play.html and editor.html load theme-loader, navbar and footer from /shared/ instead of https://openvibe.network/shared/, so they run what Games pins and keep their frame while Network is down (Vite leaves the classic script tags as they are). Test: the pinned navbar with its hash and ETag, only listed files, and no page loading the frame from openvibe.network. pnpm test: 79 files, 866 tests.

OP OpenVibers committed 27 Sept 2026 · 8200c2cf1b · 9 files, +224 −12


Changes

ddiff --git a/apps/client/editor.html b/apps/client/editor.html
iindex 9fa8f2b..68e1afd 100644
---- a/apps/client/editor.html
++++ b/apps/client/editor.html
@@@ -23,9 +23,9 @@
<meta property="og:image" content="https://openvibe.games/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
-- <script src="https://openvibe.network/shared/theme-loader.js"></script>
-- <script src="https://openvibe.network/shared/navbar.js" defer></script>
-- <script src="https://openvibe.network/shared/footer.js" defer></script>
++ <script src="/shared/theme-loader.js"></script>
++ <script src="/shared/navbar.js" defer></script>
++ <script src="/shared/footer.js" defer></script>
<script>
window.__ovgFrame = { page: 'editor', collapsible: true, toggleFromStart: true }
</script>
ddiff --git a/apps/client/index.html b/apps/client/index.html
iindex 6f6d402..85e6c0e 100644
---- a/apps/client/index.html
++++ b/apps/client/index.html
@@@ -11,9 +11,9 @@
/>
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
<!-- Deferred: a slow or unreachable openvibe.network never holds the first paint (the inline colours above cover it). -->
-- <script src="https://openvibe.network/shared/theme-loader.js" defer></script>
-- <script src="https://openvibe.network/shared/navbar.js" defer></script>
-- <script src="https://openvibe.network/shared/footer.js" defer></script>
++ <script src="/shared/theme-loader.js" defer></script>
++ <script src="/shared/navbar.js" defer></script>
++ <script src="/shared/footer.js" defer></script>
<link rel="canonical" href="https://openvibe.games/" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
@@@ -508,7 +508,7 @@
<noscript><p><a href="https://openvibe.network/updates?site=games">What shipped recently</a></p></noscript>
</section>
-- <!-- Shared cross-site footer (openvibe.network/shared/footer.js): network links, legal, signed-in row. -->
++ <!-- Shared cross-site footer (/shared/footer.js, this site's pinned copy): network links, legal, signed-in row. -->
<footer id="ov-footer" class="ovf"></footer>
<script>
ddiff --git a/apps/client/play.html b/apps/client/play.html
iindex 8fdfdab..a658e6e 100644
---- a/apps/client/play.html
++++ b/apps/client/play.html
@@@ -10,9 +10,9 @@
content="Play Scraplandia: scrappy multiplayer survival-building in and around Scrap City, in your browser, no download. Part of the OpenVibe network."
/>
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
-- <script src="https://openvibe.network/shared/theme-loader.js"></script>
-- <script src="https://openvibe.network/shared/navbar.js" defer></script>
-- <script src="https://openvibe.network/shared/footer.js" defer></script>
++ <script src="/shared/theme-loader.js"></script>
++ <script src="/shared/navbar.js" defer></script>
++ <script src="/shared/footer.js" defer></script>
<link rel="canonical" href="https://play.openvibe.games/" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
ddiff --git a/apps/client/public/frame.js b/apps/client/public/frame.js
iindex 108b349..6719295 100644
---- a/apps/client/public/frame.js
++++ b/apps/client/public/frame.js
@@@ -1,8 +1,8 @@
/**
* OpenVibe.Games — the OpenVibe Frame (navbar + footer) on every page.
*
-- * The navbar and footer themselves come from openvibe.network/shared/*.js,
-- * loaded by each page's <head>; this file only tells them who we are: the
++ * The navbar and footer themselves come from this site's own pinned copy at
++ * /shared/*.js (served by the game server, D42), loaded by each page's <head>; this file only tells them who we are: the
* site's links, where sign-in/sign-out live, and the session the game keeps
* in the `ovg_sso` cookie / localStorage entry (set by /auth/callback).
*
ddiff --git a/apps/server/package.json b/apps/server/package.json
iindex 83b23a5..b64313c 100644
---- a/apps/server/package.json
++++ b/apps/server/package.json
@@@ -21,6 +21,7 @@
"better-sqlite3": "^11.9.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0",
"tsx": "^4.19.3",
"ws": "^8.18.1"
},
ddiff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
iindex 23375ee..c3a231d 100644
---- a/apps/server/src/main.ts
++++ b/apps/server/src/main.ts
@@@ -29,6 +29,7 @@ import { attachWebSocket } from './net/wsTransport.js'
import { ServerMetrics } from './observability/metrics.js'
import { createReadiness } from './observability/readiness.js'
import { buildRelease, releaseHandler } from './observability/release.js'
++import { sharedAssetsHandler } from './net/sharedAssets.js'
import { ModRegistry } from './mods/registry.js'
import { handleModsRequest } from './mods/routes.js'
import { ModRuntime } from './mods/runtime.js'
@@@ -206,6 +207,8 @@ async function main(): Promise<void> {
// GET /release.json (D43): the deployed commit and package versions, as every OpenVibe service serves it.
const serveRelease = releaseHandler(buildRelease(process.cwd()))
++ // GET /shared/*: this server's own pinned OpenVibe Frame files (D42), not openvibe.network's.
++ const serveShared = sharedAssetsHandler()
// GET /api/ready: world.db answers and the simulation ticks (/healthz stays liveness only).
const schemaVersion = store.db.prepare('SELECT value FROM meta WHERE key = ?')
@@@ -270,6 +273,7 @@ async function main(): Promise<void> {
if (revocations.handle(req, res)) return true
if (readiness.handle(req, res)) return true
if (serveRelease(req, res)) return true
++ if (serveShared(req, res)) return true
if ((req.url ?? '').split('?')[0] === '/api/v1/platform' && req.method === 'GET') {
// Operational status of the platform adapters; never secrets.
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
ddiff --git a/apps/server/src/net/sharedAssets.test.ts b/apps/server/src/net/sharedAssets.test.ts
nnew file mode 100644
iindex 0000000..6b91f89
---- /dev/null
++++ b/apps/server/src/net/sharedAssets.test.ts
@@@ -0,0 +1,58 @@
++import { createServer, type Server } from 'node:http'
++import type { AddressInfo } from 'node:net'
++import { createRequire } from 'node:module'
++import { readFileSync } from 'node:fs'
++import { dirname, join } from 'node:path'
++import { fileURLToPath } from 'node:url'
++import { afterAll, beforeAll, describe, expect, it } from 'vitest'
++import { sharedAssetsHandler } from './sharedAssets.js'
++
++// D42 (roadmap WS-P task 4): the game server serves its own pinned OpenVibe Frame files at /shared, and the
++// client pages load them from there, never from openvibe.network.
++const req = createRequire(import.meta.url)
++const serve = req('openvibe-shared/serve') as { hashOf(n: string): string }
++let server: Server
++let base = ''
++
++beforeAll(async () => {
++ const handle = sharedAssetsHandler()
++ server = createServer((rq, res) => {
++ if (handle(rq, res)) return
++ res.writeHead(404)
++ res.end('not shared')
++ })
++ await new Promise<void>((r) => server.listen(0, '127.0.0.1', () => r()))
++ base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`
++})
++afterAll(() => new Promise<void>((r) => server.close(() => r())))
++
++describe('/shared (D42)', () => {
++ it('serves the pinned navbar, content-addressed, with an ETag', async () => {
++ const hash = serve.hashOf('navbar.js')
++ let r = await fetch(`${base}/shared/navbar.js?v=${hash}`)
++ expect(r.status).toBe(200)
++ expect(r.headers.get('content-type')).toMatch(/javascript/)
++ expect(r.headers.get('cache-control')).toMatch(/immutable/)
++ expect((await r.text()).length).toBeGreaterThan(1000)
++ r = await fetch(`${base}/shared/navbar.js`)
++ expect(r.headers.get('cache-control')).toMatch(/max-age=300/)
++ r = await fetch(`${base}/shared/navbar.js`, { headers: { 'if-none-match': `"${hash}"` } })
++ expect(r.status).toBe(304)
++ })
++
++ it('serves only the browser files openvibe-shared lists', async () => {
++ for (const p of ['/shared/serve.js', '/shared/../package.json', '/shared/', '/other/navbar.js']) {
++ expect((await fetch(base + p)).status).toBe(404)
++ }
++ expect((await fetch(`${base}/shared/navbar.js`, { method: 'POST' })).status).toBe(404)
++ })
++
++ it('the client pages load the frame from /shared, not openvibe.network', () => {
++ const client = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'client')
++ for (const page of ['index.html', 'play.html', 'editor.html']) {
++ const html = readFileSync(join(client, page), 'utf8')
++ expect(html).not.toMatch(/https:\/\/openvibe\.network\/shared\/[\w.-]+\.js/)
++ expect(html).toMatch(/src="\/shared\/navbar\.js"/)
++ }
++ })
++})
ddiff --git a/apps/server/src/net/sharedAssets.ts b/apps/server/src/net/sharedAssets.ts
nnew file mode 100644
iindex 0000000..7f0ff50
---- /dev/null
++++ b/apps/server/src/net/sharedAssets.ts
@@@ -0,0 +1,57 @@
++import { createReadStream } from 'node:fs'
++import type { IncomingMessage, ServerResponse } from 'node:http'
++import { createRequire } from 'node:module'
++
++/**
++ * GET /shared/<file>: this server's own pinned copy of the OpenVibe Frame's browser files (navbar,
++ * footer, theme loader…), roadmap WS-P task 4 / D42. The pages run what Games pins (openvibe-shared in
++ * apps/server/package.json), not whatever openvibe.network happens to run, and keep their frame while
++ * the Network is down. Only the files openvibe-shared/files lists; a request carrying the file's content
++ * hash (?v=) is cached for a year, anything else for five minutes; ETag = the hash. Same rules as
++ * openvibe-shared/serve, which is Express-shaped (this server is plain node:http).
++ */
++interface SharedFiles {
++ isBrowserFile(name: string): boolean
++ path(name: string): string
++}
++interface SharedServe {
++ hashOf(name: string): string
++}
++
++export function sharedAssetsHandler(req: NodeRequire = createRequire(import.meta.url)) {
++ const files = req('openvibe-shared/files') as SharedFiles
++ const serve = req('openvibe-shared/serve') as SharedServe
++ return (rq: IncomingMessage, res: ServerResponse): boolean => {
++ if (rq.method !== 'GET' && rq.method !== 'HEAD') return false
++ const url = new URL(rq.url ?? '/', 'http://games.local')
++ if (!url.pathname.startsWith('/shared/')) return false
++ const name = url.pathname.slice('/shared/'.length)
++ if (!files.isBrowserFile(name)) return false
++ const hash = serve.hashOf(name)
++ const headers = {
++ 'content-type': 'application/javascript; charset=utf-8',
++ 'cache-control':
++ url.searchParams.get('v') === hash
++ ? 'public, max-age=31536000, immutable'
++ : 'public, max-age=300, stale-while-revalidate=60',
++ etag: `"${hash}"`,
++ 'access-control-allow-origin': '*',
++ 'cross-origin-resource-policy': 'cross-origin',
++ 'x-content-type-options': 'nosniff',
++ }
++ if (rq.headers['if-none-match'] === `"${hash}"`) {
++ res.writeHead(304, headers)
++ res.end()
++ return true
++ }
++ res.writeHead(200, headers)
++ if (rq.method === 'HEAD') {
++ res.end()
++ return true
++ }
++ createReadStream(files.path(name))
++ .on('error', () => res.destroy())
++ .pipe(res)
++ return true
++ }
++}
ddiff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
iindex 4b81b98..19cb7f7 100644
---- a/pnpm-lock.yaml
++++ b/pnpm-lock.yaml
@@@ -117,6 +117,9 @@ importers:
openvibe-sdk:
specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
version: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
++ openvibe-shared:
++ specifier: https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0
++ version: https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0
tsx:
specifier: ^4.19.3
version: 4.23.11
@@@ -963,6 +966,9 @@ packages:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
++ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
++
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@@ -1042,6 +1048,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
++ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
++
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
@@@ -1261,6 +1270,16 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
++ resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
++ engines: {node: '>=12', npm: '>=6'}
++
++ resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
++
++ resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
++
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@@ -1272,9 +1291,30 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
++ resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
++
++ resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
++
++ resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
++
++ resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
++
++ resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
++
++ resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
++
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
++ resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
++
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@@ -1337,6 +1377,10 @@ packages:
version: 0.11.0
engines: {node: '>=20'}
++ openvibe-shared@https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0:
++ resolution: {tarball: https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0}
++ version: 1.23.0
++
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@@ -2352,6 +2396,8 @@ snapshots:
dependencies:
balanced-match: 4.0.4
++
dependencies:
base64-js: 1.5.1
@@@ -2422,6 +2468,10 @@ snapshots:
++ dependencies:
++ safe-buffer: 5.2.1
++
dependencies:
once: 1.4.0
@@@ -2687,6 +2737,30 @@ snapshots:
++ dependencies:
++ jws: 4.0.1
++ lodash.includes: 4.3.0
++ lodash.isboolean: 3.0.3
++ lodash.isinteger: 4.0.4
++ lodash.isnumber: 3.0.3
++ lodash.isplainobject: 4.0.6
++ lodash.isstring: 4.0.1
++ lodash.once: 4.1.1
++ ms: 2.1.3
++ semver: 7.8.5
++
++ dependencies:
++ buffer-equal-constant-time: 1.0.1
++ ecdsa-sig-formatter: 1.0.11
++ safe-buffer: 5.2.1
++
++ dependencies:
++ jwa: 2.0.1
++ safe-buffer: 5.2.1
++
dependencies:
json-buffer: 3.0.1
@@@ -2700,8 +2774,22 @@ snapshots:
dependencies:
p-locate: 5.0.0
++
++
++
++
++
++
++
@@@ -2749,6 +2837,10 @@ snapshots:
openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0: {}
++ openvibe-shared@https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.23.0:
++ dependencies:
++ jsonwebtoken: 9.0.3
++
dependencies:
deep-is: 0.1.4