openvibers/openvibe.sdk Commits
Resumable Media upload facade and UI progress entry (#155)
Changes
@@@ -1,5 +1,9 @@# Changelog++## 0.120.0 (2026-11-09)++++- createUploadClient(): resumable Media uploads in 8 MiB parts, three at a time, with a session reference to resume; ov.ui.progress(el)++## 0.119.0 (2026-11-09)- openvibe-sdk/wallet: the wallet, Vibes, the store and items, through Billing and Inventory with the person's bearer and idempotency keys@@@ -10,7 +10,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags> Supported browser and server clients for the OpenVibe platform.--**Status:** alpha, v0.119.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.++**Status:** alpha, v0.120.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.@@@ -19,7 +19,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tagsAfter v0.97.1 is tagged:```bash--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.119.0++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.120.0```It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).@@@ -109,6 +109,23 @@ const file = await media.upload(fs.readFileSync('logo.png'), { filename: 'logo.pfile.public_url; // https://openvibe.media/f/<key>```++For large files, use Media's resumable multipart object API. The uploader uses 8 MiB parts,++three concurrent requests, retries missing parts, and exposes the upload reference before++any bytes leave so the app can persist it. Keep the credential on a server.++++```js++const uploader = sdk.mediaUpload.createUploadClient({ app: unverifiedClaims.project_id, tokenClient: tokens });++const object = await uploader.upload(file, {++ onSession: ({ objectId, uploadId }) => saveUploadReference({ objectId, uploadId }),++ onProgress: ({ uploadedBytes, totalBytes }) => reportProgress(uploadedBytes, totalBytes),++});++// After a restart: uploader.resume(savedReference, sameFile, { onProgress });++```++++On a page that loads the pinned `ov-upload.js` and `ov-upload.css` from++`openvibe-shared`, `sdk.ui.progress(el)` mounts the same progress component used++across OpenVibe sites.++The token's `env` picks the tenant: a sandbox app's files go to the project's sandbox tenant (`app_id: '<prj_…>-sandbox'`) and are never served publicly. Media answers them with `sandbox: true`, a signed, expiring `url` and `url_expires_at`; the client adds `signed_url` (the same URL) and sets `public_url` to `null`. Uploading and deleting need `media.object.upload`, listing and getting `media.object.read`.App events (`events.app.publish | read | subscribe`) live under the project's own topic `app.<project_key>.*` (`project_key` = `p` + the lowercased project ULID) with `source: app-<lowercased app ULID>`. `createAppEvents()` fills that in, and scopes pulls and subscriptions to the project:@@@ -575,7 +592,7 @@ shows the cards in `GET /api/frame`.- **Community comments on other content, the forum and Pulse** are being built in Community right now. Only paste comments are wrapped. The rest is a TODO in `src/community.js` until those APIs settle.- **Media:** the v1 files API, the object API v2 (`createObjectsClient`) and the public URL helpers are wrapped. Retention holds on v2 objects are not (app keys only). VOD, clip, thumbnail and admin-storage routes are not wrapped because apps reach them only through Live and Media's own servers. Media's own paste API is not wrapped because pastes moved to Community (ADR-011). The mock platform (`openvibe-sdk/testing`) models Media's v1 files only, not v2 objects.--- **Media from the browser:** Media refuses user JWTs on `/api/v1/:app/files`, so a browser uploads through its own app server. That server holds the app key and names the user with `actingUserId`.++- **Media from the browser:** Media refuses user JWTs on the v1 files and v2 objects APIs, so a browser uploads through its own app server. That server holds the app credential and names the user with `actingUserId` or a subject.- **Network coins, notifications and legacy-map writes** are internal service-to-service routes with no public capability, so they are not wrapped. Staff-only paste routes are also internal: admin stats, bulk, censor and the AI pass.- **Billing and Games** from the original charter come when those services publish their capabilities in Contracts (Live has its module since v0.52.0). Codes' tasks (they run code on a person's own machine) stay first-party, and OpenVibe.Watch is not built yet; Host and News have modules for their public reads, while editing stays first-party. Blog opened its posts to apps (openvibe-contracts 0.206.0, `openvibe-sdk/blog`), then Reviews and the Wiki their reads (0.207.0), then Coupons, Deals and Trade what a person does for themselves (0.208.0), then Space its sites (0.209.0). News' published stories need no capability, but its editorial reads stay first-party. Chat is wrapped over REST as the token's person (`openvibe-sdk/chat`, v0.11.0), but it has no app principal (`chat.message.send` is `first-party`) and the mock platform has no Chat.- **Tools** are not wrapped one by one: `openvibe-sdk/tools` runs any tool with `api: true` by id, and its descriptor (`tools.get(id)`) gives the input schema, files and limits. `openvibe-sdk/jobs` still submits any job `type` directly. The YouTube downloader is page-only (`api: false`), so it is not in the SDK.@@@ -3,7 +3,7 @@"stage": "alpha","code": true,"wave": "W2",-- "version": "0.119.0",++ "version": "0.120.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30","deployed": false,"deployment": "none: a package released as git tags; services and apps pin a tag",@@@ -16,6 +16,8 @@ module.exports = {realtime: require('./src/realtime'),media: require('./src/media'),cam: require('./src/cam'),++ mediaUpload: require('./src/media-upload'),++ ui: require('./src/ui'),community: require('./src/community'),homeFeed: require('./src/home-feed'),bot: require('./src/bot'),@@@ -1,6 +1,6 @@--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.119.0, MIT). Do not edit.++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.120.0, MIT). Do not edit.// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces--// (auth, locker, registry, trust, modules, realtime, media, cam, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.++// (auth, locker, registry, trust, modules, realtime, media, cam, mediaUpload, ui, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity./* eslint-disable */const __defs = [// ── 0: browser.js ──────────────────────────────────────────────────@@@ -23,23 +23,25 @@ module.exports = {realtime: __load(24),media: __load(25),cam: __load(27),-- community: __load(28),-- homeFeed: __load(29),-- bot: __load(30),-- search: __load(31),-- bench: __load(32),-- meme: __load(33),-- vision: __load(34),-- themes: __load(35),-- design: __load(36),-- redact: __load(37),-- chat: __load(38),-- ai: __load(39),-- credentials: __load(40),-- jobs: __load(41),-- tools: __load(43),-- projects: __load(44),-- geo: __load(45),++ mediaUpload: __load(28),++ ui: __load(29),++ community: __load(30),++ homeFeed: __load(31),++ bot: __load(32),++ search: __load(33),++ bench: __load(34),++ meme: __load(35),++ vision: __load(36),++ themes: __load(37),++ design: __load(38),++ redact: __load(39),++ chat: __load(40),++ ai: __load(41),++ credentials: __load(42),++ jobs: __load(43),++ tools: __load(45),++ projects: __load(46),++ geo: __load(47),};},// ── 1: src/core/index.js ───────────────────────────────────────────@@@ -54,7 +56,7 @@ const { paginate, offsetPager } = __load(7);const semver = __load(6);/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.119.0';++const SDK_VERSION = '0.120.0';module.exports = {createClient,@@@ -2856,6 +2858,7 @@ function (module, exports) {* delete, media.object.read to list and get. Media refuses a user JWT here: a browser uploads* through its own app server, which names the user with X-OV-User-Id (the `actingUserId` option).* Browser-safe code (FormData/Blob), but the credentials it needs belong on a server.++ * The upload facade lives at openvibe-sdk/media-upload so this frozen module keeps its names.** Sandbox app tokens land in the project's sandbox tenant (`<prj_…>-sandbox`, shown as `app_id`),* whose files are never served publicly: Media answers them with `sandbox: true`, a signed,@@@ -3185,6 +3188,7 @@ function createObjectsClient({}const up = created.upload || {};if (up.multipart) {++ if (opts.onSession) await opts.onSession({ objectId: created.id, uploadId: up.multipart.upload_id });return finishMultipart(created.id, up.multipart.upload_id, sessionIo(up.multipart, { objectId: created.id }), body, { signal, onProgress, contentHash, total });}await presigned(up.url, {@@@ -3368,7 +3372,40 @@ function defineOvCamera(options = {}) {module.exports = { defineOvCamera, OV_CAMERA_SRC, CAM_KIT_SRC, CAM_KIT_SRI, loadCamKit, open };},--// ── 28: src/community.js ────────────────────────────────────────────++// ── 28: src/media-upload.js ─────────────────────────────────────────++function (module, exports) {++'use strict';++const { createObjectsClient } = __load(25);++const MIB = 1024 * 1024;++++/** Media's resumable multipart transport, configured for 8 MiB parts and three in flight. */++function createUploadClient(config) {++ const objects = createObjectsClient({ partSize: 8 * MIB, concurrency: 3, ...config });++ return {++ upload(file, options = {}) {++ return objects.upload(file, { kind: 'file', filename: file && file.name, ...options });++ },++ resume(ref, file, options) { return objects.resume(ref, file, options); },++ objects,++ };++}++++module.exports = { createUploadClient };++},++// ── 29: src/ui.js ───────────────────────────────────────────────────++function (module, exports) {++'use strict';++++/** Mount the shared OpenVibe progress UI after loading /shared/ov-upload.js and .css. */++function progress(el, options) {++ const upload = globalThis.OVUpload;++ if (!upload || typeof upload.progress !== 'function') throw new Error('ov.ui.progress: load /shared/ov-upload.js first');++ return upload.progress(el, options);++}++++module.exports = { progress };++},++// ── 30: src/community.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3503,7 +3540,7 @@ function createCommunityClient(client, defaults = {}) {module.exports = { createCommunityClient, actingHeaders };},--// ── 29: src/home-feed.js ────────────────────────────────────────────++// ── 31: src/home-feed.js ────────────────────────────────────────────function (module, exports) {'use strict';/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;@@@ -3530,7 +3567,7 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})module.exports = { createHomeFeedClient };},--// ── 30: src/bot.js ──────────────────────────────────────────────────++// ── 32: src/bot.js ──────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3752,7 +3789,7 @@ function createBotClient(client, defaults = {}) {module.exports = { createBotClient, actingHeaders };},--// ── 31: src/search.js ───────────────────────────────────────────────++// ── 33: src/search.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -3865,7 +3902,7 @@ function createSearchClient(client, defaults = {}) {module.exports = { createSearchClient, queryOf, fabricOf };},--// ── 32: src/bench.js ────────────────────────────────────────────────++// ── 34: src/bench.js ────────────────────────────────────────────────function (module, exports) {'use strict';/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */@@@ -3966,7 +4003,7 @@ function createBenchClient(client, defaults = {}) {module.exports = { createBenchClient };},--// ── 33: src/meme.js ─────────────────────────────────────────────────++// ── 35: src/meme.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4034,7 +4071,7 @@ function createMemeClient(client, defaults = {}) {module.exports = { createMemeClient };},--// ── 34: src/vision.js ───────────────────────────────────────────────++// ── 36: src/vision.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4192,7 +4229,7 @@ function pick(o, keys) {module.exports = { createVisionClient, DEFAULT_AUDIENCE };},--// ── 35: src/themes.js ───────────────────────────────────────────────++// ── 37: src/themes.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4342,7 +4379,7 @@ function followTheme(fn, opts = {}) {module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };},--// ── 36: src/design.js ───────────────────────────────────────────────++// ── 38: src/design.js ───────────────────────────────────────────────function (module, exports) {'use strict';@@@ -4409,7 +4446,7 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,module.exports = { createDesignClient, DEFAULT_DESIGN_URL };},--// ── 37: src/redact.js ───────────────────────────────────────────────++// ── 39: src/redact.js ───────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4656,7 +4693,7 @@ function onlySecret(text) {module.exports = { redact, onlySecret };},--// ── 38: src/chat.js ─────────────────────────────────────────────────++// ── 40: src/chat.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -4904,7 +4941,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {module.exports = { createChatClient, createInboxVisitor, createFeedPublisher };},--// ── 39: src/ai.js ───────────────────────────────────────────────────++// ── 41: src/ai.js ───────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5036,7 +5073,7 @@ function createAiClient(client, defaults = {}) {module.exports = { createAiClient, TERMINAL, OPS, CLASSES };},--// ── 40: src/credentials.js ──────────────────────────────────────────++// ── 42: src/credentials.js ──────────────────────────────────────────function (module, exports) {'use strict';@@@ -5054,7 +5091,7 @@ function createCredentialsClient(client, defaults = {}) {module.exports = { createCredentialsClient };},--// ── 41: src/jobs.js ─────────────────────────────────────────────────++// ── 43: src/jobs.js ─────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5092,7 +5129,7 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { appendFiles } = __load(42);++const { appendFiles } = __load(44);const { parseSSE } = __load(24);const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);@@@ -5256,7 +5293,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'openmodule.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };},--// ── 42: src/core/form.js ────────────────────────────────────────────++// ── 44: src/core/form.js ────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5290,7 +5327,7 @@ function appendFiles(form, list, where) {module.exports = { isBlob, isUpload, appendFiles };},--// ── 43: src/tools.js ────────────────────────────────────────────────++// ── 45: src/tools.js ────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5324,8 +5361,8 @@ function (module, exports) {*/const { OpenVibeError, isOpenVibeError } = __load(3);const { newIdempotencyKey } = __load(5);--const { isUpload, appendFiles } = __load(42);--const { createJobsClient } = __load(41);++const { isUpload, appendFiles } = __load(44);++const { createJobsClient } = __load(43);const enc = encodeURIComponent;const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;@@@ -5585,7 +5622,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'opemodule.exports = { createToolsClient, ToolRunError, isToolRunError };},--// ── 44: src/projects.js ─────────────────────────────────────────────++// ── 46: src/projects.js ─────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5701,7 +5738,7 @@ function createProjectsClient(client, { baseUrl } = {}) {module.exports = { createProjectsClient };},--// ── 45: src/geo.js ──────────────────────────────────────────────────++// ── 47: src/geo.js ──────────────────────────────────────────────────function (module, exports) {'use strict';/**@@@ -5781,9 +5818,9 @@ export const {parseTraceparent, startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey,isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,auth, locker, registry, trust, modules, realtime,-- media, cam, community, homeFeed, bot, search,-- bench, meme, vision, themes, design, redact,-- chat, ai, credentials, jobs, tools, projects,-- geo,++ media, cam, mediaUpload, ui, community, homeFeed,++ bot, search, bench, meme, vision, themes,++ design, redact, chat, ai, credentials, jobs,++ tools, projects, geo,} = sdk;export default sdk;@@@ -251,6 +251,11 @@"mediaUrls"]},++ "mediaUpload": {++ "object": [++ "createUploadClient"++ ]++ },"meme": {"object": ["createMemeClient"@@@ -409,6 +414,11 @@"isNew"]},++ "ui": {++ "object": [++ "progress"++ ]++ },"ulid": "function","usage": {"object": [@@@ -877,6 +887,9 @@"createObjectsClient": "function","mediaUrls": "function"},++ "openvibe-sdk/media-upload": {++ "createUploadClient": "function"++ },"openvibe-sdk/meme": {"createMemeClient": "function"},@@@ -1166,6 +1179,9 @@"createTrustClient": "function","isNew": "function"},++ "openvibe-sdk/ui": {++ "progress": "function"++ },"openvibe-sdk/usage": {"createUsageReporter": "function","usageKey": "function",@@@ -7,13 +7,13 @@ export const {isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,report, share, locker, auth, registry, identity,trust, modules, events, realtime, media, cam,-- community, homeFeed, bot, search, bench, meme,-- vision, themes, design, redact, chat, chatApps,-- ai, credentials, jobs, tools, projects, resources,-- vip, quest, live, inventory, tips, blog,-- reviews, wiki, coupons, deals, shoes, trade,-- space, host, news, food, actor, run,-- video, work, zone, play, download, openre,-- geo, usage, commerce, telemetry,++ mediaUpload, ui, community, homeFeed, bot, search,++ bench, meme, vision, themes, design, redact,++ chat, chatApps, ai, credentials, jobs, tools,++ projects, resources, vip, quest, live, inventory,++ tips, blog, reviews, wiki, coupons, deals,++ shoes, trade, space, host, news, food,++ actor, run, video, work, zone, play,++ download, openre, geo, usage, commerce, telemetry,} = cjs;export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/media-upload.js. Do not edit.++import cjs from '../src/media-upload.js';++++export const {++ createUploadClient,++} = cjs;++export default cjs;nnew file mode 100644@@@ -0,0 +1,7 @@++// Generated by scripts/esm.js from src/ui.js. Do not edit.++import cjs from '../src/ui.js';++++export const {++ progress,++} = cjs;++export default cjs;@@@ -21,6 +21,8 @@ module.exports = {realtime: require('./src/realtime'),media: require('./src/media'),cam: require('./src/cam'),++ mediaUpload: require('./src/media-upload'),++ ui: require('./src/ui'),community: require('./src/community'),homeFeed: require('./src/home-feed'),bot: require('./src/bot'),@@@ -1,12 +1,12 @@{"name": "openvibe-sdk",-- "version": "0.119.0",++ "version": "0.120.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-sdk",-- "version": "0.119.0",++ "version": "0.120.0","license": "MIT","bin": {"openvibe-download": "bin/openvibe-download.js"@@@ -1,6 +1,6 @@{"name": "openvibe-sdk",-- "version": "0.119.0",++ "version": "0.120.0","description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.","main": "index.js","types": "types/index.d.ts",@@@ -84,6 +84,16 @@"import": "./esm/media.mjs","default": "./src/media.js"},++ "./media-upload": {++ "types": "./types/media-upload.d.ts",++ "import": "./esm/media-upload.mjs",++ "default": "./src/media-upload.js"++ },++ "./ui": {++ "types": "./types/ui.d.ts",++ "import": "./esm/ui.mjs",++ "default": "./src/ui.js"++ },"./meme": {"types": "./types/meme.d.ts","browser": "./src/meme.js",@@@ -34,6 +34,8 @@ const ENTRIES = {'cam.mjs': '../src/cam/index.js','realtime.mjs': '../src/realtime.js','media.mjs': '../src/media.js',++ 'media-upload.mjs': '../src/media-upload.js',++ 'ui.mjs': '../src/ui.js','community.mjs': '../src/community.js','bot.mjs': '../src/bot.js','machines.mjs': '../src/machines.js',@@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');const semver = require('./semver');/** The package version (test/version.test.js keeps it equal to package.json). */--const SDK_VERSION = '0.119.0';++const SDK_VERSION = '0.120.0';module.exports = {createClient,nnew file mode 100644@@@ -0,0 +1,17 @@++'use strict';++const { createObjectsClient } = require('./media');++const MIB = 1024 * 1024;++++/** Media's resumable multipart transport, configured for 8 MiB parts and three in flight. */++function createUploadClient(config) {++ const objects = createObjectsClient({ partSize: 8 * MIB, concurrency: 3, ...config });++ return {++ upload(file, options = {}) {++ return objects.upload(file, { kind: 'file', filename: file && file.name, ...options });++ },++ resume(ref, file, options) { return objects.resume(ref, file, options); },++ objects,++ };++}++++module.exports = { createUploadClient };@@@ -10,6 +10,7 @@* delete, media.object.read to list and get. Media refuses a user JWT here: a browser uploads* through its own app server, which names the user with X-OV-User-Id (the `actingUserId` option).* Browser-safe code (FormData/Blob), but the credentials it needs belong on a server.++ * The upload facade lives at openvibe-sdk/media-upload so this frozen module keeps its names.** Sandbox app tokens land in the project's sandbox tenant (`<prj_…>-sandbox`, shown as `app_id`),* whose files are never served publicly: Media answers them with `sandbox: true`, a signed,@@@ -225,6 +225,7 @@ function createObjectsClient({}const up = created.upload || {};if (up.multipart) {++ if (opts.onSession) await opts.onSession({ objectId: created.id, uploadId: up.multipart.upload_id });return finishMultipart(created.id, up.multipart.upload_id, sessionIo(up.multipart, { objectId: created.id }), body, { signal, onProgress, contentHash, total });}await presigned(up.url, {nnew file mode 100644@@@ -0,0 +1,10 @@++'use strict';++++/** Mount the shared OpenVibe progress UI after loading /shared/ov-upload.js and .css. */++function progress(el, options) {++ const upload = globalThis.OVUpload;++ if (!upload || typeof upload.progress !== 'function') throw new Error('ov.ui.progress: load /shared/ov-upload.js first');++ return upload.progress(el, options);++}++++module.exports = { progress };@@@ -11,6 +11,7 @@ const crypto = require('node:crypto');const { stubServer, send, problem, run } = require('./helpers');const { createClient } = require('../src/core');const { createObjectsClient } = require('../src/media');++const { createUploadClient } = require('../src/media-upload');const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');@@@ -188,10 +189,15 @@ run([const m = await mockMedia();const objects = createObjectsClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), multipartThreshold: 10000, partSize: 4096, concurrency: 3 });const bytes = crypto.randomBytes(4096 * 4 + 100);-- const obj = await objects.upload(new Blob([bytes], { type: 'video/webm' }), { kind: 'vod' });++ let ref;++ const obj = await objects.upload(new Blob([bytes], { type: 'video/webm' }), { kind: 'vod', onSession: (value) => {++ ref = value;++ assert.equal(m.srv.requests.filter((r) => r.method === 'PUT').length, 0, 'session is offered for persistence before bytes leave');++ } });const init = JSON.parse(m.srv.requests.find((r) => r.url === '/api/v2/demo/objects').body);assert.deepEqual([init.multipart, init.part_size, init.mime_type, init.content_hash], [true, 4096, 'video/webm', sha(bytes)]);const s = [...m.state.sessions.values()][0];++ assert.deepEqual(ref, { objectId: obj.id, uploadId: s.upload_id });assert.equal(s.parts_expected, 5);const puts = m.srv.requests.filter((r) => r.method === 'PUT');assert.equal(puts.length, 5);@@@ -203,6 +209,18 @@ run([await m.srv.close();}],++ ['the upload facade uses Media multipart and reports progress', async () => {++ const m = await mockMedia();++ const media = createUploadClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), multipartThreshold: 100, partSize: 1024 });++ const bytes = crypto.randomBytes(2500);++ const progress = [];++ const obj = await media.upload(bytes, { onProgress: (p) => progress.push(p.uploadedBytes) });++ assert.equal(obj.lifecycle_status, 'ready');++ assert.equal(progress.at(-1), bytes.length);++ assert.equal(JSON.parse(m.srv.requests.find((r) => r.url === '/api/v2/demo/objects').body).part_size, 1024);++ await m.srv.close();++ }],++['Media refusing a single part (413) sends it as multipart instead', async () => {const m = await mockMedia({ singleMax: 1000 });const objects = createObjectsClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), partSize: 1024 });@@@ -72,7 +72,7 @@ run([})().catch((err) => { console.error(err); process.exit(1); });`;const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));-- assert.equal(subpaths.length, 96); // Adds the wallet client (0.119.0).++ assert.equal(subpaths.length, 98); // Adds the upload client and the UI entry (0.120.0); the wallet came in 0.119.0.assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');} finally {fs.rmSync(tmp, { recursive: true, force: true });nnew file mode 100644@@@ -0,0 +1,9 @@++'use strict';++const assert = require('node:assert/strict');++const sdk = require('../index');++++assert.throws(() => sdk.ui.progress({}), /load \/shared\/ov-upload\.js first/);++const expected = { update() {}, destroy() {} };++globalThis.OVUpload = { progress: (el, options) => { assert.equal(el.id, 'p'); assert.equal(typeof options.onRetry, 'function'); return expected; } };++try { assert.equal(sdk.ui.progress({ id: 'p' }, { onRetry() {} }), expected); }++finally { delete globalThis.OVUpload; }@@@ -5,6 +5,8 @@ import * as modules from './modules';import * as realtime from './realtime';import * as media from './media';import * as cam from './cam';++import * as mediaUpload from './media-upload';++import * as ui from './ui';import * as community from './community';import * as homeFeed from './home-feed';import * as bot from './bot';@@@ -34,6 +36,8 @@ export * as modules from './modules';export * as realtime from './realtime';export * as media from './media';export * as cam from './cam';++export * as mediaUpload from './media-upload';++export * as ui from './ui';export * as community from './community';export * as homeFeed from './home-feed';export * as bot from './bot';@@@ -57,6 +61,6 @@ export * as trust from './trust';declare const sdk: typeof core & {createWalletClient: typeof createWalletClient;auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;-- media: typeof media; cam: typeof cam; community: typeof community; homeFeed: typeof homeFeed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo; locker: typeof locker; trust: typeof trust;++ media: typeof media; cam: typeof cam; mediaUpload: typeof mediaUpload; ui: typeof ui; community: typeof community; homeFeed: typeof homeFeed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo; locker: typeof locker; trust: typeof trust;};export default sdk;@@@ -11,6 +11,8 @@ export * as events from './events';export * as realtime from './realtime';export * as media from './media';export * as cam from './cam';++export * as mediaUpload from './media-upload';++export * as ui from './ui';export * as community from './community';export * as homeFeed from './home-feed';export * as bot from './bot';nnew file mode 100644@@@ -0,0 +1,8 @@++import type { createObjectsClient, UploadBody, ObjectUploadOptions, MediaObject, ObjectUploadRef, ObjectsClient } from './media';++++/** A ready-to-use multipart client with 8 MiB parts and three concurrent requests. */++export declare function createUploadClient(opts: Parameters<typeof createObjectsClient>[0]): {++ upload(file: UploadBody, options?: ObjectUploadOptions): Promise<MediaObject>;++ resume(ref: ObjectUploadRef, file: UploadBody, options?: { signal?: AbortSignal; onProgress?: ObjectUploadOptions['onProgress']; contentHash?: string | false }): Promise<MediaObject>;++ objects: ObjectsClient;++};@@@ -124,6 +124,8 @@ export interface ObjectUploadOptions {uploadTtl?: number;signal?: AbortSignal;onProgress?(p: { uploadedBytes: number; totalBytes: number; part?: number }): void;++ /** Called as soon as Media creates a multipart session; persist this reference for reload resume. */++ onSession?(ref: ObjectUploadRef): void | Promise<void>;}/** err.resume of an sdk.upload_incomplete error. */@@@ -151,6 +153,7 @@ export interface ObjectsClient {client: OpenVibeClient;}++/*** Media's object API v2. Credentials: tokenClient (createServiceTokenClient; media.object.upload and* media.object.read for namespace `app`), apiKey, or a client carrying its own. Without baseUrl the Mediannew file mode 100644@@@ -0,0 +1,12 @@++export type ProgressState = 'waiting' | 'sending' | 'paused' | 'processing' | 'done' | 'failed';++export interface ProgressUpdate {++ state?: ProgressState;++ done?: number;++ total?: number;++ message?: string;++}++/** Requires the shared ov-upload.js and ov-upload.css assets on the page. */++export declare function progress(el: HTMLElement, options?: { onRetry?(): void }): {++ update(next: ProgressUpdate): void;++ destroy(): void;++};
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
# Changelog
# Changelog
+## 0.120.0 (2026-11-09)
+
+- createUploadClient(): resumable Media uploads in 8 MiB parts, three at a time, with a session reference to resume; ov.ui.progress(el)
+
## 0.119.0 (2026-11-09)
## 0.119.0 (2026-11-09)
- openvibe-sdk/wallet: the wallet, Vibes, the store and items, through Billing and Inventory with the person's bearer and idempotency keys
- openvibe-sdk/wallet: the wallet, Vibes, the store and items, through Billing and Inventory with the person's bearer and idempotency keys
@@ -10,7 +10,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
@@ -10,7 +10,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
> Supported browser and server clients for the OpenVibe platform.
> Supported browser and server clients for the OpenVibe platform.
-**Status:** alpha, v0.119.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
+**Status:** alpha, v0.120.0 (the five 1.0 core entries — `core`, `auth`, `events`, `db` and `media` — are frozen: their exported names change only with a major release; roadmap Wave 2; developer apps from Wave 20; Media objects v2 from Wave 4; the Tools platform API; Search, Chat, AI, Vision and consent clients; sign-in with recovery and a client health check). Every release is tagged (see [CHANGELOG.md](CHANGELOG.md)). Tested against local stub servers and the built-in mock platform only, never against the live platform. Services pin release tags; each repository's `STATUS.json` names the one it uses.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
**License:** MIT ([LICENSE](LICENSE)). This package is a library that apps outside the network embed, so it uses MIT. The OpenVibe services themselves stay AGPL-3.0.
@@ -19,7 +19,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
@@ -19,7 +19,7 @@ npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags
After v0.97.1 is tagged:
After v0.97.1 is tagged:
```bash
```bash
-npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.119.0
+npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.120.0
```
```
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
It has no runtime dependencies. It needs Node ≥ 20, or any browser with `fetch`, Web Crypto and `TextDecoder`. There is no build step. The package is CommonJS with ESM entry points (`import` works). Each subpath has its own `.d.ts`. For a page with no bundler, `browser/openvibe-sdk.mjs` is one self-contained ES module of the browser-safe subpaths (see [Browser without a bundler](#browser-without-a-bundler)). It does not depend on `openvibe-contracts` at runtime: it copies the contract types it uses (from the temporary Contracts sharing branch, a devDependency the tests check them against).
@@ -109,6 +109,23 @@ const file = await media.upload(fs.readFileSync('logo.png'), { filename: 'logo.p
@@ -109,6 +109,23 @@ const file = await media.upload(fs.readFileSync('logo.png'), { filename: 'logo.p
file.public_url; // https://openvibe.media/f/<key>
file.public_url; // https://openvibe.media/f/<key>
```
```
+For large files, use Media's resumable multipart object API. The uploader uses 8 MiB parts,
+three concurrent requests, retries missing parts, and exposes the upload reference before
+any bytes leave so the app can persist it. Keep the credential on a server.
+
+```js
+const uploader = sdk.mediaUpload.createUploadClient({ app: unverifiedClaims.project_id, tokenClient: tokens });
+const object = await uploader.upload(file, {
+ onSession: ({ objectId, uploadId }) => saveUploadReference({ objectId, uploadId }),
+ onProgress: ({ uploadedBytes, totalBytes }) => reportProgress(uploadedBytes, totalBytes),
+});
+// After a restart: uploader.resume(savedReference, sameFile, { onProgress });
+```
+
+On a page that loads the pinned `ov-upload.js` and `ov-upload.css` from
+`openvibe-shared`, `sdk.ui.progress(el)` mounts the same progress component used
+across OpenVibe sites.
+
The token's `env` picks the tenant: a sandbox app's files go to the project's sandbox tenant (`app_id: '<prj_…>-sandbox'`) and are never served publicly. Media answers them with `sandbox: true`, a signed, expiring `url` and `url_expires_at`; the client adds `signed_url` (the same URL) and sets `public_url` to `null`. Uploading and deleting need `media.object.upload`, listing and getting `media.object.read`.
The token's `env` picks the tenant: a sandbox app's files go to the project's sandbox tenant (`app_id: '<prj_…>-sandbox'`) and are never served publicly. Media answers them with `sandbox: true`, a signed, expiring `url` and `url_expires_at`; the client adds `signed_url` (the same URL) and sets `public_url` to `null`. Uploading and deleting need `media.object.upload`, listing and getting `media.object.read`.
App events (`events.app.publish | read | subscribe`) live under the project's own topic `app.<project_key>.*` (`project_key` = `p` + the lowercased project ULID) with `source: app-<lowercased app ULID>`. `createAppEvents()` fills that in, and scopes pulls and subscriptions to the project:
App events (`events.app.publish | read | subscribe`) live under the project's own topic `app.<project_key>.*` (`project_key` = `p` + the lowercased project ULID) with `source: app-<lowercased app ULID>`. `createAppEvents()` fills that in, and scopes pulls and subscriptions to the project:
@@ -575,7 +592,7 @@ shows the cards in `GET /api/frame`.
@@ -575,7 +592,7 @@ shows the cards in `GET /api/frame`.
- **Community comments on other content, the forum and Pulse** are being built in Community right now. Only paste comments are wrapped. The rest is a TODO in `src/community.js` until those APIs settle.
- **Community comments on other content, the forum and Pulse** are being built in Community right now. Only paste comments are wrapped. The rest is a TODO in `src/community.js` until those APIs settle.
- **Media:** the v1 files API, the object API v2 (`createObjectsClient`) and the public URL helpers are wrapped. Retention holds on v2 objects are not (app keys only). VOD, clip, thumbnail and admin-storage routes are not wrapped because apps reach them only through Live and Media's own servers. Media's own paste API is not wrapped because pastes moved to Community (ADR-011). The mock platform (`openvibe-sdk/testing`) models Media's v1 files only, not v2 objects.
- **Media:** the v1 files API, the object API v2 (`createObjectsClient`) and the public URL helpers are wrapped. Retention holds on v2 objects are not (app keys only). VOD, clip, thumbnail and admin-storage routes are not wrapped because apps reach them only through Live and Media's own servers. Media's own paste API is not wrapped because pastes moved to Community (ADR-011). The mock platform (`openvibe-sdk/testing`) models Media's v1 files only, not v2 objects.
-- **Media from the browser:** Media refuses user JWTs on `/api/v1/:app/files`, so a browser uploads through its own app server. That server holds the app key and names the user with `actingUserId`.
+- **Media from the browser:** Media refuses user JWTs on the v1 files and v2 objects APIs, so a browser uploads through its own app server. That server holds the app credential and names the user with `actingUserId` or a subject.
- **Network coins, notifications and legacy-map writes** are internal service-to-service routes with no public capability, so they are not wrapped. Staff-only paste routes are also internal: admin stats, bulk, censor and the AI pass.
- **Network coins, notifications and legacy-map writes** are internal service-to-service routes with no public capability, so they are not wrapped. Staff-only paste routes are also internal: admin stats, bulk, censor and the AI pass.
- **Billing and Games** from the original charter come when those services publish their capabilities in Contracts (Live has its module since v0.52.0). Codes' tasks (they run code on a person's own machine) stay first-party, and OpenVibe.Watch is not built yet; Host and News have modules for their public reads, while editing stays first-party. Blog opened its posts to apps (openvibe-contracts 0.206.0, `openvibe-sdk/blog`), then Reviews and the Wiki their reads (0.207.0), then Coupons, Deals and Trade what a person does for themselves (0.208.0), then Space its sites (0.209.0). News' published stories need no capability, but its editorial reads stay first-party. Chat is wrapped over REST as the token's person (`openvibe-sdk/chat`, v0.11.0), but it has no app principal (`chat.message.send` is `first-party`) and the mock platform has no Chat.
- **Billing and Games** from the original charter come when those services publish their capabilities in Contracts (Live has its module since v0.52.0). Codes' tasks (they run code on a person's own machine) stay first-party, and OpenVibe.Watch is not built yet; Host and News have modules for their public reads, while editing stays first-party. Blog opened its posts to apps (openvibe-contracts 0.206.0, `openvibe-sdk/blog`), then Reviews and the Wiki their reads (0.207.0), then Coupons, Deals and Trade what a person does for themselves (0.208.0), then Space its sites (0.209.0). News' published stories need no capability, but its editorial reads stay first-party. Chat is wrapped over REST as the token's person (`openvibe-sdk/chat`, v0.11.0), but it has no app principal (`chat.message.send` is `first-party`) and the mock platform has no Chat.
- **Tools** are not wrapped one by one: `openvibe-sdk/tools` runs any tool with `api: true` by id, and its descriptor (`tools.get(id)`) gives the input schema, files and limits. `openvibe-sdk/jobs` still submits any job `type` directly. The YouTube downloader is page-only (`api: false`), so it is not in the SDK.
- **Tools** are not wrapped one by one: `openvibe-sdk/tools` runs any tool with `api: true` by id, and its descriptor (`tools.get(id)`) gives the input schema, files and limits. `openvibe-sdk/jobs` still submits any job `type` directly. The YouTube downloader is page-only (`api: false`), so it is not in the SDK.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
"stage": "alpha",
"stage": "alpha",
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
- "version": "0.119.0",
+ "version": "0.120.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.2; roadmap §30",
"deployed": false,
"deployed": false,
"deployment": "none: a package released as git tags; services and apps pin a tag",
"deployment": "none: a package released as git tags; services and apps pin a tag",
@@ -16,6 +16,8 @@ module.exports = {
@@ -16,6 +16,8 @@ module.exports = {
realtime: require('./src/realtime'),
realtime: require('./src/realtime'),
media: require('./src/media'),
media: require('./src/media'),
cam: require('./src/cam'),
cam: require('./src/cam'),
+ mediaUpload: require('./src/media-upload'),
+ ui: require('./src/ui'),
community: require('./src/community'),
community: require('./src/community'),
homeFeed: require('./src/home-feed'),
homeFeed: require('./src/home-feed'),
bot: require('./src/bot'),
bot: require('./src/bot'),
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
-// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.119.0, MIT). Do not edit.
+// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.120.0, MIT). Do not edit.
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
// One self-contained ES module: core at the top level plus the browser-safe subpaths as namespaces
-// (auth, locker, registry, trust, modules, realtime, media, cam, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
+// (auth, locker, registry, trust, modules, realtime, media, cam, mediaUpload, ui, community, homeFeed, bot, search, bench, meme, vision, themes, design, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
/* eslint-disable */
const __defs = [
const __defs = [
// ── 0: browser.js ──────────────────────────────────────────────────
// ── 0: browser.js ──────────────────────────────────────────────────
@@ -23,23 +23,25 @@ module.exports = {
@@ -23,23 +23,25 @@ module.exports = {
realtime: __load(24),
realtime: __load(24),
media: __load(25),
media: __load(25),
cam: __load(27),
cam: __load(27),
- community: __load(28),
- homeFeed: __load(29),
- bot: __load(30),
- search: __load(31),
- bench: __load(32),
- meme: __load(33),
- vision: __load(34),
- themes: __load(35),
- design: __load(36),
- redact: __load(37),
- chat: __load(38),
- ai: __load(39),
- credentials: __load(40),
- jobs: __load(41),
- tools: __load(43),
- projects: __load(44),
- geo: __load(45),
+ mediaUpload: __load(28),
+ ui: __load(29),
+ community: __load(30),
+ homeFeed: __load(31),
+ bot: __load(32),
+ search: __load(33),
+ bench: __load(34),
+ meme: __load(35),
+ vision: __load(36),
+ themes: __load(37),
+ design: __load(38),
+ redact: __load(39),
+ chat: __load(40),
+ ai: __load(41),
+ credentials: __load(42),
+ jobs: __load(43),
+ tools: __load(45),
+ projects: __load(46),
+ geo: __load(47),
};
};
},
},
// ── 1: src/core/index.js ───────────────────────────────────────────
// ── 1: src/core/index.js ───────────────────────────────────────────
@@ -54,7 +56,7 @@ const { paginate, offsetPager } = __load(7);
@@ -54,7 +56,7 @@ const { paginate, offsetPager } = __load(7);
const semver = __load(6);
const semver = __load(6);
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.119.0';
+const SDK_VERSION = '0.120.0';
module.exports = {
module.exports = {
createClient,
createClient,
@@ -2856,6 +2858,7 @@ function (module, exports) {
@@ -2856,6 +2858,7 @@ function (module, exports) {
* delete, media.object.read to list and get. Media refuses a user JWT here: a browser uploads
* delete, media.object.read to list and get. Media refuses a user JWT here: a browser uploads
* through its own app server, which names the user with X-OV-User-Id (the `actingUserId` option).
* through its own app server, which names the user with X-OV-User-Id (the `actingUserId` option).
* Browser-safe code (FormData/Blob), but the credentials it needs belong on a server.
* Browser-safe code (FormData/Blob), but the credentials it needs belong on a server.
+ * The upload facade lives at openvibe-sdk/media-upload so this frozen module keeps its names.
*
*
* Sandbox app tokens land in the project's sandbox tenant (`<prj_…>-sandbox`, shown as `app_id`),
* Sandbox app tokens land in the project's sandbox tenant (`<prj_…>-sandbox`, shown as `app_id`),
* whose files are never served publicly: Media answers them with `sandbox: true`, a signed,
* whose files are never served publicly: Media answers them with `sandbox: true`, a signed,
@@ -3185,6 +3188,7 @@ function createObjectsClient({
@@ -3185,6 +3188,7 @@ function createObjectsClient({
}
}
const up = created.upload || {};
const up = created.upload || {};
if (up.multipart) {
if (up.multipart) {
+ if (opts.onSession) await opts.onSession({ objectId: created.id, uploadId: up.multipart.upload_id });
return finishMultipart(created.id, up.multipart.upload_id, sessionIo(up.multipart, { objectId: created.id }), body, { signal, onProgress, contentHash, total });
return finishMultipart(created.id, up.multipart.upload_id, sessionIo(up.multipart, { objectId: created.id }), body, { signal, onProgress, contentHash, total });
}
}
await presigned(up.url, {
await presigned(up.url, {
@@ -3368,7 +3372,40 @@ function defineOvCamera(options = {}) {
@@ -3368,7 +3372,40 @@ function defineOvCamera(options = {}) {
module.exports = { defineOvCamera, OV_CAMERA_SRC, CAM_KIT_SRC, CAM_KIT_SRI, loadCamKit, open };
module.exports = { defineOvCamera, OV_CAMERA_SRC, CAM_KIT_SRC, CAM_KIT_SRI, loadCamKit, open };
},
},
-// ── 28: src/community.js ────────────────────────────────────────────
+// ── 28: src/media-upload.js ─────────────────────────────────────────
+function (module, exports) {
+'use strict';
+const { createObjectsClient } = __load(25);
+const MIB = 1024 * 1024;
+
+/** Media's resumable multipart transport, configured for 8 MiB parts and three in flight. */
+function createUploadClient(config) {
+ const objects = createObjectsClient({ partSize: 8 * MIB, concurrency: 3, ...config });
+ return {
+ upload(file, options = {}) {
+ return objects.upload(file, { kind: 'file', filename: file && file.name, ...options });
+ },
+ resume(ref, file, options) { return objects.resume(ref, file, options); },
+ objects,
+ };
+}
+
+module.exports = { createUploadClient };
+},
+// ── 29: src/ui.js ───────────────────────────────────────────────────
+function (module, exports) {
+'use strict';
+
+/** Mount the shared OpenVibe progress UI after loading /shared/ov-upload.js and .css. */
+function progress(el, options) {
+ const upload = globalThis.OVUpload;
+ if (!upload || typeof upload.progress !== 'function') throw new Error('ov.ui.progress: load /shared/ov-upload.js first');
+ return upload.progress(el, options);
+}
+
+module.exports = { progress };
+},
+// ── 30: src/community.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3503,7 +3540,7 @@ function createCommunityClient(client, defaults = {}) {
@@ -3503,7 +3540,7 @@ function createCommunityClient(client, defaults = {}) {
module.exports = { createCommunityClient, actingHeaders };
module.exports = { createCommunityClient, actingHeaders };
},
},
-// ── 29: src/home-feed.js ────────────────────────────────────────────
+// ── 31: src/home-feed.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;
/** The menu's portable Home layout, network day counts and personal wall. Pass a user-token core client;
@@ -3530,7 +3567,7 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
@@ -3530,7 +3567,7 @@ function createHomeFeedClient(client, { networkBaseUrl, communityBaseUrl } = {})
module.exports = { createHomeFeedClient };
module.exports = { createHomeFeedClient };
},
},
-// ── 30: src/bot.js ──────────────────────────────────────────────────
+// ── 32: src/bot.js ──────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3752,7 +3789,7 @@ function createBotClient(client, defaults = {}) {
@@ -3752,7 +3789,7 @@ function createBotClient(client, defaults = {}) {
module.exports = { createBotClient, actingHeaders };
module.exports = { createBotClient, actingHeaders };
},
},
-// ── 31: src/search.js ───────────────────────────────────────────────
+// ── 33: src/search.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -3865,7 +3902,7 @@ function createSearchClient(client, defaults = {}) {
@@ -3865,7 +3902,7 @@ function createSearchClient(client, defaults = {}) {
module.exports = { createSearchClient, queryOf, fabricOf };
module.exports = { createSearchClient, queryOf, fabricOf };
},
},
-// ── 32: src/bench.js ────────────────────────────────────────────────
+// ── 34: src/bench.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */
/** openvibe-sdk/bench: OpenVibe.Bench's JSON API and streamed open dataset. Browser-safe. */
@@ -3966,7 +4003,7 @@ function createBenchClient(client, defaults = {}) {
@@ -3966,7 +4003,7 @@ function createBenchClient(client, defaults = {}) {
module.exports = { createBenchClient };
module.exports = { createBenchClient };
},
},
-// ── 33: src/meme.js ─────────────────────────────────────────────────
+// ── 35: src/meme.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4034,7 +4071,7 @@ function createMemeClient(client, defaults = {}) {
@@ -4034,7 +4071,7 @@ function createMemeClient(client, defaults = {}) {
module.exports = { createMemeClient };
module.exports = { createMemeClient };
},
},
-// ── 34: src/vision.js ───────────────────────────────────────────────
+// ── 36: src/vision.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4192,7 +4229,7 @@ function pick(o, keys) {
@@ -4192,7 +4229,7 @@ function pick(o, keys) {
module.exports = { createVisionClient, DEFAULT_AUDIENCE };
module.exports = { createVisionClient, DEFAULT_AUDIENCE };
},
},
-// ── 35: src/themes.js ───────────────────────────────────────────────
+// ── 37: src/themes.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4342,7 +4379,7 @@ function followTheme(fn, opts = {}) {
@@ -4342,7 +4379,7 @@ function followTheme(fn, opts = {}) {
module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };
module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seasonOn, onColor, DISPLAY_OPTIONS, TOKENS };
},
},
-// ── 36: src/design.js ───────────────────────────────────────────────
+// ── 38: src/design.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -4409,7 +4446,7 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
@@ -4409,7 +4446,7 @@ function createDesignClient({ accessToken = null, baseUrl = DEFAULT_DESIGN_URL,
module.exports = { createDesignClient, DEFAULT_DESIGN_URL };
module.exports = { createDesignClient, DEFAULT_DESIGN_URL };
},
},
-// ── 37: src/redact.js ───────────────────────────────────────────────
+// ── 39: src/redact.js ───────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4656,7 +4693,7 @@ function onlySecret(text) {
@@ -4656,7 +4693,7 @@ function onlySecret(text) {
module.exports = { redact, onlySecret };
module.exports = { redact, onlySecret };
},
},
-// ── 38: src/chat.js ─────────────────────────────────────────────────
+// ── 40: src/chat.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -4904,7 +4941,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {
@@ -4904,7 +4941,7 @@ function createFeedPublisher(client, { feed, key, baseUrl } = {}) {
module.exports = { createChatClient, createInboxVisitor, createFeedPublisher };
module.exports = { createChatClient, createInboxVisitor, createFeedPublisher };
},
},
-// ── 39: src/ai.js ───────────────────────────────────────────────────
+// ── 41: src/ai.js ───────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5036,7 +5073,7 @@ function createAiClient(client, defaults = {}) {
@@ -5036,7 +5073,7 @@ function createAiClient(client, defaults = {}) {
module.exports = { createAiClient, TERMINAL, OPS, CLASSES };
module.exports = { createAiClient, TERMINAL, OPS, CLASSES };
},
},
-// ── 40: src/credentials.js ──────────────────────────────────────────
+// ── 42: src/credentials.js ──────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
@@ -5054,7 +5091,7 @@ function createCredentialsClient(client, defaults = {}) {
@@ -5054,7 +5091,7 @@ function createCredentialsClient(client, defaults = {}) {
module.exports = { createCredentialsClient };
module.exports = { createCredentialsClient };
},
},
-// ── 41: src/jobs.js ─────────────────────────────────────────────────
+// ── 43: src/jobs.js ─────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5092,7 +5129,7 @@ function (module, exports) {
@@ -5092,7 +5129,7 @@ function (module, exports) {
*/
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
const { newIdempotencyKey } = __load(5);
-const { appendFiles } = __load(42);
+const { appendFiles } = __load(44);
const { parseSSE } = __load(24);
const { parseSSE } = __load(24);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
@@ -5256,7 +5293,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
@@ -5256,7 +5293,7 @@ function createJobsClient(client, { baseUrl, service = 'tools', audience = 'open
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
module.exports = { createJobsClient, isTerminal, TERMINAL_STATES: [...TERMINAL] };
},
},
-// ── 42: src/core/form.js ────────────────────────────────────────────
+// ── 44: src/core/form.js ────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5290,7 +5327,7 @@ function appendFiles(form, list, where) {
@@ -5290,7 +5327,7 @@ function appendFiles(form, list, where) {
module.exports = { isBlob, isUpload, appendFiles };
module.exports = { isBlob, isUpload, appendFiles };
},
},
-// ── 43: src/tools.js ────────────────────────────────────────────────
+// ── 45: src/tools.js ────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5324,8 +5361,8 @@ function (module, exports) {
@@ -5324,8 +5361,8 @@ function (module, exports) {
*/
*/
const { OpenVibeError, isOpenVibeError } = __load(3);
const { OpenVibeError, isOpenVibeError } = __load(3);
const { newIdempotencyKey } = __load(5);
const { newIdempotencyKey } = __load(5);
-const { isUpload, appendFiles } = __load(42);
-const { createJobsClient } = __load(41);
+const { isUpload, appendFiles } = __load(44);
+const { createJobsClient } = __load(43);
const enc = encodeURIComponent;
const enc = encodeURIComponent;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
const ID_RE = /^[a-z][a-z0-9-]{0,39}$/;
@@ -5585,7 +5622,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
@@ -5585,7 +5622,7 @@ function createToolsClient(client, { baseUrl, service = 'tools', audience = 'ope
module.exports = { createToolsClient, ToolRunError, isToolRunError };
module.exports = { createToolsClient, ToolRunError, isToolRunError };
},
},
-// ── 44: src/projects.js ─────────────────────────────────────────────
+// ── 46: src/projects.js ─────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5701,7 +5738,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
@@ -5701,7 +5738,7 @@ function createProjectsClient(client, { baseUrl } = {}) {
module.exports = { createProjectsClient };
module.exports = { createProjectsClient };
},
},
-// ── 45: src/geo.js ──────────────────────────────────────────────────
+// ── 47: src/geo.js ──────────────────────────────────────────────────
function (module, exports) {
function (module, exports) {
'use strict';
'use strict';
/**
/**
@@ -5781,9 +5818,9 @@ export const {
@@ -5781,9 +5818,9 @@ export const {
parseTraceparent, startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey,
parseTraceparent, startSpan, contextFromHeaders, ulid, newEventId, newIdempotencyKey,
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
auth, locker, registry, trust, modules, realtime,
auth, locker, registry, trust, modules, realtime,
- media, cam, community, homeFeed, bot, search,
- bench, meme, vision, themes, design, redact,
- chat, ai, credentials, jobs, tools, projects,
- geo,
+ media, cam, mediaUpload, ui, community, homeFeed,
+ bot, search, bench, meme, vision, themes,
+ design, redact, chat, ai, credentials, jobs,
+ tools, projects, geo,
} = sdk;
} = sdk;
export default sdk;
export default sdk;
@@ -251,6 +251,11 @@
@@ -251,6 +251,11 @@
"mediaUrls"
"mediaUrls"
]
]
},
},
+ "mediaUpload": {
+ "object": [
+ "createUploadClient"
+ ]
+ },
"meme": {
"meme": {
"object": [
"object": [
"createMemeClient"
"createMemeClient"
@@ -409,6 +414,11 @@
@@ -409,6 +414,11 @@
"isNew"
"isNew"
]
]
},
},
+ "ui": {
+ "object": [
+ "progress"
+ ]
+ },
"ulid": "function",
"ulid": "function",
"usage": {
"usage": {
"object": [
"object": [
@@ -877,6 +887,9 @@
@@ -877,6 +887,9 @@
"createObjectsClient": "function",
"createObjectsClient": "function",
"mediaUrls": "function"
"mediaUrls": "function"
},
},
+ "openvibe-sdk/media-upload": {
+ "createUploadClient": "function"
+ },
"openvibe-sdk/meme": {
"openvibe-sdk/meme": {
"createMemeClient": "function"
"createMemeClient": "function"
},
},
@@ -1166,6 +1179,9 @@
@@ -1166,6 +1179,9 @@
"createTrustClient": "function",
"createTrustClient": "function",
"isNew": "function"
"isNew": "function"
},
},
+ "openvibe-sdk/ui": {
+ "progress": "function"
+ },
"openvibe-sdk/usage": {
"openvibe-sdk/usage": {
"createUsageReporter": "function",
"createUsageReporter": "function",
"usageKey": "function",
"usageKey": "function",
@@ -7,13 +7,13 @@ export const {
@@ -7,13 +7,13 @@ export const {
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
isActingSubjectId, satisfiesRange, compareVersions, CONTRACTS_RANGE, DEFAULT_NETWORK, SDK_VERSION,
report, share, locker, auth, registry, identity,
report, share, locker, auth, registry, identity,
trust, modules, events, realtime, media, cam,
trust, modules, events, realtime, media, cam,
- community, homeFeed, bot, search, bench, meme,
- vision, themes, design, redact, chat, chatApps,
- ai, credentials, jobs, tools, projects, resources,
- vip, quest, live, inventory, tips, blog,
- reviews, wiki, coupons, deals, shoes, trade,
- space, host, news, food, actor, run,
- video, work, zone, play, download, openre,
- geo, usage, commerce, telemetry,
+ mediaUpload, ui, community, homeFeed, bot, search,
+ bench, meme, vision, themes, design, redact,
+ chat, chatApps, ai, credentials, jobs, tools,
+ projects, resources, vip, quest, live, inventory,
+ tips, blog, reviews, wiki, coupons, deals,
+ shoes, trade, space, host, news, food,
+ actor, run, video, work, zone, play,
+ download, openre, geo, usage, commerce, telemetry,
} = cjs;
} = cjs;
export default cjs;
export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/media-upload.js. Do not edit.
+import cjs from '../src/media-upload.js';
+
+export const {
+ createUploadClient,
+} = cjs;
+export default cjs;
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+// Generated by scripts/esm.js from src/ui.js. Do not edit.
+import cjs from '../src/ui.js';
+
+export const {
+ progress,
+} = cjs;
+export default cjs;
@@ -21,6 +21,8 @@ module.exports = {
@@ -21,6 +21,8 @@ module.exports = {
realtime: require('./src/realtime'),
realtime: require('./src/realtime'),
media: require('./src/media'),
media: require('./src/media'),
cam: require('./src/cam'),
cam: require('./src/cam'),
+ mediaUpload: require('./src/media-upload'),
+ ui: require('./src/ui'),
community: require('./src/community'),
community: require('./src/community'),
homeFeed: require('./src/home-feed'),
homeFeed: require('./src/home-feed'),
bot: require('./src/bot'),
bot: require('./src/bot'),
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.119.0",
+ "version": "0.120.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.119.0",
+ "version": "0.120.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-download": "bin/openvibe-download.js"
"openvibe-download": "bin/openvibe-download.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-sdk",
"name": "openvibe-sdk",
- "version": "0.119.0",
+ "version": "0.120.0",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"description": "Supported browser and server clients for the OpenVibe platform: auth (incl. developer apps), registry discovery, identity, user modules, events, realtime, media, community, Bot robots, search, chat, AI runs, Tools (registry, run API and jobs), VIP memberships and developer projects, the async PostgreSQL data layer (db) and the Valkey cache, limits store, queue and pub/sub, with deadlines, safe retries, problem errors and trace propagation.",
"main": "index.js",
"main": "index.js",
"types": "types/index.d.ts",
"types": "types/index.d.ts",
@@ -84,6 +84,16 @@
@@ -84,6 +84,16 @@
"import": "./esm/media.mjs",
"import": "./esm/media.mjs",
"default": "./src/media.js"
"default": "./src/media.js"
},
},
+ "./media-upload": {
+ "types": "./types/media-upload.d.ts",
+ "import": "./esm/media-upload.mjs",
+ "default": "./src/media-upload.js"
+ },
+ "./ui": {
+ "types": "./types/ui.d.ts",
+ "import": "./esm/ui.mjs",
+ "default": "./src/ui.js"
+ },
"./meme": {
"./meme": {
"types": "./types/meme.d.ts",
"types": "./types/meme.d.ts",
"browser": "./src/meme.js",
"browser": "./src/meme.js",
@@ -34,6 +34,8 @@ const ENTRIES = {
@@ -34,6 +34,8 @@ const ENTRIES = {
'cam.mjs': '../src/cam/index.js',
'cam.mjs': '../src/cam/index.js',
'realtime.mjs': '../src/realtime.js',
'realtime.mjs': '../src/realtime.js',
'media.mjs': '../src/media.js',
'media.mjs': '../src/media.js',
+ 'media-upload.mjs': '../src/media-upload.js',
+ 'ui.mjs': '../src/ui.js',
'community.mjs': '../src/community.js',
'community.mjs': '../src/community.js',
'bot.mjs': '../src/bot.js',
'bot.mjs': '../src/bot.js',
'machines.mjs': '../src/machines.js',
'machines.mjs': '../src/machines.js',
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
@@ -8,7 +8,7 @@ const { paginate, offsetPager } = require('./paginate');
const semver = require('./semver');
const semver = require('./semver');
/** The package version (test/version.test.js keeps it equal to package.json). */
/** The package version (test/version.test.js keeps it equal to package.json). */
-const SDK_VERSION = '0.119.0';
+const SDK_VERSION = '0.120.0';
module.exports = {
module.exports = {
createClient,
createClient,
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+'use strict';
+const { createObjectsClient } = require('./media');
+const MIB = 1024 * 1024;
+
+/** Media's resumable multipart transport, configured for 8 MiB parts and three in flight. */
+function createUploadClient(config) {
+ const objects = createObjectsClient({ partSize: 8 * MIB, concurrency: 3, ...config });
+ return {
+ upload(file, options = {}) {
+ return objects.upload(file, { kind: 'file', filename: file && file.name, ...options });
+ },
+ resume(ref, file, options) { return objects.resume(ref, file, options); },
+ objects,
+ };
+}
+
+module.exports = { createUploadClient };
@@ -10,6 +10,7 @@
@@ -10,6 +10,7 @@
* delete, media.object.read to list and get. Media refuses a user JWT here: a browser uploads
* delete, media.object.read to list and get. Media refuses a user JWT here: a browser uploads
* through its own app server, which names the user with X-OV-User-Id (the `actingUserId` option).
* through its own app server, which names the user with X-OV-User-Id (the `actingUserId` option).
* Browser-safe code (FormData/Blob), but the credentials it needs belong on a server.
* Browser-safe code (FormData/Blob), but the credentials it needs belong on a server.
+ * The upload facade lives at openvibe-sdk/media-upload so this frozen module keeps its names.
*
*
* Sandbox app tokens land in the project's sandbox tenant (`<prj_…>-sandbox`, shown as `app_id`),
* Sandbox app tokens land in the project's sandbox tenant (`<prj_…>-sandbox`, shown as `app_id`),
* whose files are never served publicly: Media answers them with `sandbox: true`, a signed,
* whose files are never served publicly: Media answers them with `sandbox: true`, a signed,
@@ -225,6 +225,7 @@ function createObjectsClient({
@@ -225,6 +225,7 @@ function createObjectsClient({
}
}
const up = created.upload || {};
const up = created.upload || {};
if (up.multipart) {
if (up.multipart) {
+ if (opts.onSession) await opts.onSession({ objectId: created.id, uploadId: up.multipart.upload_id });
return finishMultipart(created.id, up.multipart.upload_id, sessionIo(up.multipart, { objectId: created.id }), body, { signal, onProgress, contentHash, total });
return finishMultipart(created.id, up.multipart.upload_id, sessionIo(up.multipart, { objectId: created.id }), body, { signal, onProgress, contentHash, total });
}
}
await presigned(up.url, {
await presigned(up.url, {
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+'use strict';
+
+/** Mount the shared OpenVibe progress UI after loading /shared/ov-upload.js and .css. */
+function progress(el, options) {
+ const upload = globalThis.OVUpload;
+ if (!upload || typeof upload.progress !== 'function') throw new Error('ov.ui.progress: load /shared/ov-upload.js first');
+ return upload.progress(el, options);
+}
+
+module.exports = { progress };
@@ -11,6 +11,7 @@ const crypto = require('node:crypto');
@@ -11,6 +11,7 @@ const crypto = require('node:crypto');
const { stubServer, send, problem, run } = require('./helpers');
const { stubServer, send, problem, run } = require('./helpers');
const { createClient } = require('../src/core');
const { createClient } = require('../src/core');
const { createObjectsClient } = require('../src/media');
const { createObjectsClient } = require('../src/media');
+const { createUploadClient } = require('../src/media-upload');
const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');
const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');
@@ -188,10 +189,15 @@ run([
@@ -188,10 +189,15 @@ run([
const m = await mockMedia();
const m = await mockMedia();
const objects = createObjectsClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), multipartThreshold: 10000, partSize: 4096, concurrency: 3 });
const objects = createObjectsClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), multipartThreshold: 10000, partSize: 4096, concurrency: 3 });
const bytes = crypto.randomBytes(4096 * 4 + 100);
const bytes = crypto.randomBytes(4096 * 4 + 100);
- const obj = await objects.upload(new Blob([bytes], { type: 'video/webm' }), { kind: 'vod' });
+ let ref;
+ const obj = await objects.upload(new Blob([bytes], { type: 'video/webm' }), { kind: 'vod', onSession: (value) => {
+ ref = value;
+ assert.equal(m.srv.requests.filter((r) => r.method === 'PUT').length, 0, 'session is offered for persistence before bytes leave');
+ } });
const init = JSON.parse(m.srv.requests.find((r) => r.url === '/api/v2/demo/objects').body);
const init = JSON.parse(m.srv.requests.find((r) => r.url === '/api/v2/demo/objects').body);
assert.deepEqual([init.multipart, init.part_size, init.mime_type, init.content_hash], [true, 4096, 'video/webm', sha(bytes)]);
assert.deepEqual([init.multipart, init.part_size, init.mime_type, init.content_hash], [true, 4096, 'video/webm', sha(bytes)]);
const s = [...m.state.sessions.values()][0];
const s = [...m.state.sessions.values()][0];
+ assert.deepEqual(ref, { objectId: obj.id, uploadId: s.upload_id });
assert.equal(s.parts_expected, 5);
assert.equal(s.parts_expected, 5);
const puts = m.srv.requests.filter((r) => r.method === 'PUT');
const puts = m.srv.requests.filter((r) => r.method === 'PUT');
assert.equal(puts.length, 5);
assert.equal(puts.length, 5);
@@ -203,6 +209,18 @@ run([
@@ -203,6 +209,18 @@ run([
await m.srv.close();
await m.srv.close();
}],
}],
+ ['the upload facade uses Media multipart and reports progress', async () => {
+ const m = await mockMedia();
+ const media = createUploadClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), multipartThreshold: 100, partSize: 1024 });
+ const bytes = crypto.randomBytes(2500);
+ const progress = [];
+ const obj = await media.upload(bytes, { onProgress: (p) => progress.push(p.uploadedBytes) });
+ assert.equal(obj.lifecycle_status, 'ready');
+ assert.equal(progress.at(-1), bytes.length);
+ assert.equal(JSON.parse(m.srv.requests.find((r) => r.url === '/api/v2/demo/objects').body).part_size, 1024);
+ await m.srv.close();
+ }],
+
['Media refusing a single part (413) sends it as multipart instead', async () => {
['Media refusing a single part (413) sends it as multipart instead', async () => {
const m = await mockMedia({ singleMax: 1000 });
const m = await mockMedia({ singleMax: 1000 });
const objects = createObjectsClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), partSize: 1024 });
const objects = createObjectsClient({ app: 'demo', baseUrl: m.srv.url, tokenClient: tokenClient(), partSize: 1024 });
@@ -72,7 +72,7 @@ run([
@@ -72,7 +72,7 @@ run([
})().catch((err) => { console.error(err); process.exit(1); });`;
})().catch((err) => { console.error(err); process.exit(1); });`;
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
const res = execFileSync(process.execPath, ['-e', script], { cwd: app, encoding: 'utf8' });
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
assert.match(res, new RegExp(`consumer ok: ${subpaths.length} subpaths \\+ the browser bundle`));
- assert.equal(subpaths.length, 96); // Adds the wallet client (0.119.0).
+ assert.equal(subpaths.length, 98); // Adds the upload client and the UI entry (0.120.0); the wallet came in 0.119.0.
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
assert.ok(!fs.existsSync(path.join(app, 'node_modules/openvibe-contracts')), 'the optional peer is not installed');
} finally {
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+'use strict';
+const assert = require('node:assert/strict');
+const sdk = require('../index');
+
+assert.throws(() => sdk.ui.progress({}), /load \/shared\/ov-upload\.js first/);
+const expected = { update() {}, destroy() {} };
+globalThis.OVUpload = { progress: (el, options) => { assert.equal(el.id, 'p'); assert.equal(typeof options.onRetry, 'function'); return expected; } };
+try { assert.equal(sdk.ui.progress({ id: 'p' }, { onRetry() {} }), expected); }
+finally { delete globalThis.OVUpload; }
@@ -5,6 +5,8 @@ import * as modules from './modules';
@@ -5,6 +5,8 @@ import * as modules from './modules';
import * as realtime from './realtime';
import * as realtime from './realtime';
import * as media from './media';
import * as media from './media';
import * as cam from './cam';
import * as cam from './cam';
+import * as mediaUpload from './media-upload';
+import * as ui from './ui';
import * as community from './community';
import * as community from './community';
import * as homeFeed from './home-feed';
import * as homeFeed from './home-feed';
import * as bot from './bot';
import * as bot from './bot';
@@ -34,6 +36,8 @@ export * as modules from './modules';
@@ -34,6 +36,8 @@ export * as modules from './modules';
export * as realtime from './realtime';
export * as realtime from './realtime';
export * as media from './media';
export * as media from './media';
export * as cam from './cam';
export * as cam from './cam';
+export * as mediaUpload from './media-upload';
+export * as ui from './ui';
export * as community from './community';
export * as community from './community';
export * as homeFeed from './home-feed';
export * as homeFeed from './home-feed';
export * as bot from './bot';
export * as bot from './bot';
@@ -57,6 +61,6 @@ export * as trust from './trust';
@@ -57,6 +61,6 @@ export * as trust from './trust';
declare const sdk: typeof core & {
declare const sdk: typeof core & {
createWalletClient: typeof createWalletClient;
createWalletClient: typeof createWalletClient;
auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;
auth: typeof auth; registry: typeof registry; modules: typeof modules; realtime: typeof realtime;
- media: typeof media; cam: typeof cam; community: typeof community; homeFeed: typeof homeFeed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo; locker: typeof locker; trust: typeof trust;
+ media: typeof media; cam: typeof cam; mediaUpload: typeof mediaUpload; ui: typeof ui; community: typeof community; homeFeed: typeof homeFeed; bot: typeof bot; search: typeof search; bench: typeof bench; meme: typeof meme; vision: typeof vision; themes: typeof themes; design: typeof design; redact: typeof redact; chat: typeof chat; ai: typeof ai; credentials: typeof credentials; jobs: typeof jobs; tools: typeof tools; projects: typeof projects; geo: typeof geo; locker: typeof locker; trust: typeof trust;
};
};
export default sdk;
export default sdk;
@@ -11,6 +11,8 @@ export * as events from './events';
@@ -11,6 +11,8 @@ export * as events from './events';
export * as realtime from './realtime';
export * as realtime from './realtime';
export * as media from './media';
export * as media from './media';
export * as cam from './cam';
export * as cam from './cam';
+export * as mediaUpload from './media-upload';
+export * as ui from './ui';
export * as community from './community';
export * as community from './community';
export * as homeFeed from './home-feed';
export * as homeFeed from './home-feed';
export * as bot from './bot';
export * as bot from './bot';
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+import type { createObjectsClient, UploadBody, ObjectUploadOptions, MediaObject, ObjectUploadRef, ObjectsClient } from './media';
+
+/** A ready-to-use multipart client with 8 MiB parts and three concurrent requests. */
+export declare function createUploadClient(opts: Parameters<typeof createObjectsClient>[0]): {
+ upload(file: UploadBody, options?: ObjectUploadOptions): Promise<MediaObject>;
+ resume(ref: ObjectUploadRef, file: UploadBody, options?: { signal?: AbortSignal; onProgress?: ObjectUploadOptions['onProgress']; contentHash?: string | false }): Promise<MediaObject>;
+ objects: ObjectsClient;
+};
@@ -124,6 +124,8 @@ export interface ObjectUploadOptions {
@@ -124,6 +124,8 @@ export interface ObjectUploadOptions {
uploadTtl?: number;
uploadTtl?: number;
signal?: AbortSignal;
signal?: AbortSignal;
onProgress?(p: { uploadedBytes: number; totalBytes: number; part?: number }): void;
onProgress?(p: { uploadedBytes: number; totalBytes: number; part?: number }): void;
+ /** Called as soon as Media creates a multipart session; persist this reference for reload resume. */
+ onSession?(ref: ObjectUploadRef): void | Promise<void>;
}
}
/** err.resume of an sdk.upload_incomplete error. */
/** err.resume of an sdk.upload_incomplete error. */
@@ -151,6 +153,7 @@ export interface ObjectsClient {
@@ -151,6 +153,7 @@ export interface ObjectsClient {
client: OpenVibeClient;
client: OpenVibeClient;
}
}
+
/**
/**
* Media's object API v2. Credentials: tokenClient (createServiceTokenClient; media.object.upload and
* Media's object API v2. Credentials: tokenClient (createServiceTokenClient; media.object.upload and
* media.object.read for namespace `app`), apiKey, or a client carrying its own. Without baseUrl the Media
* media.object.read for namespace `app`), apiKey, or a client carrying its own. Without baseUrl the Media
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+export type ProgressState = 'waiting' | 'sending' | 'paused' | 'processing' | 'done' | 'failed';
+export interface ProgressUpdate {
+ state?: ProgressState;
+ done?: number;
+ total?: number;
+ message?: string;
+}
+/** Requires the shared ov-upload.js and ov-upload.css assets on the page. */
+export declare function progress(el: HTMLElement, options?: { onRetry?(): void }): {
+ update(next: ProgressUpdate): void;
+ destroy(): void;
+};