openvibers/openvibe.sdk Commits

Add daily quest and step type SDK methods (#149)

OP OpenVibers committed 4 days ago · 4d3870c037 · 12 files, +200 −18

* Expose daily quests and step contributions in SDK

* Merge remote-tracking branch 'origin/main' into feat/daily-quest-sdk-1108

* Release daily quests in SDK 0.108.0

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 71c0e9b..06a3d30 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,9 @@
# Changelog
++## 0.108.0 — 2026-11-09
++
++- Add daily quest reads, progress, step type listings and reviewable step contributions to `openvibe-sdk/quest`.
++
## 0.107.0 — 2026-11-09
- Add `ov.cam.open()` for the Cam recorder, with typed modes, options and file results across Node and browser exports.
ddiff --git a/README.md b/README.md
iindex c8d016e..a220b7c 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Supported browser and server clients for the OpenVibe platform.
--**Status:** alpha, v0.107.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.108.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.
@@@ -11,7 +11,7 @@
After v0.97.1 is tagged:
```bash
--npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.107.0
++npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.108.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).
@@@ -195,7 +195,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/placement` | server | `plan(requirements, offers, {rateCards, states, now, current, minGain, weights, success})` → `platform.placement-result@1`; `place(requirements, offers, {…plan, person, step, floor, priors, outcomes, rules, names})` → `{result, line, ranked, rejected, fallback}`: the person's rules and ownership, the quality bar, cost per success and one sentence saying why; `optionKey`, `learn`, `successOf`, `escalate`, `lineFor`; `TRUST_CLASSES`, `TRUST_ORDER`, `DEFAULT_TRUST`; `planAi(requirements, deployments, {enabled, usage, evidence, rates, current})` → `{chosen, rejected, objective, estimate, explain}`: the AI Exchange face of the same planner (hard gates, the cost per success in nano-USD, sparse evidence marked), off by default ([details](docs/placement.md)) |
| `openvibe-sdk/play` | server | `createPlayClient(client)`: OpenVibe.Play's achievements and leaderboards for any game — `titles.define(title, {name, url?, summary?, achievements, boards})`, `scores.post(title, {board, ref, entries})`, `achievements.unlock(title, {player, achievements})`, `players.adopt(title, guest, person)`, `players.achievements(title, player)`; public `titles.list/get` and `boards.get(title, board, {period, page})` need no token |
| `openvibe-sdk/download` | server | `createDownloadClient({ key })`: OpenVibe.Download, a person's drive — `files.list/all/get/remove`, `folders.list/create/ensurePath(path)`, `upload({ path \| data, name, folderId })` in parts, `usage()`, and `sync(dir, { to, delete, dryRun, exclude })` backups. The `openvibe-download` command: `sync <folder> --to <drive/path>`, `ls`, `usage` (`OPENVIBE_DOWNLOAD_KEY`, a sync key from openvibe.download/sync) |
--| `openvibe-sdk/quest` | server | `createQuestClient(client)`: `appQuests.define(slug, {title, why?, steps: [{event_type, count, label}], badge: {name}, url?})`, `appQuests.list()`, `appQuests.retire(slug)` with an app token (audience `openvibe.quest`, `quest.app_quest.manage`); `eventType(projectId, name)` gives the `app.<project_key>.<name>` type to publish at Events. Badges only, never OpenCoins; production events count |
++| `openvibe-sdk/quest` | server | `createQuestClient(client)`: `daily()`, `myDaily()`, `stepTypes()`, `contributeStepType(step)` for the shared daily quest and reviewable event-backed steps (`quest.steps.contribute`); `appQuests.define/list/retire` for an app's own badge quests (`quest.app_quest.manage`); `eventType(projectId, name)` builds an app event topic. App quests pay badges only; production events count |
| `openvibe-sdk/live` | server | `createLiveClient(client)`: OpenVibe.Live's documented routes. `streams.list/get/recentlyOnline/mine/update/end/heartbeat/endpoint`, `slots.list/create/update/remove/regenerateKey` (a slot is a saved setup with its own stream key), `goLive(slotId, {title, category, tags})` → `{ stream, endpoint }`, `channels.get/mine/update/toggleFollow/url`, `content.feed/moments({type, sort, window, cursor, limit})`; WHIP: `whip.url(streamKey)`, `whip.publish(url, offerSdp, {key?})` → `{ answer, location }` (the client's own token never goes to `/whip`), `whip.end(location)`. Writes are never retried |
| `openvibe-sdk/streams` | server | `createStreamsClient(client, {baseUrl?})`: an agent's own stream on OpenVibe.Live (the Agent Streams plan, S1; `live.agent-stream@1`, an `ags_…` id with no stream key and no slot). `startAgentStream({title, task, visibility, allowSites})` → `{stream, publish}` (the WHIP url and the one-time `wpt_…` ticket with its expiry), `renewTicket(id)` and `revokeTicket(id)` (the kill switch: Live ends the WHIP session at once), `state(id, {ticket})` (what the desk polls: viewers, `encode`, the curtain, `ends_at`), `postFrame(id, bytes, {ticket, contentType})` (the last frame while Live wants no video, JPEG or WebP at most 200 KB), `setCurtain(id, up, reason)`, `setVisibility(id, v)`, `end(id)` and `viewTicket(id)`. The publish ticket is a bearer of its own on the desk routes; a bad id, visibility, curtain reason or an oversized frame is refused before anything leaves. [Docs](docs/streams.md) |
| `openvibe-sdk/inventory` | server | `createInventoryClient(client)`: OpenVibe.Inventory, the network-wide inventory (ADR-054). Public reads: `kinds.list/get`, `definitions.list({kind, issuer})/get`, `people.items(subject)`, `people.equipped(subject)`, `equipped([subjects])` (≤ 100, for chat renders). The person's own: `me.items()`, `me.equip({kind, slot, instance_id | null})`. An issuer's: `definitions.create/update`, `grants.create({definition_id, subject, idempotency_key})` → `{ instance, created }` (retry-safe on the key), `instances.consume/revoke`. `forPerson(usr_…)` sends `X-OV-Subject` for a service acting for a person. Audience `openvibe.inventory` |
ddiff --git a/STATUS.json b/STATUS.json
iindex f7e6eef..a5f00be 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -3,7 +3,7 @@
"stage": "alpha",
"code": true,
"wave": "W2",
-- "version": "0.107.0",
++ "version": "0.108.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",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex 3a28f04..8e24903 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -1,4 +1,4 @@
--// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.107.0, MIT). Do not edit.
++// Generated by scripts/browser-bundle.js from browser.js (openvibe-sdk 0.108.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, redact, chat, ai, credentials, jobs, tools, projects, geo). No server-only code: no service tokens, JWT verification, events, identity.
/* eslint-disable */
@@@ -52,7 +52,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.107.0';
++const SDK_VERSION = '0.108.0';
module.exports = {
createClient,
ddiff --git a/package-lock.json b/package-lock.json
iindex b31fb17..53568ed 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-sdk",
-- "version": "0.107.0",
++ "version": "0.108.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-sdk",
-- "version": "0.107.0",
++ "version": "0.108.0",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
@@@ -14,7 +14,7 @@
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.352.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.355.0",
"pg": "^8.23.0"
},
"engines": {
@@@ -198,9 +198,9 @@
"license": "MIT"
},
"node_modules/openvibe-contracts": {
-- "version": "0.352.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.352.0",
-- "integrity": "sha512-TN02BrMU4jzXYZ0rjp5dmLbQI06h5tT6miISS6zS7E7/UFI+fGFidTVtQpZXEYG8gppGEo2UEtZ//hk9kkASyQ==",
++ "version": "0.355.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.355.0",
++ "integrity": "sha512-j3Pl93MLl/9hsIVqz4xwnlOVEUDRZlYDnmyaLTahMk46uxLJyLfLsOrJwR1N9jWublhJBhoBEc6q5fRj+bAwMw==",
"dev": true,
"license": "AGPL-3.0-only",
"dependencies": {
ddiff --git a/package.json b/package.json
iindex 462ff4b..3c87c44 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-sdk",
-- "version": "0.107.0",
++ "version": "0.108.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",
@@@ -584,7 +584,7 @@
"devDependencies": {
"@electric-sql/pglite": "^0.5.8",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.352.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.355.0",
"pg": "^8.23.0"
},
"peerDependencies": {
ddiff --git a/src/core/index.js b/src/core/index.js
iindex 583ed2e..145b34f 100644
---- a/src/core/index.js
++++ b/src/core/index.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.107.0';
++const SDK_VERSION = '0.108.0';
module.exports = {
createClient,
ddiff --git a/src/quest.js b/src/quest.js
iindex 4bf4551..bdf0475 100644
---- a/src/quest.js
++++ b/src/quest.js
@@@ -46,7 +46,17 @@ function createQuestClient(client, { baseUrl } = {}) {
/** Retire one: no longer counted or listed; its completions and badges stay. → quest.app-quest@1 */
retire: (slug, { signal } = {}) => call({ method: 'DELETE', path: `/api/v1/app-quests/${enc(slug)}`, signal }),
};
-- return { appQuests, eventType, projectKey };
++ return {
++ appQuests, eventType, projectKey,
++ /** The network-wide quest for today, or a past YYYY-MM-DD date (quest.daily@1). */
++ daily: ({ date, signal } = {}) => call({ path: date ? `/api/v1/daily/${enc(date)}` : '/api/v1/daily', signal }),
++ /** The person token's own checklist, completion and streak (quest.daily-progress@1). */
++ myDaily: ({ signal } = {}) => call({ path: '/api/v1/me/daily', signal }),
++ /** Approved event-backed step types (quest.step-type@1). */
++ stepTypes: ({ signal } = {}) => call({ path: '/api/v1/step-types', signal }),
++ /** Suggest a step; the app token needs quest.steps.contribute. Review precedes use. */
++ contributeStepType: (stepType, { signal } = {}) => call({ method: 'POST', path: '/api/v1/step-types', json: stepType, signal, idempotencyKey: false }),
++ };
}
module.exports = { createQuestClient, eventType, projectKey };
ddiff --git a/test/pack.test.js b/test/pack.test.js
iindex 3e8971d..1ccd251 100644
---- a/test/pack.test.js
++++ b/test/pack.test.js
@@@ -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, 93); // Cam recorder methods extend the existing cam subpath (0.107.0); the 93-subpath count is unchanged.
++ assert.equal(subpaths.length, 93); // Daily quest methods extend the existing quest subpath (0.108.0); the 93-subpath count is unchanged.
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 });
ddiff --git a/test/quest-daily.test.js b/test/quest-daily.test.js
nnew file mode 100644
iindex 0000000..501e486
---- /dev/null
++++ b/test/quest-daily.test.js
@@@ -0,0 +1,21 @@
++'use strict';
++const assert = require('node:assert/strict');
++const { createQuestClient } = require('../src/quest');
++
++const calls = [];
++const client = createQuestClient({ json: async (opts) => { calls.push(opts); return { ok: true }; } });
++(async () => {
++ await client.daily();
++ await client.daily({ date: '2026-11-08' });
++ await client.myDaily();
++ await client.stepTypes();
++ await client.contributeStepType({ id: 'chat', event_type: 'chat.room.message.created', site: 'openvibe.chat', sentence: 'Chat', href: 'https://openvibe.chat/' });
++ assert.deepEqual(calls.map((x) => [x.method || 'GET', x.path]), [
++ ['GET', '/api/v1/daily'], ['GET', '/api/v1/daily/2026-11-08'], ['GET', '/api/v1/me/daily'],
++ ['GET', '/api/v1/step-types'], ['POST', '/api/v1/step-types'],
++ ]);
++ assert.equal(calls[4].audience, 'openvibe.quest');
++ assert.equal(calls[4].json.site, 'openvibe.chat');
++ assert.equal(calls[4].idempotencyKey, false);
++ process.stdout.write('quest daily SDK methods passed\n');
++})().catch((err) => { process.stderr.write(String(err.stack || err) + '\n'); process.exit(1); });
ddiff --git a/types/contracts.d.ts b/types/contracts.d.ts
iindex 0dc268c..c14174b 100644
---- a/types/contracts.d.ts
++++ b/types/contracts.d.ts
@@@ -1,4 +1,4 @@
--/* Copied verbatim from openvibe-contracts feat/sharing-core-contracts (0.291.0) generated/typescript/index.d.ts (the SDK does not
++/* Copied verbatim from openvibe-contracts v0.355.0 generated/typescript/index.d.ts (the SDK does not
* require the package at runtime; it is a devDependency for the tests). test/types.test.js checks
* these against it. */
/* eslint-disable */
@@@ -32,6 +32,7 @@ export type SubjectRef =
type: "agent";
id: string;
};
++
/** [email protected] (owner: network) */
/**
* Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service, app, mod, node or agent principal. Replaces X-Internal-Key. App tokens (actor_type app) also carry project_id and env; receivers refuse env=sandbox unless they opted in. Agent tokens (actor_type agent, sub agent:agt_<ULID>) are minted for the agent's host and also carry project_id, env and on_behalf_of (the owner), and optionally cap_confirm and act.
@@@ -111,6 +112,7 @@ export type ServiceTokenClaims = {
};
[k: string]: unknown | undefined;
};
++
/** [email protected] (owner: contracts) */
/**
* Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.
@@@ -125,6 +127,7 @@ export interface EntityRef {
*/
label?: string;
}
++
/** [email protected] (owner: contracts) */
/**
* RFC 9457 problem details (application/problem+json) with a stable OpenVibe error code and trace identifiers. The legacy 'error' string is allowed during migration so existing clients keep working.
@@@ -148,6 +151,7 @@ export interface Problem {
error?: string;
[k: string]: unknown | undefined;
}
++
/** [email protected] (owner: network) */
/**
* What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.
@@@ -160,6 +164,14 @@ export interface ServiceManifest {
repository: string;
domains: string[];
publicOrigin?: string;
++ /**
++ * Public service routes whose access comes from a person's session or token, rather than a service capability.
++ */
++ apiRoutes?: {
++ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
++ path: string;
++ responseSchema?: string;
++ }[];
health?: string;
/**
* Public HTTP routes implemented by this service, with their response contract where one exists.
@@@ -2194,6 +2206,7 @@ export interface ServiceManifest {
};
notes?: string;
}
++
/** [email protected] (owner: network) */
/**
* An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.
@@@ -2220,6 +2233,7 @@ export interface Capability {
*/
sensitive?: boolean;
}
++
/** [email protected] (owner: events) */
/**
* Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.
@@@ -2242,6 +2256,7 @@ export interface EventEnvelope {
};
payload: {};
}
++
/** [email protected] (owner: network) */
/**
* Policy for one user-module namespace: portable per-subject summaries and preferences stored by OpenVibe.Network. Never domain truth, money or authoritative game inventory (roadmap 4.3-4.5). The person's account decides what happens to their records in every namespace: when an account is removed, Network deletes all of its records; when two accounts are merged (ADR-029), in a namespace where both have a record the survivor keeps its own and gains only the top-level fields it lacks from the other (its own values always win), a record only the absorbed account had moves to the survivor, and the absorbed account's records are deleted. Each of these changes is announced as network.module.updated (reason subject_removed or subject_merged). That is separate from onOwnerRemoved, which says what happens to a namespace's records when the service that owns the namespace is retired.
@@@ -2300,6 +2315,7 @@ export interface ModuleNamespace {
defaults?: {};
}[];
}
++
/** [email protected] (owner: network) */
/**
* One subject's value in one namespace, as returned by Network. revision increases by one on every write; a write names the revision it read (If-Match) and fails with 412 if it moved.
@@@ -2316,6 +2332,7 @@ export interface ModuleRecord {
*/
updated_by?: string;
}
++
/** [email protected] (owner: codes) */
/**
* PROPOSAL (OpenVibe.Codes, roadmap Wave 20): an app release as the platform knows it. The app is a Network developer app (ADR-014): its id is the Network app id, its capabilities are only a request (the grants in Network are the authority), and trust tiers are metadata that never change a grant check. OpenVibe.Codes validates with this schema, loaded into openvibe-contracts' validator, until Contracts publishes it as codes.app-manifest@1.
@@@ -2388,6 +2405,7 @@ export interface AppManifest {
sdk?: string;
};
}
++
/** [email protected] (owner: tools) */
/**
* One tool on openvibe.tools as its registry describes it (GET /api/v1/tools/:id, and each item of GET /api/v1/tools; capability tools.tool.read; ADR-027). There is one descriptor per catalogue tool (GET /api/catalog.json tools[].id), built from the tool's own code, so its page, the run API, openvibe-sdk/tools, the OpenAPI document and the docs all read the same facts. EXECUTION is where the tool's engine runs for its page: client (in the browser; nothing leaves it), sync (a server request answered inline) or job (an asynchronous job on a satellite, tools.job@1). API says whether POST /api/v1/tools/{id}/run exposes the tool. A client tool has api true only when it also has a server engine (a pure transform that runs in Node), and its page stays browser-only. A page-only tool (yt) has api false and run null. A job tool's run creates a job of run.job.type whose input is { ...input, ...run.job.preset, tool: run.job.operation } (the preset and the operation always win). INPUT is the JSON Schema (2020-12) that a run request's input must match, embedded or as { $ref: https://openvibe.tools/api/v1/tools/{id}/schema#/$defs/input }. GET /api/v1/tools/:id/schema answers { $schema, $id, $defs: { input, output } }, and the list uses the $ref form. When api is false, $defs.input is false (the schema that accepts nothing), since there is no run to take an input. OUTPUT.SCHEMA describes result.data for every execution: inline and job tools alike. KEYWORDS (the catalogue's search terms) and EXAMPLES (sample runs for the docs and the OpenAPI document) are optional. NOT A TOOL: there is no planned status. A planned catalogue entry has no descriptor and is not listed, and neither is a mirror (a second build of another tool). GET /api/v1/tools/:id and /schema answer 404 tools.tool.not_found for either, with a detail that says the id is planned or names the tool it mirrors, as for an id that does not exist. RULES this schema enforces: api false has no run and no examples, and api true has a run and an input; an API job tool names its job, and only job tools name one; an API tool whose output is a file runs as a job (its results are served as job files); a tools.net.probe tool fetches (egress) and is never anonymous; an anonymous egress tool has a per-target throttle (limits.perTargetPerMinute); a client tool never fetches; an unavailable tool says why (statusReason); JSON output has a schema. contracts.tools.checkDescriptor(d) also checks what depends on the id (run.path, $ref targets), files.min <= files.max, and each example: its input against an embedded input schema and limits.maxInputBytes, and its files against files (count and accept).
@@@ -2607,6 +2625,7 @@ export interface ToolDescriptor {
*/
docs: string;
}
++
/** [email protected] (owner: tools) */
/**
* The answer of GET /api/v1/tools on openvibe.tools (capability tools.tool.read; ADR-027): every tool's descriptor (tools.tool@1), with input and output schemas in the { $ref } form (GET /api/v1/tools/:id has them embedded). Filters: ?family=, ?execution=client|sync|job, ?api=true|false, ?status=, ?q= (every word must appear in the id, name, summary or keywords); family, execution and status take comma lists. An unknown execution or status value, or an api that is not true or false, is 400 tools.query.invalid. Every listed tool has one of the descriptor's statuses: there is no planned status, so planned placeholders are not tools and are not listed, and neither are mirrors (GET /api/v1/tools/:id answers 404 tools.tool.not_found for both, with a detail saying why). Cacheable (ETag); updated_at changes when any descriptor does.
@@@ -2627,6 +2646,7 @@ export interface ToolList {
count: number;
}[];
}
++
/** [email protected] (owner: tools) */
/**
* Body of POST /api/v1/tools/{id}/run on openvibe.tools (capability tools.tool.run, or tools.net.probe for network probes; ADR-027). JSON, or multipart/form-data like POST /api/v1/jobs: the text parts input (JSON text), wait_ms and idempotency_key, and uploaded files as file or files parts. The tool gets the uploaded files first, then the files references, in order; the count must fit the descriptor's files.min and files.max. input must match the tool's input schema (tools.tool@1 input). A job tool's job input is { ...input, ...run.job.preset, tool: run.job.operation } (the preset and the operation always win). The Idempotency-Key header wins over idempotency_key; ?wait_ms= on the query does the same as the field. Answers tools.run@1: 200 finished (succeeded, or failed with the tool's own error), 202 + Location while a job is queued or running, 200 + Idempotent-Replayed: true for a replay. Refusals are problem+json: 400 tools.run.invalid (not JSON, a bad field, wrong file count), 401 token.*, 403 capability.denied, 404 tools.tool.not_found, 404 tools.tool.not_runnable (api false: a page-only tool), 404 tools.run.file_not_found (a reference the caller cannot read), 409 tools.job.idempotency_conflict, 413 tools.file.too_large, 415 tools.file.unsupported_type (checked against the bytes), 422 tools.input.invalid (errors[] with JSON pointers), 429 tools.quota.exceeded (Retry-After; the caller's allowance in the tool's quota class is spent), quota.exceeded or tools.job.too_many_active (Retry-After), 503 tools.tool.unavailable (the descriptor's status is unavailable), 503 tools.unavailable (a program or upstream the tool needs is missing), 503 tools.busy (Retry-After; every slot for this kind of work is taken). A job tool's own refusals (413 tools.pdf.too_many_pages, 422 tools.pdf.wrong_password…) come back as a failed run (tools.run@1 error).
@@@ -2659,6 +2679,7 @@ export interface ToolsRunRequest {
*/
idempotency_key?: string;
}
++
/** [email protected] (owner: tools) */
/**
* The answer of POST /api/v1/tools/{id}/run (tools.run-request@1; ADR-027). Finished: { state: succeeded, tool, result, took_ms } or { state: failed|cancelled, tool, error, took_ms }, with job set when the tool ran as a job. Not finished within wait_ms: { state: queued|running, tool, job, location }, sent with 202 and a Location header; follow the job there (GET, its events, its files; tools.job@1). result carries data (kind json, matching the descriptor's output schema), text (kind text) or files (kind file or files: the job's result files, downloaded from their url). took_ms runs from the request to the answer for an inline run, and from created_at to finished_at for a job. error is problem+json from the tool: tools.job.failed, tools.job.timeout, tools.job.cancelled, or a code the tool chose (tools.…), such as 413 tools.pdf.too_many_pages or 422 tools.pdf.wrong_password; its status is the problem's, not the HTTP answer's. A request refused before the tool ran is a problem+json answer, not a run. Also: 413 tools.input.too_large (the input exceeded a worker's memory or size limit), 504 tools.run.timeout (the run passed the descriptor's timeoutMs), 403 tools.origin.refused (a cookie-authenticated mutation from a foreign Origin), 401 tools.session_required (the tool is not anonymous and the call has no browser session or token), 404 tools.run.file_not_found (a files reference names no result the caller may read), 500 tools.run.failed (the engine failed without a more specific code).
@@@ -2734,6 +2755,7 @@ export type ToolsRun =
*/
location: string;
};
++
/** [email protected] (owner: tools) */
/**
* A Tools job as the jobs API answers it (OpenVibe.Tools apps/_shared/jobs/http.js, system.view; ADR-027): POST /api/v1/jobs (202 + Location, or 200 + Idempotent-Replayed: true), GET and DELETE /api/v1/jobs/:id, POST /api/v1/jobs/:id/retry, PUT and DELETE /api/v1/jobs/:id/references/:ref, the data of every SSE event on GET /api/v1/jobs/:id/events, and the job member of tools.run@1. A job is visible only to its owner (a person, a service/app principal, or one browser session); anyone else gets the same 404 tools.job.not_found as for a job that does not exist. result is null until the job succeeded; each result file is downloaded from its url (GET /api/v1/jobs/:id/files/:n, ?inline=1 for previews) and is stored as a private OpenVibe.Media object (storage media) or on the satellite until the job expires (storage local). expires_at is null while a reference keeps the result. error is problem+json: tools.job.failed, tools.job.timeout, tools.job.cancelled, or a code the tool chose (tools.…), such as 413 tools.pdf.too_many_pages or 422 tools.pdf.wrong_password, with server paths removed.
@@@ -2853,6 +2875,7 @@ export interface ToolsJob {
retried_by: string | null;
};
}
++
/** [email protected] (owner: tools) */
/**
* Body of POST /api/v1/jobs on the Tools satellites that run jobs (img, audio, docs; capability tools.job.create; OpenVibe.Tools apps/_shared/jobs/http.js). JSON, or multipart/form-data with the same fields as text parts (input as JSON text) and the input files as file or files parts. The Idempotency-Key header wins over idempotency_key: the same (owner, key) and request returns the same job (200 + Idempotent-Replayed: true), a different request under it is 409 tools.job.idempotency_conflict. Types: img.process (input { tool: convert|compress|resize|crop, … }, one image), audio.process ({ tool, … } as /api/process takes them, one audio or video file), docs.process ({ tool, … }, one PDF, or several files for merge and img2pdf). A format host fills in its format (webp.openvibe.tools converts to WebP). Answers tools.job@1 (202 + Location). Refusals are problem+json: 400 tools.job.invalid (input not a JSON object or over 16 KB, wrong file count, the type's own validation) or tools.job.unknown_type, 401 token.* or tools.job.no_owner, 403 capability.denied, 413 tools.job.too_large, 429 tools.job.too_many_active (unfinished jobs per owner), tools.quota.exceeded (Retry-After; the owner's allowance is spent) or a rate limit, 503 tools.job.unavailable, 503 tools.unavailable (a program or upstream the job type needs is missing), 503 tools.busy (Retry-After; every slot for this kind of work is taken). A tool that refuses its input after the job was accepted fails the job with its own code (413 tools.pdf.too_many_pages, 422 tools.pdf.wrong_password…; tools.job@1 error).
@@@ -2871,3 +2894,118 @@ export interface ToolsJobRequest {
*/
idempotency_key?: string;
}
++
++/** [email protected] (owner: quest) */
++/**
++ * quest.daily@1: one quest shared by everyone on this UTC date. Progress is private and is not in this answer.
++ */
++export interface QuestDaily {
++ date: string;
++ title: string;
++ story: string;
++ /**
++ * @minItems 3
++ * @maxItems 3
++ */
++ steps: [QuestStepType, QuestStepType, QuestStepType];
++ source: "ai" | "rotation";
++ finished_today: number;
++}
++
++/** [email protected] (owner: quest) */
++/**
++ * quest.daily-progress@1: the caller's own progress, counted from Events in their IANA time zone.
++ */
++export interface QuestDailyProgress {
++ date: string;
++ title: string;
++ story: string;
++ /**
++ * @minItems 3
++ * @maxItems 3
++ */
++ steps: [
++ {
++ id: string;
++ event_type: string;
++ site: string;
++ sentence: string;
++ href: string;
++ weight: number;
++ count: 1;
++ verification: string;
++ status: "approved";
++ source: string;
++ progress: number;
++ done: boolean;
++ },
++ {
++ id: string;
++ event_type: string;
++ site: string;
++ sentence: string;
++ href: string;
++ weight: number;
++ count: 1;
++ verification: string;
++ status: "approved";
++ source: string;
++ progress: number;
++ done: boolean;
++ },
++ {
++ id: string;
++ event_type: string;
++ site: string;
++ sentence: string;
++ href: string;
++ weight: number;
++ count: 1;
++ verification: string;
++ status: "approved";
++ source: string;
++ progress: number;
++ done: boolean;
++ }
++ ];
++ source: "ai" | "rotation";
++ finished_today: number;
++ progress: number;
++ completed: boolean;
++ completed_at?: string | null;
++ streak: number;
++ time_zone: string;
++}
++
++/** [email protected] (owner: quest) */
++/**
++ * quest.step-type@1: a verified Events action that may be selected for a daily quest. A contributed type stays pending until staff review.
++ */
++export interface QuestStepType {
++ id: string;
++ event_type: string;
++ site: string;
++ sentence: string;
++ href: string;
++ weight: number;
++ count?: 1;
++ verification?: string;
++ status: "pending" | "approved" | "rejected";
++ source?: string;
++ project_id?: string;
++ created_at?: string;
++ reviewed_at?: string | null;
++}
++
++/** [email protected] (owner: quest) */
++/**
++ * A developer app's proposed daily step. Quest accepts only topics in its known Events catalog, verifies the site's host and queues the proposal for staff review.
++ */
++export interface QuestStepTypeRequest {
++ id: string;
++ event_type: string;
++ site: string;
++ sentence: string;
++ href: string;
++ weight?: number;
++}
ddiff --git a/types/quest.d.ts b/types/quest.d.ts
iindex a680ffb..8547ec7 100644
---- a/types/quest.d.ts
++++ b/types/quest.d.ts
@@@ -1,4 +1,5 @@
/** openvibe-sdk/quest: OpenVibe.Quest for developer apps — quests on your own events (contracts 0.193.0). */
++import type { QuestDaily, QuestDailyProgress, QuestStepType, QuestStepTypeRequest } from './contracts';
export interface AppQuestStep { event_type: string; count: number; label: string }
export interface AppQuestRequest {
title: string;
@@@ -14,6 +15,10 @@ export interface AppQuest {
status: 'active' | 'retired'; created_at: string; updated_at: string; completions: number;
}
export interface QuestClient {
++ daily(opts?: { date?: string; signal?: AbortSignal }): Promise<DailyQuest>;
++ myDaily(opts?: { signal?: AbortSignal }): Promise<DailyProgress>;
++ stepTypes(opts?: { signal?: AbortSignal }): Promise<{ step_types: QuestStepType[] }>;
++ contributeStepType(stepType: DailyStepTypeRequest, opts?: { signal?: AbortSignal }): Promise<QuestStepType>;
appQuests: {
define(slug: string, quest: AppQuestRequest, opts?: { signal?: AbortSignal }): Promise<AppQuest>;
list(opts?: { signal?: AbortSignal }): Promise<{ project_id: string; max: number; quests: AppQuest[] }>;
@@@ -22,6 +27,10 @@ export interface QuestClient {
eventType(projectId: string, name: string): string;
projectKey(projectId: string): string;
}
++export type DailyStepType = QuestStepType;
++export type DailyStepTypeRequest = QuestStepTypeRequest;
++export type DailyQuest = QuestDaily;
++export type DailyProgress = QuestDailyProgress;
export function createQuestClient(client: { json(opts: Record<string, unknown>): Promise<any> }, options?: { baseUrl?: string }): QuestClient;
export function eventType(projectId: string, name: string): string;
export function projectKey(projectId: string): string;