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.
Changes
@@@ -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>@@@ -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>@@@ -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" />@@@ -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).*@@@ -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"},@@@ -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 trueif (readiness.handle(req, res)) return trueif (serveRelease(req, res)) return true++ if (serveShared(req, res)) return trueif ((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' })nnew file mode 100644@@@ -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"/)++ }++ })++})nnew file mode 100644@@@ -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++ }++}@@@ -117,6 +117,9 @@ importers:openvibe-sdk:specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0version: 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.0tsx:specifier: ^4.19.3version: 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.0engines: {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++ [email protected]: {}++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++ [email protected]: {}++++ [email protected]: {}++++ [email protected]: {}++++ [email protected]: {}++++ [email protected]: {}++++ [email protected]: {}++++ [email protected]: {}++@@@ -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
@@ -23,9 +23,9 @@
@@ -23,9 +23,9 @@
<meta property="og:image" content="https://openvibe.games/og-image.png" />
<meta property="og:image" content="https://openvibe.games/og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:card" content="summary_large_image" />
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
<!-- 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>
<script>
window.__ovgFrame = { page: 'editor', collapsible: true, toggleFromStart: true }
window.__ovgFrame = { page: 'editor', collapsible: true, toggleFromStart: true }
</script>
</script>
@@ -11,9 +11,9 @@
@@ -11,9 +11,9 @@
/>
/>
<!-- The OpenVibe Frame: the user's theme before first paint, then navbar + footer. -->
<!-- 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). -->
<!-- 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/" />
<link rel="canonical" href="https://openvibe.games/" />
<meta name="robots" content="index, follow" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
<meta property="og:type" content="website" />
@@ -508,7 +508,7 @@
@@ -508,7 +508,7 @@
<noscript><p><a href="https://openvibe.network/updates?site=games">What shipped recently</a></p></noscript>
<noscript><p><a href="https://openvibe.network/updates?site=games">What shipped recently</a></p></noscript>
</section>
</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>
<footer id="ov-footer" class="ovf"></footer>
<script>
<script>
@@ -10,9 +10,9 @@
@@ -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."
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. -->
<!-- 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/" />
<link rel="canonical" href="https://play.openvibe.games/" />
<meta name="robots" content="index, follow" />
<meta name="robots" content="index, follow" />
<meta property="og:type" content="website" />
<meta property="og:type" content="website" />
@@ -1,8 +1,8 @@
@@ -1,8 +1,8 @@
/**
/**
* OpenVibe.Games — the OpenVibe Frame (navbar + footer) on every page.
* 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
* 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).
* in the `ovg_sso` cookie / localStorage entry (set by /auth/callback).
*
*
@@ -21,6 +21,7 @@
@@ -21,6 +21,7 @@
"better-sqlite3": "^11.9.1",
"better-sqlite3": "^11.9.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.53.0",
"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-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",
"tsx": "^4.19.3",
"ws": "^8.18.1"
"ws": "^8.18.1"
},
},
@@ -29,6 +29,7 @@ import { attachWebSocket } from './net/wsTransport.js'
@@ -29,6 +29,7 @@ import { attachWebSocket } from './net/wsTransport.js'
import { ServerMetrics } from './observability/metrics.js'
import { ServerMetrics } from './observability/metrics.js'
import { createReadiness } from './observability/readiness.js'
import { createReadiness } from './observability/readiness.js'
import { buildRelease, releaseHandler } from './observability/release.js'
import { buildRelease, releaseHandler } from './observability/release.js'
+import { sharedAssetsHandler } from './net/sharedAssets.js'
import { ModRegistry } from './mods/registry.js'
import { ModRegistry } from './mods/registry.js'
import { handleModsRequest } from './mods/routes.js'
import { handleModsRequest } from './mods/routes.js'
import { ModRuntime } from './mods/runtime.js'
import { ModRuntime } from './mods/runtime.js'
@@ -206,6 +207,8 @@ async function main(): Promise<void> {
@@ -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.
// GET /release.json (D43): the deployed commit and package versions, as every OpenVibe service serves it.
const serveRelease = releaseHandler(buildRelease(process.cwd()))
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).
// 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 = ?')
const schemaVersion = store.db.prepare('SELECT value FROM meta WHERE key = ?')
@@ -270,6 +273,7 @@ async function main(): Promise<void> {
@@ -270,6 +273,7 @@ async function main(): Promise<void> {
if (revocations.handle(req, res)) return true
if (revocations.handle(req, res)) return true
if (readiness.handle(req, res)) return true
if (readiness.handle(req, res)) return true
if (serveRelease(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') {
if ((req.url ?? '').split('?')[0] === '/api/v1/platform' && req.method === 'GET') {
// Operational status of the platform adapters; never secrets.
// Operational status of the platform adapters; never secrets.
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' })
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -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"/)
+ }
+ })
+})
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -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
+ }
+}
@@ -117,6 +117,9 @@ importers:
@@ -117,6 +117,9 @@ importers:
openvibe-sdk:
openvibe-sdk:
specifier: https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0
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
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:
tsx:
specifier: ^4.19.3
specifier: ^4.19.3
version: 4.23.11
version: 4.23.11
@@ -963,6 +966,9 @@ packages:
@@ -963,6 +966,9 @@ packages:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==}
engines: {node: 20 || >=22}
engines: {node: 20 || >=22}
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
@@ -1042,6 +1048,9 @@ packages:
@@ -1042,6 +1048,9 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
engines: {node: '>=8'}
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
@@ -1261,6 +1270,16 @@ packages:
@@ -1261,6 +1270,16 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
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==}
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1272,9 +1291,30 @@ packages:
@@ -1272,9 +1291,30 @@ packages:
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
engines: {node: '>=10'}
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-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+ resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
+
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
@@ -1337,6 +1377,10 @@ packages:
@@ -1337,6 +1377,10 @@ packages:
version: 0.11.0
version: 0.11.0
engines: {node: '>=20'}
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==}
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
engines: {node: '>= 0.8.0'}
@@ -2352,6 +2396,8 @@ snapshots:
@@ -2352,6 +2396,8 @@ snapshots:
dependencies:
dependencies:
balanced-match: 4.0.4
balanced-match: 4.0.4
+ [email protected]: {}
+
dependencies:
dependencies:
base64-js: 1.5.1
base64-js: 1.5.1
@@ -2422,6 +2468,10 @@ snapshots:
@@ -2422,6 +2468,10 @@ snapshots:
+ dependencies:
+ safe-buffer: 5.2.1
+
dependencies:
dependencies:
once: 1.4.0
once: 1.4.0
@@ -2687,6 +2737,30 @@ snapshots:
@@ -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:
dependencies:
json-buffer: 3.0.1
json-buffer: 3.0.1
@@ -2700,8 +2774,22 @@ snapshots:
@@ -2700,8 +2774,22 @@ snapshots:
dependencies:
dependencies:
p-locate: 5.0.0
p-locate: 5.0.0
+ [email protected]: {}
+
+ [email protected]: {}
+
+ [email protected]: {}
+
+ [email protected]: {}
+
+ [email protected]: {}
+
+ [email protected]: {}
+
+ [email protected]: {}
+
@@ -2749,6 +2837,10 @@ snapshots:
@@ -2749,6 +2837,10 @@ snapshots:
openvibe-sdk@https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.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:
+ dependencies:
+ jsonwebtoken: 9.0.3
+
dependencies:
dependencies:
deep-is: 0.1.4
deep-is: 0.1.4