Commits
-
Size budgets for the home page (WS-T task 1): openvibe-shared/perf-budget (shared 1.15.0) measures / as the server answers it on a fresh database, no browser, in npm test; budgets a little above the 2026-09-26 measurementOpenVibers · 26 Sept 2026
37accc9 -
Moderation audit (WS-D task 1): placing or releasing a staff hold (moderation, dmca, admin, evidence) announces media.moderation.action (common.moderation-action@1) in the same transaction, the staff subject as actor when named; a creator's own pin, a repeat release and sandbox tenants announce nothing; contracts 0.54.0OpenVibers · 26 Sept 2026
c90ea88 -
Storage-tier policy as revisioned configuration (WS-C task 7): media.storage_tier on openvibe-shared/config 1.14.0: revision 1 imported from media_settings, PUT /tiers/settings is one validated revision (types, 1-100 percentages, interval floors, threshold order; refused changes change nothing, 422), the admin config routes (list, history, rollback), media_settings mirrors the active revision for older releases, the sweep restarts with a new policy; pins shared 1.14.0, contracts 0.52.0OpenVibers · 26 Sept 2026
41b548e -
Clip cut on media_jobs (WS-G task 3): a new clip's cut and every re-cut run as the job clip.cut (lane clips, 2 at a time, runs while recording): POST /clips and /:id/recut answer 202 with job_id for the UI to follow and reattach to, media.job.* events, queue-owned retries (2/10/30 min, a window with no footage fails at once), clip.ready/clip.failed and the webhook as before; the old sweeper only retries clips without a jobOpenVibers · 26 Sept 2026
1337cc1 -
Waveform and preview-sprite variants as jobs (WS-G task 13): object.waveform (PNG, showwavespic) and object.sprite (JPEG sheet from one fast seek per frame, layout in its metadata) make private asset objects recorded as the source's waveform / sprite variant; no_audio / no_video are permanent; a remote-only source over MEDIA_PREVIEW_REMOTE_MAX_MB gets no waveformOpenVibers · 25 Sept 2026
f9c89e1 -
H15 repair (WS-G task 7): scripts/h15-repair.js tries every copy of each object with no good copy before anything is condemned: a B2/R2 copy marked corrupt only because its size differs from the size recorded at creation (no content hash) is downloaded and decoded to the end, and a good one becomes the object (size and sha256 re-recorded); a fragment under 2 s is a recording that failed when it was made (lifecycle failed, not playable); a missing AI-moment frame is extracted again from its VOD; anything else is reported as lost. Dry run by default; --apply takes an online backup and writes a rollback file; --rollback restoresOpenVibers · 25 Sept 2026
cbe1413 -
Media index: the selected tab's count is not dimmed (3.3:1), and a pager arrow that goes nowhere says it is disabled — WS-T task 3OpenVibers · 25 Sept 2026
81ccb68 -
Media index: the selected tab uses the derived --accent-strong/--on-accent-strong pair (Shared 1.13.0), 4.5:1 in every theme; white on the accent was below it (WS-T task 3)OpenVibers · 25 Sept 2026
ab8cc62 -
Pins: openvibe-contracts 0.45.0 → 0.49.0, openvibe-shared 1.12.0 → 1.13.0, openvibe-sdk 0.9.1 → 0.11.0OpenVibers · 25 Sept 2026
91520ee -
Footer: Media's own links as the shared footer's { name, url } items (label/href rendered four empty links; found by an axe survey, WS-T task 3)OpenVibers · 25 Sept 2026
43a051e -
Object-first writes (WS-G task 1, first stage of retiring C-75): every write to a vods/clips/files/pastes row now creates or updates its media_object in the same SQLite transaction (model.withObject; inside announce() for vod/clip outcomes, events still queued after the outcome), so a crash or error between the two can no longer leave drift, and a write whose object cannot be written changes nothing; the recorder start and tier moves fall back to the row alone because the bytes or ffmpeg already moved; a read-only drift report (server/objects/drift.js, scripts/object-drift-report.js) lists rows whose object is missing, unlinked or disagrees on kind, visibility, lifecycle, size, legacy ref or owner; the boot backfill stays until a release shows zero driftOpenVibers · 25 Sept 2026
e9a0f3a -
Media's own watch pages in OpenVibe.Search (WS-O task 10, Contracts 0.45.0): media.index_document.upserted for exactly the pages the sitemap lists (one WATCH_WHERE rule shared by both: public, finished, playable, made by a person, not Live's, no sandbox), a tombstone once one stops qualifying, sent only when it changed, every 10 minutes through the outbox; Live's VODs and clips stay Live's documentsOpenVibers · 25 Sept 2026
bc7b1a5 -
Pins: openvibe-contracts 0.33.0 → 0.44.0; CI runs the pin-drift check (shared test and security workflows at 423fe51)OpenVibers · 25 Sept 2026
30dc5eb -
export-legacy-refs --from-pastes <app>: each paste row's own object (pastes.object_id), keyed legacy:<app>:paste:<slug>, also when the object is filed under another legacy name (screenshots that were also avatars)OpenVibers · 25 Sept 2026
e027ac5 -
scripts/export-legacy-refs.js: which med_ object stands behind each legacy reference under a prefix (read-only), for services moving to object ids (C-24, Community's screenshot pastes)OpenVibers · 25 Sept 2026
9cf52c0 -
Retention holds (WS-G task 10), on the existing media_holds (no second table): a note column (added to existing databases), placed_by/placed_at in every answer (created_by/created_at kept), and staff routes on the app-key admin router: GET/POST /api/v1/:app/admin/storage/holds (by object_id, legacy ref, vod_id or clip_id; reason required; a row with no object yet is projected first; refused to calls acting for a user) and POST /holds/:id/release (also DELETE); every placement and release is an [Admin] log line and the row keeps who and when. Clips follow their VOD: a clip cut from a held VOD is held through clip_of or its row's vod_id, even before it has an object (database.js heldSql, one rule for model.isHeld and the new _v2 delete triggers, which replace the first-version ones in one transaction; idx_clips_object). Newly guarded paths: moveToHot refuses a held VOD (it flipped the row to local and dropped the R2 copy; a clip cut then uses the cloud copy), finalize settles a held empty or zero-byte recording as failed and keeps its row and file instead of deleting them, the boot junk sweep skips held rows, the paste DELETE answers 409 and the paste bulk delete skips held screenshots. v2 GET /:id/holds lists inherited_holds. test/retention-holds.test.jsOpenVibers · 25 Sept 2026
a63bb3a -
Storage orphan report (WS-G task 8), report only: server/vod/orphans-report.js buildStorageReport compares what storage holds with what the database names and lists files under the data directories and B2/R2 keys that no row names (with hints: a recorder name whose VOD row is gone, bytes of a deleted object, an R2 copy left after its VOD went back to B2, parked vods-orphans/ keys, parts of a session that no longer exists, a temp of an upload that never finished), copies the database records for live objects that are not there (no local file, a key absent from its bucket's listing), and multipart uploads left open (media_uploads sessions, expired ones flagged, and the buckets' own via ListMultipartUploads, new read-only vodStorage.listMultipartUploads). Live thumbnails and temps younger than an hour are counted as expected. It never deletes, moves or writes a file, an object or a row; it writes <data>/reports/storage-orphans-<time>.json. The job storage.orphans.scan runs it every MEDIA_ORPHAN_SCAN_DAYS (30; 0 = never) from the jobs worker (off in a restore drill) under the system app _media: no apps row, no media.job events, and POST /api/v2/:app/jobs refuses it (403 media.job.forbidden). On demand: scripts/vods-orphans-report.js --storage [--no-remote]. test/storage-orphans.test.js snapshots files, tables and buckets before and after, and its fake S3 refuses and counts any writeOpenVibers · 25 Sept 2026
95a449d -
Readiness levels (WS-G task 5): every media object answers metadata, bytes_verified and playable, read from the object row and its copies' recorded checks (server/objects/readiness.js). bytes_verified needs a copy that is present with a verified_at (the local file found, a B2/R2 HEAD with the right size, a v2 upload hashed on receipt; sha256 against content_hash where the verification job checks it) and no checksum contradicting content_hash; a B2 copy nobody has checked yet does not count. playable adds a finished vod or clip (lifecycle ready: not recording, not still cutting, not failed or deleted); the size invariant does not gate it, as in code it only refuses oversized v2 uploads. v2 object answers and the v1 VOD/clip answers carry readiness (webhook payloads do not; existing fields unchanged). The /v and /c watch pages offer a player, download link, video tags and VideoObject only when playable, and otherwise say why (still being recorded, still processing, not available) and are noindex; a clip still being cut gets that page instead of a 404, its bytes stay a 404. The byte routes are unchanged (Live's DVR reads recordings through them). The sitemap lists only playable pages (readiness.playableSql). test/readiness.test.jsOpenVibers · 25 Sept 2026
8b1abf0 -
Sign-out everywhere deliveries verify: POST /internal/events is mounted before express.json(), which consumed the raw body its signature is over (every delivery answered 401). The test holds the orderOpenVibers · 25 Sept 2026
c140b32 -
Sign-out everywhere reaches openvibe.media (WS-B task 4, SDK 0.9.1): POST /internal/events (MEDIA_INBOUND_EVENTS_SECRET, signature v2, never through the proxy) applies network.user.token_valid_after into the SDK revocation store; the site's token check refuses older tokens; the subscription is created at boot (not on a drill). test/revocations.test.jsOpenVibers · 25 Sept 2026
4c3fdac -
The OpenVibe Frame from this site's own pinned copy: /shared/* is served by openvibe-shared/serve (1.12.0) and every page references it by content hash (/shared/<file>?v=<hash>), so this site's pin decides what its pages run and its navbar and footer keep working while openvibe.network is downOpenVibers · 24 Sept 2026
3c3ddf5 -
The OpenVibe Frame and the shared update system on openvibe.media: page-chrome.js is page-frame.js (frameScripts) on openvibe-shared/frame 1.11.1; the index shows what shipped and /updates is the shared log; the footer links it; the shared navbar signs out through this site (logoutUrl)OpenVibers · 24 Sept 2026
3733437 -
CI calls the shared test workflow (Track Q): OpenVibe.Shared test.yml@e8b0caf (install, node --check, npm test, openvibe-contracts-check; ffmpeg where the tests need it) instead of a copy of those stepsOpenVibers · 24 Sept 2026
28372f7 -
Channel lookups through Live's canonical lineage resolver (D20): the dev-data API's @username selector asks OpenVibe.Live /internal/lineage/resolve with Media's service token (live.lineage.resolve) for the channel and its Live user id; unresolved means not found. The old lookups (live list, public channel API) remain only for other apps or when Live cannot be asked. MEDIA_LINEAGE=off disables it. server/lineage-client.js, test/lineage-client.test.jsOpenVibers · 24 Sept 2026
9de5cb9 -
SECURITY.md: how to report a vulnerability ([email protected], 7-day reply, scope, supported versions)OpenVibers · 24 Sept 2026
984e7ad -
R2 tier decisions are recorded and explainable: every promotion to and demotion from the R2 popularity cache gets a media_tier_decisions row with the inputs it saw (view_count, last_accessed_at, provider, recording, hold, size, created_at), the R2 thresholds in force each with its source (default or a media_settings override), the reason in words, who asked (sweep, admin, clip, drill, manual) and the outcome (done, already, refused, failed, with the error). Before, a tier move only flipped media_locations and wrote a console line. The sweep states its rule per VOD (view_count 25 >= r2MinViews 20 and last accessed within r2RecentAccessDays 3; idle longer than r2MaxIdleDays 14), admin moves name the app, a clip cut that fetches an R2 VOD home (which drops the R2 copy) is logged as a demotion to local, and the eviction drill's evict and re-warm are logged as such. Staff read the policy through the app's key: GET /api/v1/:app/admin/storage/tiers/policy (each threshold with value, default and source, the promote and demote rules, provider status, the app's 24-hour counts and 20 latest decisions) and GET /tiers/decisions (filters vod_id, action, outcome; paged by before_id), both app-scoped and read-only; /metrics gains media_tier_decisions_24h{action,outcome}. The thresholds are unchanged (the test pins them). test/tier-decisions.test.js runs the sweep, admin moves, a hot-fetch and a drill step against a fake S3 through the real SDK.OpenVibers · 24 Sept 2026
7114433 -
thumbnail.regenerate refuses a recording with nothing to take a frame from. The five failed jobs on production (all at 2026-09-23 21:36:41, created by app:live, generate_failed) were Live asking for thumbnails of VODs 3044, 5537, 5540, 5546 and 5603: zero-byte recordings the health scan had quarantined (health_status zero_byte, 0-byte files on disk). The job ran ffmpeg on the empty files, got no frame, and the v1 route answered 500 to Live. Now a VOD whose health is zero_byte or missing_file, or whose local file is empty, fails at once as media_unavailable (permanent), which the route already answers 404, like a missing file. No production data was changed: the rows stay quarantined and the health job's cleanup removes them after its grace period. test/thumbnail-unavailable.test.js reproduces the production case (it fails with the old job: 500, generate_failed).OpenVibers · 24 Sept 2026
5099c97 -
Events media.object.deleted and media.object.visibility_changed through the outbox, so consumers (Search, Live, Community) learn that an object went away or stopped being public (Media emitted neither). Payloads are minimal: media.object.deleted { object_id, app_id, kind, legacy_ref, deleted_at }, media.object.visibility_changed { object_id, app_id, kind, legacy_ref, visibility, previous_visibility, changed_at }; subject object <id>, internal, important, actor service:media, times ISO 8601 UTC, legacy_ref null for native objects, never the title, owner, storage keys, sizes or metadata. Events only, no webhook; sandbox tenants produce none. Triggers on media_objects stage one media_object_changes row per deletion (lifecycle to deleted) and per visibility change (not for an object that is deleted) inside the transaction that makes it, whichever path made it: the v1 visibility routes through projection sync, the inherited row-delete triggers, a v2 soft delete, operator SQL. recordObjectChanges() turns staged rows into outbox envelopes and removes them, and must run inside a transaction: Media calls it at the end of its own transactions (projection sync, announce(), soft delete), so those events commit with their change and a rolled-back change has none; the relay drains the rest on its interval. With the outbox off the staged rows are dropped hourly. The outbox test now expects the media.object.deleted that a finalize row delete queues with media.vod.failed. test/object-events.test.js covers each path, the payload shape, rollback, the drain and sandboxes. Contract schemas for both types are being added in OpenVibe.Contracts v0.34.0 (payload shape in the C1 handoff).OpenVibers · 24 Sept 2026
039dc48 -
Crawler basics at the openvibe.media origin: /robots.txt (openvibe-shared seo.robotsTxt: AI and search crawlers named; /api/, /auth/, /internal/, /o/, /metrics, /release-metrics and the /live/ dev-data endpoints disallowed; the sitemap), /sitemap.xml and /llms.txt, each cached an hour. The sitemap lists the media index and the watch pages Media is the canonical home of: public, ready VODs and clips of apps other than Live, with their thumbnails as image entries. Live's items are left out on purpose: their watch pages here are noindex with the canonical on openvibe.live/vod and /clip, which Live's own sitemap lists, and a sitemap URL marked noindex is an error for search engines. AI clips, private, unlisted, developer-sandbox, recording, quarantined and failed items are never listed. The sitemap and the page share one predicate (pages.watchIndexable), so every listed page renders index, follow. Watch pages: AI clips (auto_generated) are now noindex, follow for every app, labelled AI clip, attributed to no person (creator Organization OpenVibe AI instead of a Person author) and isBasedOn their source VOD, as on Live; the schema.org VideoObject is emitted on public pages only (none on unlisted or private ones). test/crawl.test.js.OpenVibers · 24 Sept 2026
a90d0ed -
RTMP pull SSRF: POST /vods/:id/ingest/rtmp makes ffmpeg connect to the URL it is given, and any rtmp:// or rtmps:// URL was accepted, so an app key (or a leaked one) could point Media's ffmpeg at any host and port it can reach. The recorder now pulls only from allow-listed RTMP servers: MEDIA_RTMP_PULL_ALLOW, a host:port list defaulting to Live's RTMP server on this host (127.0.0.1:1935, localhost:1935, [::1]:1935; a URL without a port means 1935, or 443 for rtmps). The URL must be a plain rtmp[s]://host[:port]/path[?query]: no user info, backslash, whitespace or control characters, nothing a URL parser and ffmpeg could read differently, and alternative spellings of loopback (0x7f.0.0.1, 127.1) are not on the list. The scheme is handed to ffmpeg in lower case. A refused URL answers 400 (was 409) and starts no ffmpeg. test/rtmp-allowlist.test.js covers allowed and refused URLs (metadata address, other local ports, parser differentials, injected arguments) and the route.OpenVibers · 24 Sept 2026
cb17f29