Commits
-
Signed playback URLs for a VOD or clip, so a reader with no key can read a private recording its owning app asked for: OpenVibe.AI transcribing it (roadmap WS-O task 2). GET /api/v1/:app/vods/:id/signed-url and /clips/:id/signed-url (the owning app's key alone; a call acting for a user gets 403) answer { url, expires_at } for /v/:id or /c/:id with exp and sig. The signature has its own purpose per kind (getv, getc) and id space (vod:<id>, clip:<id>): it is never valid for another recording, another kind or an object. ttl is 30 s to 6 hours (default 1 hour), because a transcript pass reads a long recording window by window. /v and /c accept a valid signature for a private row; anything else still answers exactly like a missing id. test/private-not-found.test.js covers both kinds, other ids, the other kind, a forged signature, acting callers and no key refused, and the 6-hour cap. npm test passes.OpenVibers · 28 Sept 2026
9aac60f -
Per-actor rate limits at Media's capability boundaries (roadmap WS-R task 4, openvibe-sdk v0.12.0 limits). server/actor-limits.js makes one limiter that counts each caller: the Network token's principal (svc:community, app:app_… of a developer project), the person signed in to /api/v2/me, or an app acting for one of its users (X-OV-User-Id, counted per user). An app's own API key is not counted, since that first-party server speaks for every visitor of its site and limits them itself; a browser with only an upload token is counted by address. Past a limit the route answers 429 problem+json rate_limited with Retry-After before any work, logs one [Limits] line and counts media_rate_limited_total{limit,window}.OpenVibers · 28 Sept 2026
1125a3c -
N-1 fixtures re-recorded from a207b1d, the release now in production (npm run n-1:record).OpenVibers · 27 Sept 2026
793fafe -
Media takes part in account export and deletion (roadmap WS-B task 7, ADR-033; openvibe-contracts 0.54.0 -> 0.71.0). POST /internal/events takes network.account.export_requested and network.account.deleted (the boot subscriptions now include both). server/account-data.js applies each once per export or deletion (account_data_events) and answers after Network took the part or confirmation, so a failure is redelivered without erasing twice.OpenVibers · 27 Sept 2026
a207b1d -
N-1 fixtures re-recorded from the release now in production (npm run n-1:record).OpenVibers · 27 Sept 2026
33ce8c9 -
Media follows account merges (roadmap WS-B task 5, ADR-029). network.subject.merged arrives at POST /internal/events (the same signed, loopback-only endpoint as the revocations; ensureSubscription now keeps one subscription per topic) and server/subject-merge.js repoints media_objects.owner_subject from the folded-in subject to the survivor in one transaction, once per merge_id (subject_merges; source network only). VOD and clip rows name Live's user ids and follow Live's own consumer. test/revocations.test.js: both subscriptions created once, the objects moved, a redelivery moves nothing, another source ignored.OpenVibers · 27 Sept 2026
be0facd -
The media index loads its theme loader from Media's own /shared (roadmap WS-P task 4, D42): the last shared script a public page still took from openvibe.network (Media already served /shared; browse.js now uses openvibe-shared/serve.url, content-addressed). The perf budget counts it now that it is same-origin (theme-loader 44.3 KB, 8.3 brotli; 153.4 -> 197.7 KB is exactly that), so jsRawKB 180 -> 215 and jsBrotliKB 43 -> 50; the page did not grow.OpenVibers · 27 Sept 2026
124ad1a -
N-1 fixtures re-recorded from 1157a54, the release now in production (npm run n-1:record).OpenVibers · 27 Sept 2026
743c86f -
The avatar fetch uses openvibe-shared/egress's address rule (roadmap WS-R task 5; low): its own copy had drifted and took the IPv6 forms that wrap an internal IPv4 address (v4-compatible ::127.0.0.1, 6to4 2002:7f00:1::, Teredo), site-local fec0::/10 and a non-canonical ::1 as public, and handed IPv6 literals ([::1]) to DNS instead of judging them. Literals are now checked as addresses and names that can only be internal (localhost, *.local, *.internal, single labels) are refused before DNS; the pinned connection and the per-hop redirect checks are unchanged. test/security-ssrf.test.js: an inventory of every outbound call in server/ (a new one fails until classified; only the avatar fetch takes a user-chosen address, and only app seeding writes a webhook URL); the address rule over loopback, RFC 1918, metadata, CGNAT, 0/8, multicast and their IPv6 wrappings; 38 URLs (decimal, octal, hex and short IPv4, bracketed IPv6, internal names, other schemes, ports, credentials) refused with no DNS lookup or socket; DNS answers with any internal address and redirects to internal hosts refused without connecting; DNS rebinding (the socket's lookup answers the checked address and nothing is resolved again); POST /internal/avatar-ingest storing nothing; and the RTMP pull allow-list refusing encoded, IPv6-mapped and look-alike spellings. Checked by breaking it: the previous guard fails it.OpenVibers · 26 Sept 2026
1157a54 -
A call acting for one user no longer writes another user's media (roadmap WS-R task 5; medium): with the app key and X-OV-User-Id, which is meant to carry that user's permissions, user B could update (visibility included), delete, finalize, feed chunks to, start or stop ingest on and re-cut user A's VODs and clips, replace their thumbnails, place and release retention holds on v2 objects (documented app key only), and reach every admin storage route but holds and ops (bulk delete, tier moves, settings, and GET /holds with staff notes). v1 VOD and clip writes and the thumbnail route now take only the acting user's own rows: someone else's private one answers exactly as a missing id, anything else 403 (the chunk and finalize check used to answer 403 for a private one too); v2 hold writes refuse acting calls with 403 media.hold.forbidden; admin storage refuses them on every route (403 media.admin.forbidden; holds and ops keep their codes). The app key alone is unchanged, and Live does not send X-OV-User-Id on any of these. test/security-idor.test.js swaps ids between two users of an app, two apps (through each tenant), two developer projects and two namespaces of one tenant on every v2 object route (read, download, delete, restore, holds, upload URL, content, complete, multipart start/status/part/complete/abort, lists), upload and multipart tokens of another object or session, jobs (read, create, approve, cancel), namespaces, and v1 VODs, clips, thumbnails, files, assets and admin storage, and checks nothing changed. Checked by breaking it: each of the five previous route files fails it.OpenVibers · 26 Sept 2026
62160a8 -
Private and unlisted recordings' thumbnails are no longer listed on the media index (roadmap WS-R task 5; high): the Thumbnails tab of / and /browse listed every file in the thumbnails directory, so a frame of every private or unlisted VOD and clip was published with its exact file name (which /t/<name> serves, as the capability /api/thumbnails already relies on) and its id, linked to the recording's page. A vod-<id>-/clip-<id>- thumbnail is now listed only while that recording is public (public visibility, is_public, not clips-only); live stream thumbnails and other names as before. test/security-private.test.js extends test/private-not-found.test.js to the paths it did not cover: every tab of the index, /sitemap.xml, /llms.txt and /updates list nothing private or unlisted; the v1 VOD and clip lists and latest-thumbs acting for another user (include_private ignored), the v2 object and job lists; every v2 object read (metadata, download, holds, jobs) by another user, another app through its own tenant and through this one, another developer project and nobody gets exactly the unknown-id answer; /v and /c with another app's key, the /p/:slug page, projected VOD and thumbnail objects on /o; /o and sandbox /f answer an expired, extended, tampered or other object's signature as an unknown id. Checked by breaking it: the previous browse.js fails it.OpenVibers · 26 Sept 2026
385416d -
Internal secrets never leave Media, by test (roadmap WS-R task 5): test/security-secrets.test.js boots the real server/index.js in-process with an obviously fake sentinel for every secret it reads (internal key, signing and view-hash secrets, the inbound events secret, the OAuth client secret, B2/R2 credentials, app keys and a webhook secret from MEDIA_APPS_SEED/MEDIA_APP_KEYS; the list is checked against process.env reads in server/ and .env.example, so a new *_SECRET/*_KEY/*_TOKEN variable fails the test until it has one), plus a VOD's stream key, a throwaway Network signing key, the app keys' hashes and the popularity salt. It walks the Express router stack (145 routes; new ones are covered without editing the test) and requests every GET anonymously, with a wrong key, another app's key, the app key (its admin), acting for a user, a service token, an expired one and as Network staff, every write with a malformed body, and the error paths (unknown routes, ids and tenants, a body over the 2 MB limit, forged upload tokens and signatures, the internal routes, sign-in callbacks, CORS preflight), /api/ready, /healthz, /metrics, /release.json and the admin config reads. No response body or header (plain, URL-encoded or base64 at any alignment), no queued event in event_outbox and no log line carries a sentinel. Checked by breaking it: a base64 copy of MEDIA_B2_APP_KEY in /healthz fails it.OpenVibers · 26 Sept 2026
db4c60c -
Tiering for native v2 objects (roadmap WS-G task 11): popular objects get a verified R2 copy that /o/:id plays from, behind a site-level activation gate that is off by default. Popularity (server/objects/popularity.js, ADR-021): GET /o/:id counts a native object's viewer once per UTC day as HMAC-SHA256(the day's random salt, client network; IPv6 by /64), never an IP address or subject id; the day's hashes and salt are deleted once it is over (first view of the next day, hourly, every sweep), only daily counts stay (30 days); the 7-day figure sums them. Seeks, HEAD, Sec-GPC/DNT, crawlers and sandbox tenants are not counted. Policy: the revisioned namespace media.object_tier (server/objects/tier-policy.js; active=false, promoteMinUniqueViewers7d 500, viewed within 3 days, 16 MB to 4 GB, 3 promotions and 10 demotions per sweep, demote after 14 idle days), validated (types, ranges, min size below max, demoteIdleDays above promoteRecentAccessDays) and served by the admin config routes. Sweep and moves (server/objects/tiering.js), step 4 of the storage sweep, skipped under disk pressure and in a restore drill: with the gate off nothing moves (sweep or direct call) and would-be moves are logged as dry_run; holds refuse every move; only an object whose canonical copy is verified on record is promoted, the canonical copy is re-hashed, copied to R2 and the R2 copy's size and sha256 read back before it is recorded (else deleted, failed, 6 h back-off); a demotion (idle, or no longer ready) re-checks the canonical copy first and never removes the last good copy, and the purge waits for a deleted object's R2 copy. Every decision goes to media_object_tier_decisions with its inputs and the policy in force; GET /admin/storage/tiers/objects/policy and /decisions, /me/ops (native_objects) and media_tier_decisions_24h{target=vod|object} show them. The storage engine now exports uploadFile, which the scheduled verification's default re-upload called but could not reach. test/object-tiering.test.js runs it against a fake S3 through the real SDK.OpenVibers · 26 Sept 2026
43990ec -
N-1 tests (roadmap WS-P task 11): the previous release's clients against this server, and its SQL against this schema. test/n-1.test.js boots this checkout as a real server/index.js in the drill sandbox (MEDIA_DRILL: loopback, no outbound connection, program or background job; test/n-1/preload.js lets writes through) on a database created from the previous release's schema, so Media's migrations run over it, and replays test/fixtures/n-1/client.json inside openvibe-shared's mixed-version matrix (release-compat assertMixedVersion): 64 requests (the openvibe-sdk media and objects clients, anonymously and with an app key as the SDK signs in: files v1, Object API v2 and jobs; the object explorer; and every player URL, link, script and form of the /, /browse, /v, /c, /p, /me and /updates pages that release served), each answered compatibly (any 2xx for a 2xx, the same status otherwise, JSON where there was JSON, redirects to the same place, every response field the client reads with a type it had; a route that answers as an unknown path fails). Then every statement in test/fixtures/n-1/worker.json (331: what that release ran, plus its SQL literals, workers included) must still prepare on the migrated schema, and no old INSERT may miss a new NOT NULL column without a default. Fixtures recorded from b7fbe65, the release in production; refresh after each deploy with `npm run n-1:record [ref]` (a temporary worktree of the release, seeded, booted, replayed). The harness is the file Live, Chat and Community share (test/n-1/harness.js); test/n-1/service.js is Media's part. In npm test, so in CI, no git history or network. Checked by breaking it: renaming /v/:id/transcript.json and /p/:slug/raw fails it.OpenVibers · 26 Sept 2026
44bc3f2 -
Pins: openvibe-shared 1.20.0 -> 1.22.0 (realtime notification badge, off until a site turns it on; the unreachable-host browser check)OpenVibers · 26 Sept 2026
b7fbe65 -
Export tokens pinned (roadmap WS-N task 9): OpenVibe.Codes' project archive lists a project's objects, namespaces and download URLs with Network's read-only export token (sub app:app_<project ULID>, media.object.list + media.object.read, ns [project_id, app.<project_id>.*], env, purpose export). Media takes it unchanged; test/export-tokens.test.js pins that it lists per environment (deleted objects when asked), signs private and sandbox URLs, and writes nothing and reaches no other projectOpenVibers · 26 Sept 2026
777dbf0 -
The index page carries the default theme tokens too (ADR-024): browse.js writes its own <style>, which now starts with openvibe-shared's DEFAULT_VARS (exported from page-frame as DEFAULT_THEME_CSS)OpenVibers · 26 Sept 2026
25ee318 -
Pages define the default theme's tokens (ADR-024, WS-E task 1): the network-down browser run found --accent undefined on Media with openvibe.network unreachable (Media's styles fell back, shared components did not); baseCss now carries openvibe-shared's DEFAULT_VARS on :root, which the theme loader's per-person values overrideOpenVibers · 26 Sept 2026
2be314b -
Open redirect fixed (roadmap WS-R task 5): the sign-in next let "/<TAB>/evil.com" through, which browsers read as "//evil.com" (they drop tab and newline characters and read a backslash as "/"); any control character or backslash in next now goes home. test/open-redirect.test.js feeds it the known bypassesOpenVibers · 26 Sept 2026
f356d72 -
Pins: openvibe-shared 1.18.0 -> 1.20.0 (the update matrix and release manifest 1.2.0: client generations, the shell)OpenVibers · 26 Sept 2026
a11fe37 -
Object explorer (roadmap WS-G task 12): openvibe.media/me shows a signed-in person the objects whose owner_subject is their Network subject, in every tenant, and /me/ops gives Network staff the operator viewsOpenVibers · 26 Sept 2026
15b8f93 -
Limits page source (roadmap WS-N task 7): GET /limits.json lists the developer limits enforced here (tenant storage per environment, child namespaces, single-part, multipart and part sizes, public playback size, how long an unfinished upload holds its quota, the restore window) from the running config, with the problem each answers past it; public, CORS *; openvibe.codes/docs/limits renders itOpenVibers · 26 Sept 2026
0fc4452 -
Pins: openvibe-shared 1.17.1 -> 1.18.0 (update telemetry: prompted count, sessions by generation)OpenVibers · 26 Sept 2026
b353594 -
Namespaces and grants (roadmap WS-G task 2): every namespace is a media_namespaces row (tenant, owner, policy, quota_bytes/quota_objects, used/reserved snapshot). A tenant's root is its app id, or app.<project_id> / app.<project_id>.sandbox for a developer project; children (<root>.<segment>, up to 3, MEDIA_NAMESPACE_MAX_CHILDREN per tenant) are made by the first init that names one. Network tokens are checked per namespace for five verbs (server/auth.js VERBS): read, list (media.object.list), write, delete (media.object.delete), transform (media.derivative.create); the older ids keep granting the newer verbs (read lists, upload deletes and transforms) unless the namespace's policy says strict_verbs, and an app token's legacy ns [prj_…] means app.<project_id>.*. Quotas cover a namespace's subtree up to the tenant (bytes: the root inherits apps.quota_bytes): init reserves the declared size in the same transaction as the object (413 media.quota.exceeded / media.quota.objects_exceeded), the stored bytes re-reserve, complete re-checks the real size and settles, a multipart abort or expiry releases, and the hourly sweep fails uploads whose reservation passed MEDIA_UPLOAD_RESERVATION_HOURS (72) and frees their bytes. Policy: kinds, visibilities, max_object_bytes. Owner read GET /api/v2/:app/namespaces[/:namespace]; operator scripts/namespaces.js (list, --set quotas and policy). Backfill on open (idempotent): developer-project objects move to app.<project_id>[.sandbox], every tenant and named namespace gets a row, and once, uploads in progress get a reservation for the full window. Test: namespaces.test.js (new); docs/object-model.md, READMEOpenVibers · 26 Sept 2026
09f3e0d -
Pins: openvibe-shared 1.16.1 -> 1.17.1 (release notifications)OpenVibers · 26 Sept 2026
7d765ea -
Pins: openvibe-shared 1.16.0 -> 1.16.1 (the OV mark stops animating when idle, hidden or off-screen)OpenVibers · 26 Sept 2026
c110173 -
A guest's session probe answers 200, not 401 (browser check, WS-Q task 3): GET /auth/me with no session credential at all answers { user: null }, which the shared navbar reads as signed out. It asks on every page view, and the 401 logged a console error on each. A credential that is present but invalid or expired still answers 401. Test: auth-me.test.js (new)OpenVibers · 26 Sept 2026
7a0eaac -
Unknown paths answer Express's own 404 page under Media's CSP, which now allows Cloudflare Web Analytics (browser check, WS-Q task 3): server/not-found.js keeps default-src 'none' and adds script-src https://static.cloudflareinsights.com (the beacon Cloudflare injects at the edge) and connect-src https://cloudflareinsights.com (where it reports). Media's pages send no policy of their own, and Express's default 404 policy blocked the beacon: a CSP error on every not-found page. The network's privacy text already says Cloudflare may measure performance. Tests: not-found-csp (new), observability (the real server's unknown path)OpenVibers · 26 Sept 2026
b578bbb -
Pins: openvibe-shared 1.15.0 -> 1.16.0OpenVibers · 26 Sept 2026
8c3c6fa -
Job fencing (WS-P lifecycle follow-up): succeed() and fail() matched status = 'running' only, with no owner token, so a worker that had lost its job (lease run out, recovery requeued it, another claim took it) could still overwrite it; safe only while one process runs the queue. Now a claim writes a fresh random lease_token on the row and hands it to the worker; renew, saveCheckpoint (with its alsoInTx writes, rolled back), succeed, fail and markCancelled match the id AND that token. A stale holder is refused, not applied: logged ([Jobs] refused a stale <action> of <id> ...), counted (queue.staleStats(), worker status stale_refused, media_job_stale_completions_total{action} on /metrics) and emitted on the queue bus; its heartbeat aborts the handler (media.job.lease_lost). Recovery takes a job over only with the token it read and, past boot, only while the lease is still expired; rows claimed before the column (NULL token) are still recovered at boot. Migration is additive only: media_jobs.lease_token TEXT in schema.sql and migrateColumns (ADD COLUMN); an older release ignores it. test/jobs-fencing.test.js: the column added to an older database; two claimants, the stale one's heartbeat, checkpoint, succeed, fail and cancel refused and counted while the current holder's result stands; a declined takeover; the worker aborting a job whose lease it lostOpenVibers · 26 Sept 2026
a348ea7