Commits
-
Save as paste on the developer tools (platform S9): the output becomes an unlisted OpenVibe.Community paste through the gateway's /api/pastes proxy (as the signed-in person when there is one), linked under the buttons; Clear removes the linkOpenVibers · 24 Sept 2026
597fc6a -
Tools API for developers (platform S10): GET /api/v1/openapi.json, OpenAPI 3.1 generated from the tool descriptors (registry, one run path per tool with its input schema and an example, the jobs facade, problem responses, bearer auth); a /developers page (discover, run, jobs, the openvibe-sdk/tools client, access tiers and limits) linked from the home page, sitemap and llms.txt (new API section)OpenVibers · 24 Sept 2026
176a7b8 -
SECURITY.md: how to report a vulnerability ([email protected], 7-day reply, scope, supported versions)OpenVibers · 24 Sept 2026
b0aada2 -
openvibe-contracts v0.33.1 (gateway, img, audio, docs) and openvibe-shared v1.5.1 (every app). Descriptors now publish the catalogue's keywords (every tool; what ?q= matches) and each API tool's example as examples[] (145 tools; a job tool's with sample files that fit files.min and files.accept), which v0.33.1 validates and v0.33.0 rejected; no API, no examples. Every app mounts release.mount (GET /release.json and the navbar's POST /release-metrics into release_client_updates_total) through apps/_shared/release.js, which declares each app's components: shell (public/, the server files that stamp its pages, the job helper) and server (the rest), so an API-only release no longer reloads open tabs; the gateway, img, audio and docs serve manifest 1.1.0 validated against their own contracts (the release-manifest schema passed explicitly: the release root is the repository, which has no node_modules), maps, food, text and yt the 1.0.0 fields. Tests: release.test.js (components valid per app, mount, metrics), descriptors.test.js (keywords and examples published)OpenVibers · 24 Sept 2026
5ac8309 -
Uniform run API, jobs facade, deprecation headers and the Origin check (S6, ADR-027). POST /api/v1/tools/:id/run takes tools.run-request@1 as JSON or multipart exactly as openvibe-sdk v0.6.0 sends it (uploads as file parts; input, files (the references' JSON), wait_ms and idempotency_key as text parts; ?wait_ms= too; the Idempotency-Key header wins) and answers tools.run@1: 200 succeeded or failed (the tool's own problem), 202 + Location while a job tool is still going after wait_ms, 200 + Idempotent-Replayed for a replay. Refusals are problem+json with the contract's codes: 400 tools.run.invalid, 401 token.missing/token.invalid/tools.session_required, 403 capability.denied/tools.origin.refused, 404 tools.tool.not_found/tools.tool.not_runnable (api false: yt, regex…)/tools.run.file_not_found, 405, 409 tools.job.idempotency_conflict, 413 tools.file.too_large/tools.input.too_large, 415 (bytes sniffed), 422 tools.input.invalid with errors[] (the descriptor's input schema, Ajv from openvibe-contracts), 429 tools.quota.exceeded, 503 tools.tool.unavailable (descriptor status) vs tools.unavailable (the engine found a program missing), 503 tools.busy. Authorization is always enforced: a token needs the descriptor's auth.capability, network probes run only for tokens with tools.net.probe, auth.anonymous false needs a session, sign-in or token; quotas are the tool's class and cost per caller tier. The gateway answers for every tool: dev and text engines run the pages' code in a worker pool (terminated at the descriptor's timeout), net tools and Open Graph call their existing route in process (myip is the caller), both cached per tool where safe (spec cacheTtlMs: engines 10 min except random/time-dependent ones, slow lookups 1-60 min, never probes or live checks), inline Idempotency-Key replays for 15 min; img, audio and docs tools are streamed to their satellite, which runs them as jobs (job input { ...input, ...preset, tool: operation }; jobs now record the tool), with { job_id, index } references read across satellites under the owner's own credentials and { media_id } from the owner's Media-stored results. The gateway's /api/v1/jobs facade routes submits by type prefix (JSON, the multipart type part read from the stream head, or ?type=) and everything else by job id (remembered, else asked on loopback via GET /api/internal/jobs/:id, direct callers only and never limited), streaming events and files. /api/process…, /api/net/* and /api/dev/* keep their shapes and add Deprecation: true, Sunset: Thu, 31 Dec 2026 23:59:59 GMT and Link: </api/v1/tools/{id}/run>; rel="successor-version". The guard checks Origin (else Referer) on cookie-carried mutations (run API, job writes, /api/process…, /api/info, /api/probe, webhook bin create/delete): first-party *.openvibe.tools, the tool's own hosts and the same host pass, Bearer calls and cookie-less calls need nothing; 403 tools.origin.refused in enforce mode, recorded in report mode. Run and jobs routes give other sites' pages CORS without credentials. On /api/v1 the older limiters answer problem+json with Retry-After; img and docs let untyped upload parts through to the guard's sniffing (as audio did); refused uploads are deleted before the answer. Pool fix: a worker that errors (heap limit) leaves the pool at once, so no run is handed to it before its exit. nginx copy: a location for the run and jobs routes (512m uploads, no buffering, 600 s). Tests: run-api.test.js (gateway, img, docs, a stand-in Network: every answer checked against tools.run@1), jobs-facade.test.js, sdk-client.test.js (openvibe-sdk v0.6.0 createToolsClient against the real gateway: registry, sync run, job run + wait + file, waitMs, replays, a job reference img → docs, retry, a probe token; skips without an SDK checkout), guard Origin cases; the audio poll and the tools-api ETag checks no longer race on a busy machineOpenVibers · 24 Sept 2026
48605fa -
Worker threads for sharp and pdf-lib (S5): apps/_shared/jobs/pool.js runs img's image operations and docs' pdf-lib tools in worker_threads, so a large merge or conversion never blocks the event loop (/api/health, the job routes and every other request keep answering). TOOLS_WORKERS (2, per app TOOLS_WORKERS_<APP>) run at once, shared by the jobs and the synchronous /api/process endpoints, which now submit to the pool and wait; TOOLS_WORKER_QUEUE (64) more wait, then 503 tools.busy with Retry-After. Each worker has a V8 heap limit (TOOLS_WORKER_MEMORY_MB, 512): a run that needs more fails with 413 tools.input.too_large and its worker is replaced. A run past its timeout (the job's own, or the descriptor's limits.timeoutMs on the sync endpoints: 504 tools.run.timeout), a cancelled job or a sync client that went away has its worker terminated at once, which pdf-lib and sharp never allowed before. Buffers move by transfer (never a pooled slab), tool errors keep status/code/extra/guardReason, idle workers stop after TOOLS_WORKER_IDLE_MS; qpdf and poppler tools stay on the main thread (their child processes have their own timeouts). /api/health shows workers. The older per-route limiters no longer count liveness and readiness probes (the guard's quota already skipped them). Tests: pool.test.js (off the event loop, size cap, queue and refusal, buffers, error fields, timeout and abort terminate, heap limit, crash, idle stop, env sizing) and docs pool.test.js on a real process (health stays under 250 ms during a 5000-page merge; a merge over a 32 MB heap fails 413 and the next job succeeds)OpenVibers · 24 Sept 2026
fcc2fe0 -
Docs: Protect, Unlock and the password page count give qpdf its password arguments on stdin (@-) instead of an @file argument file. qpdf 12.3.2 on the host did not expand @file (it opened '@/tmp/…/args' as a PDF), so every Protect PDF in production failed with 'The PDF could not be encrypted ()'; the tests passed against an older qpdf. Verified on the host's qpdf 12.3.2: AES-256 (R=6) output, correct password unlocks. Passwords still never appear on the command line. A wrong password answers 422 tools.pdf.wrong_password (the contract's status; was 400). Watermark: a missing rotation now defaults to -45 degrees (parseInt gave NaN, so ?? never applied)OpenVibers · 24 Sept 2026
13b79ce -
Guard deploy and docs: systemd resource bounds (MemoryMax 2G img/audio/docs, 1G yt, 768M gateway/text/maps/food; TasksMax 256; Nice 5 for img, audio, docs, yt) and ReadWritePaths for the gateway's data/ (its guard.db); deploy.sh creates every app's data/ and checks each app loads the guard with its better-sqlite3 before restarting; the repo's nginx copies set X-Forwarded-For and CF-Connecting-IP from $remote_addr like the host (one hop, never a client's chain). README: the guard (callers, one address source, quotas and the default table, heavy work, uploads, ffmpeg, egress, the abuse log as pseudonymous under ADR-021, the challenge hook, report vs enforce, environment) and the new job boundsOpenVibers · 24 Sept 2026
ef14d55 -
Tools guard (apps/_shared/guard), used by the gateway and every satellite and driven by the tool descriptors: one caller resolver replaces the four copied auth.js files and the job routes' owner logic (service/app principals by token, aud openvibe.tools, sandbox apps at their own small tier; users by the Network token with the audience check the copies lacked; the ov_tools_jobs browser session; else anonymous by address, IPv6 by /64, hashed). trust proxy is TRUST_PROXY everywhere: exactly one hop, on loopback only (satellites used 2, maps/food 'loopback'); the gateway's proxy to a satellite now passes its own req.ip as that hop instead of appending nginx's socket address, and loopback services are told apart by their tokens. Tiered token-bucket quotas by quotaClass x tier weighted by each descriptor's cost (limits.js holds every number; tools-api for every /api/ call), UTC-day allowances in data/guard.db that survive restarts, browser sessions from one address sharing 3x one session's allowance, RateLimit-* headers, 429 problem+json tools.quota.exceeded with Retry-After. Heavy work: a semaphore for sync /api/process in img, audio and docs (503 tools.busy), the job store's queue-depth and disk bounds (system.busy(): TOOLS_JOBS_MAX_QUEUED, TOOLS_DISK_BUDGET_MB) and unfinished session jobs bounded per address as well as per owner (jobs keep only the hashed address key). Uploads: magic-byte sniffing against files.accept (415 tools.file.unsupported_type; a misleading extension is corrected; audio no longer takes whatever says octet-stream), docs uploads on disk instead of 100 MB x 50 in memory, sharp and the BMP/ICO/HEIC readers refuse inputs over 40 MP from the header (413), audio and PDF tools need a session, sign-in or token (auth.anonymous false; /api/context starts the page's session). ffmpeg: every input and probe gets -protocol_whitelist file,pipe, the operation's demuxer whitelist and -t = the descriptor's maxDurationSec (now 3 h on every audio tool), inputs are probed and a longer one refused first, and the sync path runs in the job context with the descriptor's timeout and is killed at it (504 tools.run.timeout) or when the client goes away. Egress: a per-target throttle across all callers and tools from limits.perTargetPerMinute (net tools, Open Graph), the port checker capped per request (20, refused not cut) and per caller (100 ports, 10 hosts per 10 minutes; sessions by address), probes through a token need tools.net.probe, webhook bins owned by their maker (session cookie, sent by the dev page with credentials, or token) with per-owner and per-address caps, yt downloads owned by whoever started them, Nominatim paced to 1/s and the Overpass wait list capped. Abuse log in guard.db: HMAC-SHA256(address, a random daily salt, only today's kept), principal or user id, tool, reason, enforced or not, repeats folded per minute, pruned after 30 days, no raw address anywhere; metrics tools_guard_refused_total{reason,tool}, tools_guard_enforcing, tools_guard_sync. A no-op challenge hook for Turnstile later. TOOLS_GUARD=report (default) records and counts what it would refuse and keeps the apps' older limiters in force, now keyed by the resolved caller after sign-in is read (the limiter-before-optionalAuth order bug in img, audio and docs); enforce refuses and the older limiters step aside. Hard limits apply in both modes: pixels, ffmpeg whitelists and duration, sniffing, the port-scan cap and the per-target throttle. The gateway gets better-sqlite3 for its guard.db. Tests: _shared guard.test.js (keys, tiers, bucket math and cost, session sharing, report vs enforce, headers and problems, day counters across a restart, sniffing, target throttle, port cap, abuse log without raw IPs and its pruning, the semaphore, job bounds, the challenge hook) and per-app guard/hardening tests (img pixel limit and sniffing, audio playlist/concat refusal at every layer with the duration cap and sync kill, docs disk uploads and session rule, gateway quotas, throttle, port cap, webhook ownership and proxy address)OpenVibers · 24 Sept 2026
6818bcc -
gitleaks: allow the two hand-made sample JWTs (exp 1 / signature x, sub x / signature sig) in the JWT tool's descriptor example and engine test, matched on path and valueOpenVibers · 23 Sept 2026
902b27e -
Tool registry API (capability tools.tool.read, ADR-027): GET /api/v1/tools answers tools.tool-list@1 with schemas as $refs (filters ?family= ?execution= ?api= ?status= ?q=, comma lists; a bad value is 400 tools.query.invalid), GET /api/v1/tools/:id tools.tool@1 with its schemas embedded, GET /api/v1/tools/:id/schema { $schema, $id, $defs: { input, output } } that the $refs point at. Public and cacheable: Access-Control-Allow-Origin * for any origin (each app's own CORS rules are skipped for these paths, and even a 429 is readable), OPTIONS preflight, an ETag with 304 on If-None-Match, Cache-Control public, max-age=300, HEAD; an unknown id is 404 problem+json tools.tool.not_found, and a planned placeholder or a mirror says so. Counted by each app's existing /api/ limiter (the gateway's 120 a minute). The gateway answers for all 169 tools and adds a tool_registry readiness row (degraded, never a crash, when a descriptor fails the contracts); img, audio, docs, text, yt, maps and food answer the same routes for their own tools with their own live status (apps/_shared/tools/local.js: catalogue copy, code-default hosts, a tool unavailable while a program it needs is missing, checked every 30 s), which the gateway reads every minute. /api/catalog.json is unchanged. tools-api.test.js runs the gateway and all seven satellites: contract-valid list, tool and schema documents (the $ref resolves with a JSON Schema validator), ETag/304, CORS, preflight, 404 problems, filters, the limiter, the catalogue untouched, each satellite's own list and status (qpdf, poppler, libheif, ffmpeg and yt-dlp pointed at nothing) and the gateway picking those statuses up. README documents the routes, where descriptors live, how status is decided and the server enginesOpenVibers · 23 Sept 2026
4cc923e -
Tool descriptors: one tools.tool@1 descriptor per catalogue tool (169), kept as pure data next to the code that runs it and merged by the gateway (ADR-027). apps/{img,audio,docs,text,yt,maps}/server/descriptors.js and apps/gateway/server/{net,dev}/descriptors.js hold what each app owns (execution, api, the job and its preset, older routes, input JSON Schema, files, output, limits, auth, quota class, cost, egress), derived from the domain maps (operation, default format), net/config.js (endpoint, unavailable marks) and the engines (text styles, cases, escapes); apps/_shared/tools/descriptor.js joins a spec with the catalogue's family, name, summary, hosts and docs page. Format hosts are presets (img and docs: defaultFormat, which the img job now takes and a caller's format still overrides, as on the page; audio: format, as its hosts force it). Network probes (port, ping, latency, traceroute, mtr) need tools.net.probe and never run anonymously; every other anonymous egress tool has a per-target throttle; audio and PDF jobs need a session or a token; yt is page-only (api false); browser tools with a server engine are execution client with api true; tools needing a DOM, terser or isolation (regex) are api false. apps/gateway/server/registry/descriptors.js merges them with the catalogue (owner domain overrides, the unavailable marks), checks every build with contracts.validate('tools.tool@1'), contracts.tools.checkDescriptor and contracts.tools.checkList, leaves a failing descriptor out and reports it (readyCheck), and takes each satellite's own unavailable marks (a missing qpdf, pdftoppm, heif-dec, ffmpeg or yt-dlp) from its GET /api/v1/tools every minute. Host defaults and folded builds move to registry/hosts.js (pure) so satellites name the same hosts. Tests: descriptors.test.js (every catalogue id has exactly one valid descriptor; schemas compile strictly and examples match; every job tool's operation exists and { ...example, ...preset, tool } passes its app's own job validation, within its upload types, sizes, file counts and timeout; every sync API tool runs through a route the gateway serves; hosts, overrides, statuses, planned and mirrors) and engines.test.js (every browser tool with api true names an engine that a fresh plain Node process requires and runs, its answer matching the output schema; the pages load those engines)OpenVibers · 23 Sept 2026
d04dbda -
Browser tools get server engines: the pure transforms behind the dev and text pages move out of the pages into engine files the pages load and Node can require, so a tool API can run the page's own code (ADR-027). public/js/dev-engine.js holds the dev tools' transforms (JSON minify/validate/parse/stringify, YAML, XML format/minify, CSV, SQL, HTML, Markdown to HTML, Base64, URL, JWT decode, UUID, hash, hex, timestamps, cron, beautify/minify, CSS minify/format, HTML minify, lorem, curl conversion, colour); dev.html loads it and only adds presentation (DOMParser validation, entity decoding, terser, the regex tester, HTML to Markdown and the webhook and Open Graph calls stay in the page). text-engine.js gains the transforms that were inline in pages (hex/decimal/octal code points, line shuffle and reverse, full Braille both ways, the twelve escapes, the character inspector, the cleaner's steps in order) and the new format-engine.js holds JSON helpers, the Markdown renderer and the line diff for json., markdown. and compare. (which do not load the font tables). Each page was run old and new in a vm with the same inputs: dev.html's 99 sample/mode cases, eight text pages' 103 writes and the cleaner's 512 option combinations answer identically. server/dev/engines.js and apps/text/server/engines.js wrap them for the API ({ text } or { data } per call; compare refuses more than 2000 lines a side); the pages stay browser-onlyOpenVibers · 23 Sept 2026
bc5d92e -
openvibe-contracts v0.33.0 (tools.tool@1, tools.tool-list@1, tools.run@1, tools.job@1 and contracts.tools: checkDescriptor, checkList, jobInput, ADR-027): img, audio and docs move from v0.30.1 (their jobs and tools.job.* events are unchanged), and the gateway depends on it for the first time, to validate the tool registry it will serveOpenVibers · 23 Sept 2026
c98edd4 -
Net copy: the curl and HTTP status pages say what they do. Both send one HEAD request from our server and show the status line and headers; the curl page promised any method, custom headers and a response body, and the status page a full code reference. They now describe a curl -I style check and a status code checker (with the redirect target and headers); custom methods, request headers and bodies are said to be unsupportedOpenVibers · 23 Sept 2026
a5fd2dd -
Net: the IPv4 and IPv6 pages do what they say, and every net tool has a renderer. The consistency test now also checks that net.html can render each tool's endpoint, which found two pages that showed raw JSON: IPv4 promised a CIDR subnet calculator but the route stripped the /nn (it now gives network, broadcast, netmask, wildcard, usable range and counts, /31 and /32 per RFC 3021, and names private, CGNAT, loopback, link-local, documentation, benchmarking, multicast and reserved ranges; private and reserved addresses get no upstream lookup); IPv6 promised expansion and compression but echoed the input (it now gives the full and the RFC 5952 compressed form, type, multicast scope and range, reverse DNS, and the owner of a global address). Both have renderers. whois goes back to the /rdap endpoint the page renders (the /whois alias route stays). Reverse DNS for IPv6 is injectable so the tests stay off the networkOpenVibers · 23 Sept 2026
d2c3931 -
README: the host packages some tools need (qpdf, poppler-utils, libheif-examples + libheif-plugin-libde265, ffmpeg), what happens while one is missing (503 tools.unavailable, the page says so, /api/ready degraded), the new limits and variables (PDF_MAX_PAGES, PDF2IMG_MAX_PAGES, YT_MAX_DURATION, YT_MAX_FILESIZE_MB, YT_INFO_CONCURRENCY, NET_IPINFO_TOKEN, the *_PATH overrides), the readiness rows for them, and the registry consistency testOpenVibers · 23 Sept 2026
96a7467 -
Registry consistency test: every catalogue tool (not planned, not a mirror) must resolve to something that exists or be marked unavailable. img/audio/docs: its host in the satellite's domain map naming an operation that apps/<app>/server/tools/index.js defines, with a default format that operation can write; net: an endpoint in net/config.js that the /api/net router serves and net.html calls (or status unavailable, said by the catalogue and the page); dev: an entry and a processor in dev.html; text: its host in the text app's host map (moved to apps/text/server/hosts.js so it can be read) pointing at a page that exists; media, places and pastes: the app (or Community) that serves it. A self-check shows it reports the old gap: merge.openvibe.tools → operation 'merge', which the audio tools did not define. 169 tools resolve today; 3 are marked unavailable (traceroute, mtr, reputation)OpenVibers · 23 Sept 2026
71d5808 -
Food → maps: the visitor's address goes along (X-Forwarded-For and X-Real-IP from req.ip), so maps' 30/min limit is per person again instead of one 127.0.0.1 bucket for every food visitor. Maps and food believe X-Forwarded-For only from loopback hops (trust proxy 'loopback' instead of 2: maps listens on 0.0.0.0 by default, and a direct caller's own header must not count). apps/food/test/maps-forwarding.test.js runs both and shows one visitor limited while another is not; the shared spawn helper can poll /api/ready for apps without /api/healthOpenVibers · 23 Sept 2026
a9e6762 -
YT: the length and size limits are enforced, and /api/info is capped and cached. maxDuration (YT_MAX_DURATION, default 3 h) was configured but never checked: /api/info now answers downloadable: false with the reason (too long, or a live stream that has not ended) and the page shows it instead of the formats; /api/download refuses such a video from the cached info before yt-dlp starts; yt-dlp gets --match-filter 'duration<=N & !is_live' and prints the length at pre_process, so a skipped download says why instead of 'Output file not found'. --max-filesize (YT_MAX_FILESIZE_MB, default 2048) is passed, and the progress line now carries downloaded bytes, so a part that grows past the cap is stopped. /api/info ran yt-dlp uncapped: at most YT_INFO_CONCURRENCY (3) lookups run at once, 20 wait, more get 503 + Retry-After; the same video id is looked up once at a time and kept 10 minutes. /api/health shows the limits and the info queue. apps/yt/test/limits.test.js uses a stand-in yt-dlpOpenVibers · 23 Sept 2026
cbc4155 -
Net tools: every catalogue tool works or says it is unavailable; myip, caching and tokens fixed. New (server/net/checks.js, all through the SSRF guard or DNS only): robots (fetch + RFC 9309 parse, groups, sitemaps, site-wide-block and unknown-directive warnings, test a path as a crawler; 512 KiB cap), sitemap (a bare domain uses robots.txt's Sitemap line, else /sitemap.xml; gzip, 10 MB download / 20 MB XML caps; root, namespace, loc, lastmod, changefreq, priority, 50,000 limit; an index is followed to its first 3 children), smtp (the domain's first MX or the host, port 25 or 587: greeting, EHLO extensions, STARTTLS with TLS version and certificate, AUTH after TLS, QUIT; never sends mail; egress gains connect() for a checked socket), blacklist (7 IPv4 DNSBLs + 3 domain lists, TXT reason, delisting links; refused and timed-out queries are 'unknown', not clean; IPv6 and private addresses are refused with a reason), uptime (one GET from this server, redirects re-checked: up/down, status, hops, time) and DNS propagation (ten public resolvers compared, the odd ones flagged). reputation, traceroute and mtr are marked unavailable (net/config.js status + reason; catalog.json status, the apex tool page and the tool page say so; traceroute/mtr need raw sockets). Every net tool now has an endpoint in net/config.js that net.html agrees with. Honest copy: ping and latency are timed TCP connections to port 443 from one server (not ICMP, not several regions), propagation compares public anycast resolvers, uptime checks once from one place; the family FAQ and hub copy no longer promise traceroute or regions. myip returns req.ip and the gateway trusts exactly one proxy hop (it trusted 2 and myip read the client's own first X-Forwarded-For entry); the custom-domain nginx template now sets X-Forwarded-For from $remote_addr. ip-api (or ipinfo over HTTPS when NET_IPINFO_TOKEN is set), RDAP and DoH answers are cached (net/cache.js: TTL + LRU, one upstream call per key at a time). createNetRoutes(null) could never load the ipinfo/Globalping tokens: they now come from NET_<KEY> in the environment first (Globalping is loaded, not used). Tests: net-tools.test.js (all mocked) and the SSRF test covers the new toolsOpenVibers · 23 Sept 2026
480b772 -
Docs: Protect, Unlock, PDF to image and Split do what their pages say. Protect encrypts with AES-256 through qpdf (R6; a random owner password unless one is given; printing and copying text can be refused from the page); it used to re-save the file unencrypted. Unlock decrypts with qpdf using the password (wrong password → 400 tools.pdf.wrong_password; an unencrypted file comes back unchanged with a note); pdf-lib had re-saved still-encrypted content. Passwords never reach a shell or the process list: arguments are an array and secrets go through qpdf's @file in a 0700 temp directory. PDF to image renders with poppler's pdftoppm (pdfinfo counts pages): PNG or JPEG at 72–600 dpi, one page as an image, several as a ZIP; at most 50 pages per conversion up to 150 dpi, 20 up to 300, 5 above (PDF2IMG_MAX_PAGES); it used to draw a placeholder card. Split with several parts (every page, in half, several ranges) returns a ZIP of every part instead of only the first. Every operation checks a page limit, PDF_MAX_PAGES (default 500) → 413 tools.pdf.too_many_pages. qpdf, pdftoppm and pdfinfo are found at boot (QPDF_PATH / PDFTOPPM_PATH / PDFINFO_PATH override, looked for again each minute while missing): a tool whose program is missing answers 503 problem tools.unavailable on /api/process and at job submit, /api/tools and /api/context mark it available: false, the page shows 'being set up' instead of the upload, and /api/ready lists it as degraded. server/tools/pdf.js holds the limit, the CLI runner and a stored-ZIP writer. The pdf-tools family FAQ no longer claims there is no page limit. apps/docs/test/pdf-tools.test.js: page limits, split ZIP, 503 without the programs, and with them a real AES-256 encrypt (pdf-lib and qpdf cannot open it without the password), decrypt and page renderingOpenVibers · 23 Sept 2026
fce0a49 -
Audio: merge.openvibe.tools works. Its host mapped to an operation that did not exist; the merge tool now joins 2 to 5 files in the order given (ffmpeg concat filter, every input resampled to 44.1 kHz, or 48 kHz for Opus, and stereo, so MP3, WAV and FLAC mix), re-encoded to any Convert format and bitrate, at most 3 hours in total; a file without an audio track is refused by number. audio.process jobs take 'files' for merge (receive is now uploadAny: one 'file' or up to 5 'files'); validate refuses a wrong file count per tool before anything runs; the result is <first>-merged.<ext> with fileCount. /api/process/multi is the synchronous form. The page has a Merge tab, a numbered file list with remove and add buttons, and output format and bitrate. apps/audio/test/merge.test.js covers the operation, the job, the host default and the sync endpointOpenVibers · 23 Sept 2026
614973f -
Img: BMP output is a real BMP (it was a PNG named .bmp), BMP and ICO uploads are read, ICO output decodes the upload once, HEIC decodes through libheif or answers 503. server/tools/codec.js writes BMP from sharp's raw pixels (24-bit, or 32-bit BGRA with a V4 BI_BITFIELDS header when the picture has transparency; 100 MP cap) and reads BMP itself (1/4/8/16/24/32-bit, RLE4/RLE8, bitfields, top-down) and ICO (largest entry, PNG or DIB with its AND mask), which sharp cannot; resize and crop keep a BMP a BMP. ICO output scales the one decoded upload to 256 px once and makes the smaller sizes from those pixels one after another, instead of six full decodes in parallel. HEIC (HEVC HEIF, iPhone photos) was never decodable by sharp's prebuilt libvips: it now goes through heif-dec/heif-convert (libheif-examples + libheif-plugin-libde265, found at boot, HEIF_DEC_PATH overrides, looked for again each minute while missing); without it a HEIC upload answers 503 problem tools.unavailable on /api/process and fails the job with the same problem, /api/ready lists heif_decoder as degraded, and heic.openvibe.tools says so above the upload. apps/_shared/binaries.js is the shared optional-binary helper (Unavailable → 503 tools.unavailable). apps/img/test/codec.test.js covers the header magic, round trips, palettes/RLE/top-down, ICO once, HEIC decode or 503OpenVibers · 23 Sept 2026
059d7a7 -
Analytics comes from openvibe-shared/analytics (v1.4.0; retires C-80): every app pins openvibe-shared v1.4.0 (package.json and lockfile), the seven satellites require openvibe-shared/analytics instead of the hand-copied apps/_shared/analytics/, which is deleted. scripts/analytics-prune.js is a wrapper over openvibe-shared/analytics/prune-cli (resolved from an installed app, like better-sqlite3 via loadSqlite()), keeping --app and the discovery of apps/<app>/data/analytics.db as its targets hook; backups are now owner-only. apps/_shared/test/analytics.test.js tests the package. New with v1.4.0: a request with Sec-GPC: 1 or DNT: 1 is not recorded at all (no raw row, visitor hash, session id or rate counter); README notes itOpenVibers · 23 Sept 2026
5d1b423 -
Jobs publish their lifecycle to OpenVibe.Events: tools.job.created (submit or retry; actor the owner), tools.job.started (claim), tools.job.succeeded and tools.job.failed (tool error, timeout, or failed on restart), each validated against openvibe-contracts tools.job.*@1 and written to an event_outbox table in the satellite's jobs.db (openvibe-sdk createOutbox) in the same SQLite transaction as the state change, including claim(); the relay posts to EVENTS_URL/api/v1/events with the tools client's token (events.event.publish on openvibe.events). Payloads carry ids, type, owner (null for a browser session), state, attempts, times, result locations (index, mime, size, sha256, local or media_id) and the error with server paths and the job's file names removed; never inputs, file names, output data or sessions. Cancelled jobs, progress, requeues and sandbox app jobs are not announced. Inert without EVENTS_URL (EVENTS_PUBLISH=off or no OV_OAUTH_CLIENT_SECRET also keep it off): no table, nothing written. A failed outbox write rolls the transition back; a finish that cannot commit leaves the job running with its inputs for boot recovery. apps/_shared/test/job-events.test.js covers one event per transition, rollbacks, sandbox, cancel, retry, restart, leaks, contract validation and the relayOpenVibers · 23 Sept 2026
5f3c304 -
Jobs apps (img, audio, docs): openvibe-contracts v0.30.1 (tools.job.created|started|succeeded|failed payload contracts, listed in the tools manifest's eventsProduced) and openvibe-sdk v0.4.0 (createOutbox for the job events relay)OpenVibers · 23 Sept 2026
b154b8d -
CI: run the shared security workflow (gitleaks secret scan + dependency audit, Track Q)OpenVibers · 23 Sept 2026
2a0b70c -
Jobs retention: referenced results are never pruned (PUT/DELETE /api/v1/jobs/:id/references/:ref, e.g. community:paste:p_123, owner-scoped, idempotent; a referenced job shows expires_at null), and the pruner no longer drops a job whose Media object Media keeps (retention hold 409 media.object.held, Media unreachable or not configured): the job is kept and looked at again 24 h laterOpenVibers · 23 Sept 2026
8b317eb -
Jobs: POST /api/v1/jobs/:id/retry retries a failed job as a new job with the same type, input and files (retry_of / retried_by link them); owner-scoped (tools.job.create for principals), idempotent (a second call answers 200 Idempotent-Replayed with the same retry), 409 tools.job.not_failed for anything but a failed job, 410 tools.job.inputs_goneOpenVibers · 23 Sept 2026
2aff45d