openvibers/openvibe.sdk Commits
The public surface is pinned: docs/api-surface.json lists every name each entry exports, and a test fails when they disagree (#76)
Changes
@@@ -5,6 +5,10 @@ release may change an API and says so here.## 0.63.0 (2026-10-24)++- **The public surface is pinned.** `docs/api-surface.json` lists every name each of the 66 entries exports and what it++ is; `test/surface.test.js` fails when they disagree, and `node scripts/surface.js --check` names each change as++ breaking (a name gone, a kind changed, a constant's key gone) or added. The first step toward 1.0's freeze of++ `core`, `auth`, `events`, `db` and `media`. Nothing in the package's behaviour changes.- **Outbox relays pause on a 429.** `createPgOutbox` (and so `createServiceOutbox` and the usage relay) used to hitEvents' per-actor limit again on every tick after a 429, because the rows it claims next are under the same limit.Search's re-index bursts were refused 935 times on 2026-10-23, and every event was delivered only on a retry.@@@ -477,6 +477,7 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr## Versioning- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.++- [docs/api-surface.json](docs/api-surface.json) lists every name each entry exports and what it is (function, class, a plain object's keys, …). `test/surface.test.js` fails when the exports and the file disagree, so a removal or a change of kind is a reviewed line in a diff, never a surprise on a service's next pin round; `node scripts/surface.js --check` names each change as breaking or added, and `node scripts/surface.js` updates the file. 1.0 freezes `core`, `auth`, `events`, `db` and `media` first: a breaking change there is marked `[1.0 core]`.- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release, the `openvibe-contracts` devDependency (a release tarball pin, v0.86.0), which the tools tests also use to validate requests and answers. It is never a runtime or peer dependency.- A new public capability ships as a minor release: first the contract, then the wrapper here, then the service's route. Removing a wrapper is a major release, after the capability's deprecation window in Contracts has passed.nnew file mode 100644@@@ -0,0 +1,761 @@++{++ "openvibe-sdk": {++ "CONTRACTS_RANGE": "string",++ "DEFAULT_NETWORK": "string",++ "OpenVibeError": "class",++ "SDK_VERSION": "string",++ "actor": {++ "object": [++ "createActorClient"++ ]++ },++ "ai": {++ "object": [++ "OPS",++ "TERMINAL",++ "createAiClient"++ ]++ },++ "auth": {++ "object": [++ "TOKEN_VALID_AFTER",++ "base64url",++ "buildAuthorizeUrl",++ "createCodeVerifier",++ "createJwksClient",++ "createNetworkKeys",++ "createPgRevocationStore",++ "createPkcePair",++ "createServiceTokenClient",++ "createState",++ "decodeUnverified",++ "exchangeCode",++ "jwksClient",++ "jwksStatus",++ "pkceChallenge",++ "readCallback",++ "refreshUserToken",++ "revocationSchema",++ "startAuthorization",++ "unverifiedClaims",++ "verifyAppToken",++ "verifyServiceToken",++ "verifyUserToken"++ ]++ },++ "blog": {++ "object": [++ "createBlogClient"++ ]++ },++ "bot": {++ "object": [++ "actingHeaders",++ "createBotClient"++ ]++ },++ "chat": {++ "object": [++ "createChatClient"++ ]++ },++ "commerce": {++ "object": [++ "CommerceError",++ "createCommerceClient",++ "intentKey",++ "receipt"++ ]++ },++ "community": {++ "object": [++ "actingHeaders",++ "createCommunityClient"++ ]++ },++ "compareVersions": "function",++ "contextFromHeaders": "function",++ "coupons": {++ "object": [++ "createCouponsClient"++ ]++ },++ "createClient": "function",++ "deals": {++ "object": [++ "createDealsClient"++ ]++ },++ "download": {++ "object": [++ "contentTypeOf",++ "createDownloadClient"++ ]++ },++ "events": {++ "object": [++ "appSource",++ "createAppEvents",++ "createEventsClient",++ "createPgInbox",++ "createPgOutbox",++ "createServiceOutbox",++ "inboxSchema",++ "outboxSchema",++ "parseDelivery",++ "projectKey",++ "signDelivery",++ "signDeliveryHeaders",++ "signDeliveryV2",++ "verifyDelivery",++ "verifyDeliveryV2"++ ]++ },++ "food": {++ "object": [++ "createFoodClient"++ ]++ },++ "geo": {++ "object": [++ "createGeoClient"++ ]++ },++ "host": {++ "object": [++ "createHostClient"++ ]++ },++ "identity": {++ "object": [++ "createIdentityClient"++ ]++ },++ "inventory": {++ "object": [++ "createInventoryClient"++ ]++ },++ "isActingSubjectId": "function",++ "isOpenVibeError": "function",++ "jobs": {++ "object": [++ "TERMINAL_STATES",++ "createJobsClient",++ "isTerminal"++ ]++ },++ "live": {++ "object": [++ "createLiveClient"++ ]++ },++ "media": {++ "object": [++ "DEFAULT_PUBLIC_ORIGIN",++ "createMediaClient",++ "createObjectsClient",++ "mediaUrls"++ ]++ },++ "modules": {++ "object": [++ "createModulesClient"++ ]++ },++ "newEventId": "function",++ "newIdempotencyKey": "function",++ "news": {++ "object": [++ "createNewsClient"++ ]++ },++ "offsetPager": "function",++ "openre": {++ "object": [++ "createOpenReClient"++ ]++ },++ "paginate": "function",++ "parseTraceparent": "function",++ "play": {++ "object": [++ "createPlayClient"++ ]++ },++ "projects": {++ "object": [++ "createProjectsClient"++ ]++ },++ "quest": {++ "object": [++ "createQuestClient",++ "eventType",++ "projectKey"++ ]++ },++ "realtime": {++ "object": [++ "DEFAULT_ORIGIN",++ "createRealtimeClient",++ "parseSSE",++ "subscribe"++ ]++ },++ "registry": {++ "object": [++ "createRegistryClient"++ ]++ },++ "resources": {++ "object": [++ "RESOURCE_KINDS",++ "createResourceClient",++ "createResourceIndex",++ "parseResourceName",++ "resourceName",++ "resourceNameOf"++ ]++ },++ "reviews": {++ "object": [++ "createReviewsClient"++ ]++ },++ "run": {++ "object": [++ "RUN_JOB_STATES",++ "createRunClient"++ ]++ },++ "satisfiesRange": "function",++ "search": {++ "object": [++ "createSearchClient",++ "queryOf"++ ]++ },++ "space": {++ "object": [++ "createSpaceClient"++ ]++ },++ "startSpan": "function",++ "telemetry": {++ "object": [++ "createPgTelemetrySink",++ "createTelemetry",++ "pruneTelemetry",++ "startTelemetryRetention",++ "telemetrySample",++ "telemetrySchema",++ "validateTelemetrySample"++ ]++ },++ "tips": {++ "object": [++ "createTipsClient"++ ]++ },++ "tools": {++ "object": [++ "ToolRunError",++ "createToolsClient",++ "isToolRunError"++ ]++ },++ "trade": {++ "object": [++ "createTradeClient"++ ]++ },++ "ulid": "function",++ "usage": {++ "object": [++ "createUsageReporter",++ "usageKey",++ "usageSample",++ "validateUsageSample"++ ]++ },++ "video": {++ "object": [++ "createVideoClient"++ ]++ },++ "vip": {++ "object": [++ "createVipCache",++ "createVipClient"++ ]++ },++ "wiki": {++ "object": [++ "createWikiClient"++ ]++ },++ "work": {++ "object": [++ "WORK_APPLICATION_STATUSES",++ "createWorkClient"++ ]++ },++ "zone": {++ "object": [++ "createZoneClient"++ ]++ }++ },++ "openvibe-sdk/account-data": {++ "ACCOUNT_DATA_SCHEMA": "string",++ "TOPICS": "array",++ "createAccountData": "function",++ "createNetworkSender": "function",++ "startSubscriptions": "function"++ },++ "openvibe-sdk/actor": {++ "createActorClient": "function"++ },++ "openvibe-sdk/ai": {++ "OPS": "array",++ "TERMINAL": "object",++ "createAiClient": "function"++ },++ "openvibe-sdk/auth": {++ "TOKEN_VALID_AFTER": "string",++ "base64url": "function",++ "buildAuthorizeUrl": "function",++ "createCodeVerifier": "function",++ "createJwksClient": "function",++ "createNetworkKeys": "function",++ "createPgRevocationStore": "function",++ "createPkcePair": "function",++ "createServiceTokenClient": "function",++ "createState": "function",++ "decodeUnverified": "function",++ "exchangeCode": "function",++ "jwksClient": "function",++ "jwksStatus": "function",++ "pkceChallenge": "function",++ "readCallback": "function",++ "refreshUserToken": "function",++ "revocationSchema": "function",++ "startAuthorization": "function",++ "unverifiedClaims": "function",++ "verifyAppToken": "function",++ "verifyServiceToken": "function",++ "verifyUserToken": "function"++ },++ "openvibe-sdk/auth/browser": {++ "base64url": "function",++ "buildAuthorizeUrl": "function",++ "createCodeVerifier": "function",++ "createPkcePair": "function",++ "createState": "function",++ "decodeUnverified": "function",++ "pkceChallenge": "function",++ "readCallback": "function",++ "startAuthorization": "function",++ "unverifiedClaims": "function"++ },++ "openvibe-sdk/blog": {++ "createBlogClient": "function"++ },++ "openvibe-sdk/bot": {++ "actingHeaders": "function",++ "createBotClient": "function"++ },++ "openvibe-sdk/cache": {++ "createCache": "function"++ },++ "openvibe-sdk/chat": {++ "createChatClient": "function"++ },++ "openvibe-sdk/commerce": {++ "CommerceError": "class",++ "createCommerceClient": "function",++ "intentKey": "function",++ "receipt": "function"++ },++ "openvibe-sdk/community": {++ "actingHeaders": "function",++ "createCommunityClient": "function"++ },++ "openvibe-sdk/core": {++ "CONTRACTS_RANGE": "string",++ "DEFAULT_NETWORK": "string",++ "OpenVibeError": "class",++ "SDK_VERSION": "string",++ "compareVersions": "function",++ "contextFromHeaders": "function",++ "createClient": "function",++ "isActingSubjectId": "function",++ "isOpenVibeError": "function",++ "newEventId": "function",++ "newIdempotencyKey": "function",++ "offsetPager": "function",++ "paginate": "function",++ "parseTraceparent": "function",++ "satisfiesRange": "function",++ "startSpan": "function",++ "ulid": "function"++ },++ "openvibe-sdk/cost": {++ "costSnapshot": "function",++ "estimate": "function",++ "rateCard": "function",++ "validateCostSnapshot": "function",++ "validateRateCard": "function"++ },++ "openvibe-sdk/coupons": {++ "createCouponsClient": "function"++ },++ "openvibe-sdk/db": {++ "DbError": "class",++ "ISOLATION": {++ "object": [++ "read committed",++ "repeatable read",++ "serializable"++ ]++ },++ "PARSERS": {++ "object": [++ "1082",++ "1114",++ "114",++ "1184",++ "17",++ "1700",++ "20",++ "3802"++ ]++ },++ "createDb": "function",++ "sql": "function"++ },++ "openvibe-sdk/deals": {++ "createDealsClient": "function"++ },++ "openvibe-sdk/delivery": {++ "deliveryOffer": "function",++ "validateDeliveryOffer": "function"++ },++ "openvibe-sdk/download": {++ "contentTypeOf": "function",++ "createDownloadClient": "function"++ },++ "openvibe-sdk/events": {++ "appSource": "function",++ "createAppEvents": "function",++ "createEventsClient": "function",++ "createPgInbox": "function",++ "createPgOutbox": "function",++ "createServiceOutbox": "function",++ "inboxSchema": "function",++ "outboxSchema": "function",++ "parseDelivery": "function",++ "projectKey": "function",++ "signDelivery": "function",++ "signDeliveryHeaders": "function",++ "signDeliveryV2": "function",++ "verifyDelivery": "function",++ "verifyDeliveryV2": "function"++ },++ "openvibe-sdk/food": {++ "createFoodClient": "function"++ },++ "openvibe-sdk/frame": {++ "DEFAULT_BASE": "string",++ "frameConfig": "function",++ "frameTags": "function",++ "mountFrame": "function",++ "scriptUrls": "function"++ },++ "openvibe-sdk/geo": {++ "createGeoClient": "function"++ },++ "openvibe-sdk/govern": {++ "UNITS": "array",++ "WINDOWS": {++ "object": [++ "day",++ "hour",++ "minute",++ "month"++ ]++ },++ "createGovernor": "function"++ },++ "openvibe-sdk/host": {++ "createHostClient": "function"++ },++ "openvibe-sdk/identity": {++ "createIdentityClient": "function"++ },++ "openvibe-sdk/inventory": {++ "createInventoryClient": "function"++ },++ "openvibe-sdk/jobs": {++ "TERMINAL_STATES": "array",++ "createJobsClient": "function",++ "isTerminal": "function"++ },++ "openvibe-sdk/limits": {++ "WINDOWS": {++ "object": [++ "day",++ "hour",++ "minute"++ ]++ },++ "createActorLimiter": "function",++ "createValkeyLimitStore": "function",++ "defaultActor": "function"++ },++ "openvibe-sdk/live": {++ "createLiveClient": "function"++ },++ "openvibe-sdk/media": {++ "DEFAULT_PUBLIC_ORIGIN": "string",++ "createMediaClient": "function",++ "createObjectsClient": "function",++ "mediaUrls": "function"++ },++ "openvibe-sdk/modules": {++ "createModulesClient": "function"++ },++ "openvibe-sdk/news": {++ "createNewsClient": "function"++ },++ "openvibe-sdk/no-looks": {++ "RULES_VERSION": "string",++ "hasLooks": "function",++ "noLooks": "function",++ "noLooksTags": "function"++ },++ "openvibe-sdk/node": {++ "nodeCapabilities": "function",++ "nodeOffers": "function",++ "validateNodeCapabilities": "function"++ },++ "openvibe-sdk/notifications": {++ "createNotificationsClient": "function"++ },++ "openvibe-sdk/openre": {++ "createOpenReClient": "function"++ },++ "openvibe-sdk/placement": {++ "TRUST_CLASSES": "array",++ "TRUST_ORDER": "array",++ "canonical": "function",++ "createPlanHolder": "function",++ "excluded": "function",++ "forecast": "function",++ "marginalCost": "function",++ "ownedLoadCost": "function",++ "pickTwo": "function",++ "plan": "function",++ "priceOf": "function",++ "rendezvous": "function",++ "signPlan": "function",++ "verifyPlan": "function"++ },++ "openvibe-sdk/play": {++ "createPlayClient": "function"++ },++ "openvibe-sdk/projects": {++ "createProjectsClient": "function"++ },++ "openvibe-sdk/pubsub": {++ "createPubSub": "function"++ },++ "openvibe-sdk/quest": {++ "createQuestClient": "function",++ "eventType": "function",++ "projectKey": "function"++ },++ "openvibe-sdk/queue": {++ "backoff": "function",++ "createQueue": "function"++ },++ "openvibe-sdk/realtime": {++ "DEFAULT_ORIGIN": "string",++ "createRealtimeClient": "function",++ "parseSSE": "function",++ "subscribe": "function"++ },++ "openvibe-sdk/registry": {++ "createRegistryClient": "function"++ },++ "openvibe-sdk/resources": {++ "RESOURCE_KINDS": "array",++ "createResourceClient": "function",++ "createResourceIndex": "function",++ "parseResourceName": "function",++ "resourceName": "function",++ "resourceNameOf": "function"++ },++ "openvibe-sdk/reviews": {++ "createReviewsClient": "function"++ },++ "openvibe-sdk/run": {++ "RUN_JOB_STATES": "array",++ "createRunClient": "function"++ },++ "openvibe-sdk/runtime": {++ "RUNTIME_CLASSES": "array",++ "runtimeClass": "function",++ "runtimeOffer": "function",++ "validateRuntimeClass": "function",++ "validateRuntimeOffer": "function"++ },++ "openvibe-sdk/search": {++ "createSearchClient": "function",++ "queryOf": "function"++ },++ "openvibe-sdk/service": {++ "DEADLINE_MS": "number",++ "DEFAULT_INTERVAL_MS": "number",++ "DEFAULT_MAX_ROUTE_KEYS": "number",++ "DEFAULT_SKIP_PREFIXES": "array",++ "DRAIN_MS": "number",++ "HTTP_METHODS": "object",++ "LATENCY_SAMPLE_CAP": "number",++ "ServiceError": "class",++ "asServiceError": "function",++ "createHttpTelemetry": "function",++ "createReadiness": "lazy",++ "createRegistry": "lazy",++ "createRelease": "lazy",++ "createServiceError": "function",++ "createTelemetryMiddleware": "function",++ "defaultRouteLabel": "function",++ "eventLoopLagMs": "function",++ "eventLoopMonitorEnabled": "function",++ "gracefulStop": "function",++ "instrument": "lazy",++ "isLoopbackDirect": "lazy",++ "jsonBody": "function",++ "jsonErrors": "function",++ "metricsHandler": "lazy",++ "privateNoStore": "function",++ "problem": "lazy",++ "registerSignals": "function",++ "releaseInfo": "lazy",++ "run": "function",++ "safeReason": "lazy",++ "sendError": "function",++ "sendProblem": "lazy",++ "skip": "lazy",++ "startEventLoopMonitor": "function",++ "stopEventLoopMonitor": "function",++ "telemetry": {++ "object": [++ "count",++ "flush",++ "gauge",++ "init",++ "middleware",++ "record",++ "requestFinished",++ "requestStarted",++ "stop"++ ]++ },++ "telemetryMiddleware": "function",++ "telemetrySample": "function",++ "telemetrySkipped": "function",++ "validateTelemetrySample": "function",++ "within": "function",++ "wrap": "function"++ },++ "openvibe-sdk/space": {++ "createSpaceClient": "function"++ },++ "openvibe-sdk/sso": {++ "COOKIES": {++ "object": [++ "access",++ "hint",++ "refresh"++ ]++ },++ "claimsToUser": "function",++ "createSsoClient": "function",++ "decodeJwtPayload": "function",++ "fedcmNonceMatches": "function",++ "parseCookies": "function",++ "sanitizeNext": "function",++ "withParam": "function"++ },++ "openvibe-sdk/storage": {++ "storageOffer": "function",++ "validateStorageOffer": "function"++ },++ "openvibe-sdk/telemetry": {++ "createPgTelemetrySink": "function",++ "createTelemetry": "function",++ "pruneTelemetry": "function",++ "startTelemetryRetention": "function",++ "telemetrySample": "function",++ "telemetrySchema": "function",++ "validateTelemetrySample": "function"++ },++ "openvibe-sdk/testing": {++ "DEFAULT_APP_CATALOG": "array",++ "DEFAULT_ORIGINS": {++ "object": [++ "community",++ "events",++ "media",++ "network",++ "tools"++ ]++ },++ "DEFAULT_TOOLS_SATELLITES": "array",++ "createMockPlatform": "function",++ "createTestDb": "function",++ "createTestValkey": "function",++ "pgAvailable": "function",++ "valkeyAvailable": "function"++ },++ "openvibe-sdk/tips": {++ "createTipsClient": "function"++ },++ "openvibe-sdk/tools": {++ "ToolRunError": "class",++ "createToolsClient": "function",++ "isToolRunError": "function"++ },++ "openvibe-sdk/trade": {++ "createTradeClient": "function"++ },++ "openvibe-sdk/usage": {++ "createUsageReporter": "function",++ "usageKey": "function",++ "usageSample": "function",++ "validateUsageSample": "function"++ },++ "openvibe-sdk/valkey": {++ "createValkey": "function"++ },++ "openvibe-sdk/video": {++ "createVideoClient": "function"++ },++ "openvibe-sdk/vip": {++ "createVipCache": "function",++ "createVipClient": "function"++ },++ "openvibe-sdk/wiki": {++ "createWikiClient": "function"++ },++ "openvibe-sdk/work": {++ "WORK_APPLICATION_STATUSES": "array",++ "createWorkClient": "function"++ },++ "openvibe-sdk/zone": {++ "createZoneClient": "function"++ }++}@@@ -410,7 +410,8 @@"scripts": {"test": "node test/run.js","esm": "node scripts/esm.js",-- "browser-bundle": "node scripts/browser-bundle.js"++ "browser-bundle": "node scripts/browser-bundle.js",++ "surface": "node scripts/surface.js"},"engines": {"node": ">=20"nnew file mode 100644@@@ -0,0 +1,112 @@++#!/usr/bin/env node++'use strict';++/**++ * The public surface: every name each package entry exports, and what it is, in docs/api-surface.json. About 40++ * services pin this package, so a name that disappears or changes kind breaks them on their next pin round; the file++ * makes every such change a reviewed line in a diff, and test/surface.test.js fails when the exports and the file++ * disagree.++ *++ * node scripts/surface.js write docs/api-surface.json from the exports++ * node scripts/surface.js --check exit 1, listing what changed, when they disagree++ *++ * For each entry (`openvibe-sdk`, `openvibe-sdk/core`, …): name → "function" | "class" | "array" | "string" |++ * "number" | "boolean" | "null", { "object": [its keys] } for a plain object (constants, error codes), one level++ * deep, or "lazy" for a getter that loads an optional peer package (`openvibe-sdk/service`'s openvibe-shared parts):++ * it is never called here. A name removed, a kind changed or an object key removed is breaking; anything new is an++ * addition.++ */++const fs = require('fs');++const path = require('path');++++const ROOT = path.join(__dirname, '..');++const FILE = path.join(ROOT, 'docs', 'api-surface.json');++// What the 1.0 freeze covers first (README, "Versioning"): a change there is marked on its own.++const CORE = ['openvibe-sdk/core', 'openvibe-sdk/auth', 'openvibe-sdk/events', 'openvibe-sdk/db', 'openvibe-sdk/media'];++++function kindOf(v) {++ if (typeof v === 'function') return /^class[\s{]/.test(Function.prototype.toString.call(v)) ? 'class' : 'function';++ if (v === null) return 'null';++ if (Array.isArray(v)) return 'array';++ return typeof v;++}++++function isPlainObject(v) {++ if (!v || typeof v !== 'object' || Array.isArray(v)) return false;++ const proto = Object.getPrototypeOf(v);++ return proto === Object.prototype || proto === null;++}++++/** One module's exports as the file records them. */++function describe(mod) {++ const out = {};++ for (const name of Object.keys(mod).sort()) {++ const d = Object.getOwnPropertyDescriptor(mod, name);++ if (d && d.get) { out[name] = 'lazy'; continue; }++ const v = mod[name];++ out[name] = isPlainObject(v) ? { object: Object.keys(v).sort() } : kindOf(v);++ }++ return out;++}++++/** Every entry in package.json "exports" (the browser bundle and package.json itself aside), by its import name. */++function current() {++ const pkg = require(path.join(ROOT, 'package.json'));++ const surface = {};++ for (const sub of Object.keys(pkg.exports).sort()) {++ if (sub === './package.json' || sub.endsWith('.mjs')) continue;++ const spec = sub === '.' ? 'openvibe-sdk' : `openvibe-sdk/${sub.slice(2)}`;++ surface[spec] = describe(require(spec));++ }++ return surface;++}++++const kindName = (k) => (typeof k === 'string' ? k : 'object');++++/** → { breaking: [...], added: [...] } between two surfaces. */++function diff(before, after) {++ const breaking = [];++ const added = [];++ const tag = (spec) => (CORE.includes(spec) ? ' [1.0 core]' : '');++ for (const spec of Object.keys(before)) {++ if (!(spec in after)) { breaking.push(`${spec}: the entry is gone${tag(spec)}`); continue; }++ const was = before[spec];++ const now = after[spec];++ for (const name of Object.keys(was)) {++ const a = was[name];++ const b = now[name];++ if (b === undefined) breaking.push(`${spec}: ${name} is gone${tag(spec)}`);++ else if (a !== 'lazy' && b !== 'lazy' && kindName(a) !== kindName(b)) breaking.push(`${spec}: ${name} was a ${kindName(a)}, now a ${kindName(b)}${tag(spec)}`);++ else if (typeof a !== 'string') {++ for (const k of a.object) if (!b.object.includes(k)) breaking.push(`${spec}: ${name}.${k} is gone${tag(spec)}`);++ for (const k of b.object) if (!a.object.includes(k)) added.push(`${spec}: ${name}.${k}`);++ }++ }++ for (const name of Object.keys(now)) if (!(name in was)) added.push(`${spec}: ${name}`);++ }++ for (const spec of Object.keys(after)) if (!(spec in before)) added.push(`${spec} (a new entry)`);++ return { breaking, added };++}++++function render(surface) {++ return `${JSON.stringify(surface, null, 2)}\n`;++}++++function main(argv = process.argv.slice(2)) {++ const now = current();++ if (!argv.includes('--check')) {++ fs.writeFileSync(FILE, render(now));++ console.log(`wrote ${path.relative(ROOT, FILE)}: ${Object.keys(now).length} entries, ${Object.values(now).reduce((n, m) => n + Object.keys(m).length, 0)} names`);++ return 0;++ }++ const recorded = fs.existsSync(FILE) ? JSON.parse(fs.readFileSync(FILE, 'utf8')) : {};++ const { breaking, added } = diff(recorded, now);++ if (!breaking.length && !added.length) return 0;++ if (breaking.length) console.error(`Breaking (say so in CHANGELOG.md; from 1.0 on it needs a major release):\n ${breaking.join('\n ')}`);++ if (added.length) console.error(`Added:\n ${added.join('\n ')}`);++ console.error('Then run `node scripts/surface.js` to update docs/api-surface.json.');++ return 1;++}++++if (require.main === module) process.exitCode = main();++++module.exports = { current, describe, diff, render, CORE, FILE };nnew file mode 100644@@@ -0,0 +1,43 @@++'use strict';++/**++ * docs/api-surface.json (scripts/surface.js) matches what every entry exports, so a name that disappears or changes++ * kind is a reviewed line in a diff, never a surprise on a service's next pin round; and the diff calls it breaking.++ */++const assert = require('node:assert/strict');++const fs = require('node:fs');++const { run } = require('./helpers');++const { current, describe, diff, render, CORE, FILE } = require('../scripts/surface');++++run([++ ['docs/api-surface.json lists every export (node scripts/surface.js updates it)', async () => {++ const recorded = JSON.parse(fs.readFileSync(FILE, 'utf8'));++ const { breaking, added } = diff(recorded, current());++ assert.deepEqual({ breaking, added }, { breaking: [], added: [] }, 'run `node scripts/surface.js` and say what changed in CHANGELOG.md');++ assert.equal(fs.readFileSync(FILE, 'utf8'), render(current()), 'the file is exactly what the script writes');++ }],++++ ['the 1.0 core entries are in it', async () => {++ const recorded = JSON.parse(fs.readFileSync(FILE, 'utf8'));++ for (const spec of CORE) assert.ok(Object.keys(recorded[spec] || {}).length > 0, spec);++ assert.equal(recorded['openvibe-sdk/core'].createClient, 'function');++ }],++++ ['what each kind of export records', async () => {++ class Thing {}++ const mod = { f() {}, Thing, list: [1], codes: { B: 1, A: 2 }, name: 'x', n: 1, on: true, none: null, map: new Map() };++ Object.defineProperty(mod, 'optional', { enumerable: true, get() { throw new Error('never read'); } });++ assert.deepEqual(describe(mod), {++ Thing: 'class', codes: { object: ['A', 'B'] }, f: 'function', list: 'array', map: 'object', n: 'number', name: 'string', none: 'null', on: 'boolean', optional: 'lazy',++ });++ }],++++ ['a removal, a kind change or a removed key is breaking; anything new is an addition', async () => {++ const before = { 'openvibe-sdk/core': { a: 'function', b: 'function', c: { object: ['X', 'Y'] }, d: 'lazy' }, 'openvibe-sdk/old': { z: 'function' } };++ const after = { 'openvibe-sdk/core': { a: 'function', b: 'class', c: { object: ['X', 'Z'] }, d: 'function', e: 'function' }, 'openvibe-sdk/new': { y: 'function' } };++ assert.deepEqual(diff(before, after), {++ breaking: ['openvibe-sdk/core: b was a function, now a class [1.0 core]', 'openvibe-sdk/core: c.Y is gone [1.0 core]', 'openvibe-sdk/old: the entry is gone'],++ added: ['openvibe-sdk/core: c.Z', 'openvibe-sdk/core: e', 'openvibe-sdk/new (a new entry)'],++ });++ assert.deepEqual(diff(before, before), { breaking: [], added: [] });++ }],++]);
@@ -5,6 +5,10 @@ release may change an API and says so here.
@@ -5,6 +5,10 @@ release may change an API and says so here.
## 0.63.0 (2026-10-24)
## 0.63.0 (2026-10-24)
+- **The public surface is pinned.** `docs/api-surface.json` lists every name each of the 66 entries exports and what it
+ is; `test/surface.test.js` fails when they disagree, and `node scripts/surface.js --check` names each change as
+ breaking (a name gone, a kind changed, a constant's key gone) or added. The first step toward 1.0's freeze of
+ `core`, `auth`, `events`, `db` and `media`. Nothing in the package's behaviour changes.
- **Outbox relays pause on a 429.** `createPgOutbox` (and so `createServiceOutbox` and the usage relay) used to hit
- **Outbox relays pause on a 429.** `createPgOutbox` (and so `createServiceOutbox` and the usage relay) used to hit
Events' per-actor limit again on every tick after a 429, because the rows it claims next are under the same limit.
Events' per-actor limit again on every tick after a 429, because the rows it claims next are under the same limit.
Search's re-index bursts were refused 935 times on 2026-10-23, and every event was delivered only on a retry.
Search's re-index bursts were refused 935 times on 2026-10-23, and every event was delivered only on a retry.
@@ -477,6 +477,7 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr
@@ -477,6 +477,7 @@ Install the drivers the service uses: `pg` and `iovalkey` (production), `@electr
## Versioning
## Versioning
- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.
- `openvibe-sdk` follows semver. While it is 0.x, a minor release may break an API, and [CHANGELOG.md](CHANGELOG.md) says so. Pin a tag.
+- [docs/api-surface.json](docs/api-surface.json) lists every name each entry exports and what it is (function, class, a plain object's keys, …). `test/surface.test.js` fails when the exports and the file disagree, so a removal or a change of kind is a reviewed line in a diff, never a surprise on a service's next pin round; `node scripts/surface.js --check` names each change as breaking or added, and `node scripts/surface.js` updates the file. 1.0 freezes `core`, `auth`, `events`, `db` and `media` first: a breaking change there is marked `[1.0 core]`.
- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release, the `openvibe-contracts` devDependency (a release tarball pin, v0.86.0), which the tools tests also use to validate requests and answers. It is never a runtime or peer dependency.
- Each release states the openvibe-contracts range it was tested against (`CONTRACTS_RANGE`), and `discover()` checks it at runtime. Contract types are copied into `types/contracts.d.ts`. `test/types.test.js` fails if they differ from the pinned Contracts release, the `openvibe-contracts` devDependency (a release tarball pin, v0.86.0), which the tools tests also use to validate requests and answers. It is never a runtime or peer dependency.
- A new public capability ships as a minor release: first the contract, then the wrapper here, then the service's route. Removing a wrapper is a major release, after the capability's deprecation window in Contracts has passed.
- A new public capability ships as a minor release: first the contract, then the wrapper here, then the service's route. Removing a wrapper is a major release, after the capability's deprecation window in Contracts has passed.
new file mode 100644
new file mode 100644
@@ -0,0 +1,761 @@
@@ -0,0 +1,761 @@
+{
+ "openvibe-sdk": {
+ "CONTRACTS_RANGE": "string",
+ "DEFAULT_NETWORK": "string",
+ "OpenVibeError": "class",
+ "SDK_VERSION": "string",
+ "actor": {
+ "object": [
+ "createActorClient"
+ ]
+ },
+ "ai": {
+ "object": [
+ "OPS",
+ "TERMINAL",
+ "createAiClient"
+ ]
+ },
+ "auth": {
+ "object": [
+ "TOKEN_VALID_AFTER",
+ "base64url",
+ "buildAuthorizeUrl",
+ "createCodeVerifier",
+ "createJwksClient",
+ "createNetworkKeys",
+ "createPgRevocationStore",
+ "createPkcePair",
+ "createServiceTokenClient",
+ "createState",
+ "decodeUnverified",
+ "exchangeCode",
+ "jwksClient",
+ "jwksStatus",
+ "pkceChallenge",
+ "readCallback",
+ "refreshUserToken",
+ "revocationSchema",
+ "startAuthorization",
+ "unverifiedClaims",
+ "verifyAppToken",
+ "verifyServiceToken",
+ "verifyUserToken"
+ ]
+ },
+ "blog": {
+ "object": [
+ "createBlogClient"
+ ]
+ },
+ "bot": {
+ "object": [
+ "actingHeaders",
+ "createBotClient"
+ ]
+ },
+ "chat": {
+ "object": [
+ "createChatClient"
+ ]
+ },
+ "commerce": {
+ "object": [
+ "CommerceError",
+ "createCommerceClient",
+ "intentKey",
+ "receipt"
+ ]
+ },
+ "community": {
+ "object": [
+ "actingHeaders",
+ "createCommunityClient"
+ ]
+ },
+ "compareVersions": "function",
+ "contextFromHeaders": "function",
+ "coupons": {
+ "object": [
+ "createCouponsClient"
+ ]
+ },
+ "createClient": "function",
+ "deals": {
+ "object": [
+ "createDealsClient"
+ ]
+ },
+ "download": {
+ "object": [
+ "contentTypeOf",
+ "createDownloadClient"
+ ]
+ },
+ "events": {
+ "object": [
+ "appSource",
+ "createAppEvents",
+ "createEventsClient",
+ "createPgInbox",
+ "createPgOutbox",
+ "createServiceOutbox",
+ "inboxSchema",
+ "outboxSchema",
+ "parseDelivery",
+ "projectKey",
+ "signDelivery",
+ "signDeliveryHeaders",
+ "signDeliveryV2",
+ "verifyDelivery",
+ "verifyDeliveryV2"
+ ]
+ },
+ "food": {
+ "object": [
+ "createFoodClient"
+ ]
+ },
+ "geo": {
+ "object": [
+ "createGeoClient"
+ ]
+ },
+ "host": {
+ "object": [
+ "createHostClient"
+ ]
+ },
+ "identity": {
+ "object": [
+ "createIdentityClient"
+ ]
+ },
+ "inventory": {
+ "object": [
+ "createInventoryClient"
+ ]
+ },
+ "isActingSubjectId": "function",
+ "isOpenVibeError": "function",
+ "jobs": {
+ "object": [
+ "TERMINAL_STATES",
+ "createJobsClient",
+ "isTerminal"
+ ]
+ },
+ "live": {
+ "object": [
+ "createLiveClient"
+ ]
+ },
+ "media": {
+ "object": [
+ "DEFAULT_PUBLIC_ORIGIN",
+ "createMediaClient",
+ "createObjectsClient",
+ "mediaUrls"
+ ]
+ },
+ "modules": {
+ "object": [
+ "createModulesClient"
+ ]
+ },
+ "newEventId": "function",
+ "newIdempotencyKey": "function",
+ "news": {
+ "object": [
+ "createNewsClient"
+ ]
+ },
+ "offsetPager": "function",
+ "openre": {
+ "object": [
+ "createOpenReClient"
+ ]
+ },
+ "paginate": "function",
+ "parseTraceparent": "function",
+ "play": {
+ "object": [
+ "createPlayClient"
+ ]
+ },
+ "projects": {
+ "object": [
+ "createProjectsClient"
+ ]
+ },
+ "quest": {
+ "object": [
+ "createQuestClient",
+ "eventType",
+ "projectKey"
+ ]
+ },
+ "realtime": {
+ "object": [
+ "DEFAULT_ORIGIN",
+ "createRealtimeClient",
+ "parseSSE",
+ "subscribe"
+ ]
+ },
+ "registry": {
+ "object": [
+ "createRegistryClient"
+ ]
+ },
+ "resources": {
+ "object": [
+ "RESOURCE_KINDS",
+ "createResourceClient",
+ "createResourceIndex",
+ "parseResourceName",
+ "resourceName",
+ "resourceNameOf"
+ ]
+ },
+ "reviews": {
+ "object": [
+ "createReviewsClient"
+ ]
+ },
+ "run": {
+ "object": [
+ "RUN_JOB_STATES",
+ "createRunClient"
+ ]
+ },
+ "satisfiesRange": "function",
+ "search": {
+ "object": [
+ "createSearchClient",
+ "queryOf"
+ ]
+ },
+ "space": {
+ "object": [
+ "createSpaceClient"
+ ]
+ },
+ "startSpan": "function",
+ "telemetry": {
+ "object": [
+ "createPgTelemetrySink",
+ "createTelemetry",
+ "pruneTelemetry",
+ "startTelemetryRetention",
+ "telemetrySample",
+ "telemetrySchema",
+ "validateTelemetrySample"
+ ]
+ },
+ "tips": {
+ "object": [
+ "createTipsClient"
+ ]
+ },
+ "tools": {
+ "object": [
+ "ToolRunError",
+ "createToolsClient",
+ "isToolRunError"
+ ]
+ },
+ "trade": {
+ "object": [
+ "createTradeClient"
+ ]
+ },
+ "ulid": "function",
+ "usage": {
+ "object": [
+ "createUsageReporter",
+ "usageKey",
+ "usageSample",
+ "validateUsageSample"
+ ]
+ },
+ "video": {
+ "object": [
+ "createVideoClient"
+ ]
+ },
+ "vip": {
+ "object": [
+ "createVipCache",
+ "createVipClient"
+ ]
+ },
+ "wiki": {
+ "object": [
+ "createWikiClient"
+ ]
+ },
+ "work": {
+ "object": [
+ "WORK_APPLICATION_STATUSES",
+ "createWorkClient"
+ ]
+ },
+ "zone": {
+ "object": [
+ "createZoneClient"
+ ]
+ }
+ },
+ "openvibe-sdk/account-data": {
+ "ACCOUNT_DATA_SCHEMA": "string",
+ "TOPICS": "array",
+ "createAccountData": "function",
+ "createNetworkSender": "function",
+ "startSubscriptions": "function"
+ },
+ "openvibe-sdk/actor": {
+ "createActorClient": "function"
+ },
+ "openvibe-sdk/ai": {
+ "OPS": "array",
+ "TERMINAL": "object",
+ "createAiClient": "function"
+ },
+ "openvibe-sdk/auth": {
+ "TOKEN_VALID_AFTER": "string",
+ "base64url": "function",
+ "buildAuthorizeUrl": "function",
+ "createCodeVerifier": "function",
+ "createJwksClient": "function",
+ "createNetworkKeys": "function",
+ "createPgRevocationStore": "function",
+ "createPkcePair": "function",
+ "createServiceTokenClient": "function",
+ "createState": "function",
+ "decodeUnverified": "function",
+ "exchangeCode": "function",
+ "jwksClient": "function",
+ "jwksStatus": "function",
+ "pkceChallenge": "function",
+ "readCallback": "function",
+ "refreshUserToken": "function",
+ "revocationSchema": "function",
+ "startAuthorization": "function",
+ "unverifiedClaims": "function",
+ "verifyAppToken": "function",
+ "verifyServiceToken": "function",
+ "verifyUserToken": "function"
+ },
+ "openvibe-sdk/auth/browser": {
+ "base64url": "function",
+ "buildAuthorizeUrl": "function",
+ "createCodeVerifier": "function",
+ "createPkcePair": "function",
+ "createState": "function",
+ "decodeUnverified": "function",
+ "pkceChallenge": "function",
+ "readCallback": "function",
+ "startAuthorization": "function",
+ "unverifiedClaims": "function"
+ },
+ "openvibe-sdk/blog": {
+ "createBlogClient": "function"
+ },
+ "openvibe-sdk/bot": {
+ "actingHeaders": "function",
+ "createBotClient": "function"
+ },
+ "openvibe-sdk/cache": {
+ "createCache": "function"
+ },
+ "openvibe-sdk/chat": {
+ "createChatClient": "function"
+ },
+ "openvibe-sdk/commerce": {
+ "CommerceError": "class",
+ "createCommerceClient": "function",
+ "intentKey": "function",
+ "receipt": "function"
+ },
+ "openvibe-sdk/community": {
+ "actingHeaders": "function",
+ "createCommunityClient": "function"
+ },
+ "openvibe-sdk/core": {
+ "CONTRACTS_RANGE": "string",
+ "DEFAULT_NETWORK": "string",
+ "OpenVibeError": "class",
+ "SDK_VERSION": "string",
+ "compareVersions": "function",
+ "contextFromHeaders": "function",
+ "createClient": "function",
+ "isActingSubjectId": "function",
+ "isOpenVibeError": "function",
+ "newEventId": "function",
+ "newIdempotencyKey": "function",
+ "offsetPager": "function",
+ "paginate": "function",
+ "parseTraceparent": "function",
+ "satisfiesRange": "function",
+ "startSpan": "function",
+ "ulid": "function"
+ },
+ "openvibe-sdk/cost": {
+ "costSnapshot": "function",
+ "estimate": "function",
+ "rateCard": "function",
+ "validateCostSnapshot": "function",
+ "validateRateCard": "function"
+ },
+ "openvibe-sdk/coupons": {
+ "createCouponsClient": "function"
+ },
+ "openvibe-sdk/db": {
+ "DbError": "class",
+ "ISOLATION": {
+ "object": [
+ "read committed",
+ "repeatable read",
+ "serializable"
+ ]
+ },
+ "PARSERS": {
+ "object": [
+ "1082",
+ "1114",
+ "114",
+ "1184",
+ "17",
+ "1700",
+ "20",
+ "3802"
+ ]
+ },
+ "createDb": "function",
+ "sql": "function"
+ },
+ "openvibe-sdk/deals": {
+ "createDealsClient": "function"
+ },
+ "openvibe-sdk/delivery": {
+ "deliveryOffer": "function",
+ "validateDeliveryOffer": "function"
+ },
+ "openvibe-sdk/download": {
+ "contentTypeOf": "function",
+ "createDownloadClient": "function"
+ },
+ "openvibe-sdk/events": {
+ "appSource": "function",
+ "createAppEvents": "function",
+ "createEventsClient": "function",
+ "createPgInbox": "function",
+ "createPgOutbox": "function",
+ "createServiceOutbox": "function",
+ "inboxSchema": "function",
+ "outboxSchema": "function",
+ "parseDelivery": "function",
+ "projectKey": "function",
+ "signDelivery": "function",
+ "signDeliveryHeaders": "function",
+ "signDeliveryV2": "function",
+ "verifyDelivery": "function",
+ "verifyDeliveryV2": "function"
+ },
+ "openvibe-sdk/food": {
+ "createFoodClient": "function"
+ },
+ "openvibe-sdk/frame": {
+ "DEFAULT_BASE": "string",
+ "frameConfig": "function",
+ "frameTags": "function",
+ "mountFrame": "function",
+ "scriptUrls": "function"
+ },
+ "openvibe-sdk/geo": {
+ "createGeoClient": "function"
+ },
+ "openvibe-sdk/govern": {
+ "UNITS": "array",
+ "WINDOWS": {
+ "object": [
+ "day",
+ "hour",
+ "minute",
+ "month"
+ ]
+ },
+ "createGovernor": "function"
+ },
+ "openvibe-sdk/host": {
+ "createHostClient": "function"
+ },
+ "openvibe-sdk/identity": {
+ "createIdentityClient": "function"
+ },
+ "openvibe-sdk/inventory": {
+ "createInventoryClient": "function"
+ },
+ "openvibe-sdk/jobs": {
+ "TERMINAL_STATES": "array",
+ "createJobsClient": "function",
+ "isTerminal": "function"
+ },
+ "openvibe-sdk/limits": {
+ "WINDOWS": {
+ "object": [
+ "day",
+ "hour",
+ "minute"
+ ]
+ },
+ "createActorLimiter": "function",
+ "createValkeyLimitStore": "function",
+ "defaultActor": "function"
+ },
+ "openvibe-sdk/live": {
+ "createLiveClient": "function"
+ },
+ "openvibe-sdk/media": {
+ "DEFAULT_PUBLIC_ORIGIN": "string",
+ "createMediaClient": "function",
+ "createObjectsClient": "function",
+ "mediaUrls": "function"
+ },
+ "openvibe-sdk/modules": {
+ "createModulesClient": "function"
+ },
+ "openvibe-sdk/news": {
+ "createNewsClient": "function"
+ },
+ "openvibe-sdk/no-looks": {
+ "RULES_VERSION": "string",
+ "hasLooks": "function",
+ "noLooks": "function",
+ "noLooksTags": "function"
+ },
+ "openvibe-sdk/node": {
+ "nodeCapabilities": "function",
+ "nodeOffers": "function",
+ "validateNodeCapabilities": "function"
+ },
+ "openvibe-sdk/notifications": {
+ "createNotificationsClient": "function"
+ },
+ "openvibe-sdk/openre": {
+ "createOpenReClient": "function"
+ },
+ "openvibe-sdk/placement": {
+ "TRUST_CLASSES": "array",
+ "TRUST_ORDER": "array",
+ "canonical": "function",
+ "createPlanHolder": "function",
+ "excluded": "function",
+ "forecast": "function",
+ "marginalCost": "function",
+ "ownedLoadCost": "function",
+ "pickTwo": "function",
+ "plan": "function",
+ "priceOf": "function",
+ "rendezvous": "function",
+ "signPlan": "function",
+ "verifyPlan": "function"
+ },
+ "openvibe-sdk/play": {
+ "createPlayClient": "function"
+ },
+ "openvibe-sdk/projects": {
+ "createProjectsClient": "function"
+ },
+ "openvibe-sdk/pubsub": {
+ "createPubSub": "function"
+ },
+ "openvibe-sdk/quest": {
+ "createQuestClient": "function",
+ "eventType": "function",
+ "projectKey": "function"
+ },
+ "openvibe-sdk/queue": {
+ "backoff": "function",
+ "createQueue": "function"
+ },
+ "openvibe-sdk/realtime": {
+ "DEFAULT_ORIGIN": "string",
+ "createRealtimeClient": "function",
+ "parseSSE": "function",
+ "subscribe": "function"
+ },
+ "openvibe-sdk/registry": {
+ "createRegistryClient": "function"
+ },
+ "openvibe-sdk/resources": {
+ "RESOURCE_KINDS": "array",
+ "createResourceClient": "function",
+ "createResourceIndex": "function",
+ "parseResourceName": "function",
+ "resourceName": "function",
+ "resourceNameOf": "function"
+ },
+ "openvibe-sdk/reviews": {
+ "createReviewsClient": "function"
+ },
+ "openvibe-sdk/run": {
+ "RUN_JOB_STATES": "array",
+ "createRunClient": "function"
+ },
+ "openvibe-sdk/runtime": {
+ "RUNTIME_CLASSES": "array",
+ "runtimeClass": "function",
+ "runtimeOffer": "function",
+ "validateRuntimeClass": "function",
+ "validateRuntimeOffer": "function"
+ },
+ "openvibe-sdk/search": {
+ "createSearchClient": "function",
+ "queryOf": "function"
+ },
+ "openvibe-sdk/service": {
+ "DEADLINE_MS": "number",
+ "DEFAULT_INTERVAL_MS": "number",
+ "DEFAULT_MAX_ROUTE_KEYS": "number",
+ "DEFAULT_SKIP_PREFIXES": "array",
+ "DRAIN_MS": "number",
+ "HTTP_METHODS": "object",
+ "LATENCY_SAMPLE_CAP": "number",
+ "ServiceError": "class",
+ "asServiceError": "function",
+ "createHttpTelemetry": "function",
+ "createReadiness": "lazy",
+ "createRegistry": "lazy",
+ "createRelease": "lazy",
+ "createServiceError": "function",
+ "createTelemetryMiddleware": "function",
+ "defaultRouteLabel": "function",
+ "eventLoopLagMs": "function",
+ "eventLoopMonitorEnabled": "function",
+ "gracefulStop": "function",
+ "instrument": "lazy",
+ "isLoopbackDirect": "lazy",
+ "jsonBody": "function",
+ "jsonErrors": "function",
+ "metricsHandler": "lazy",
+ "privateNoStore": "function",
+ "problem": "lazy",
+ "registerSignals": "function",
+ "releaseInfo": "lazy",
+ "run": "function",
+ "safeReason": "lazy",
+ "sendError": "function",
+ "sendProblem": "lazy",
+ "skip": "lazy",
+ "startEventLoopMonitor": "function",
+ "stopEventLoopMonitor": "function",
+ "telemetry": {
+ "object": [
+ "count",
+ "flush",
+ "gauge",
+ "init",
+ "middleware",
+ "record",
+ "requestFinished",
+ "requestStarted",
+ "stop"
+ ]
+ },
+ "telemetryMiddleware": "function",
+ "telemetrySample": "function",
+ "telemetrySkipped": "function",
+ "validateTelemetrySample": "function",
+ "within": "function",
+ "wrap": "function"
+ },
+ "openvibe-sdk/space": {
+ "createSpaceClient": "function"
+ },
+ "openvibe-sdk/sso": {
+ "COOKIES": {
+ "object": [
+ "access",
+ "hint",
+ "refresh"
+ ]
+ },
+ "claimsToUser": "function",
+ "createSsoClient": "function",
+ "decodeJwtPayload": "function",
+ "fedcmNonceMatches": "function",
+ "parseCookies": "function",
+ "sanitizeNext": "function",
+ "withParam": "function"
+ },
+ "openvibe-sdk/storage": {
+ "storageOffer": "function",
+ "validateStorageOffer": "function"
+ },
+ "openvibe-sdk/telemetry": {
+ "createPgTelemetrySink": "function",
+ "createTelemetry": "function",
+ "pruneTelemetry": "function",
+ "startTelemetryRetention": "function",
+ "telemetrySample": "function",
+ "telemetrySchema": "function",
+ "validateTelemetrySample": "function"
+ },
+ "openvibe-sdk/testing": {
+ "DEFAULT_APP_CATALOG": "array",
+ "DEFAULT_ORIGINS": {
+ "object": [
+ "community",
+ "events",
+ "media",
+ "network",
+ "tools"
+ ]
+ },
+ "DEFAULT_TOOLS_SATELLITES": "array",
+ "createMockPlatform": "function",
+ "createTestDb": "function",
+ "createTestValkey": "function",
+ "pgAvailable": "function",
+ "valkeyAvailable": "function"
+ },
+ "openvibe-sdk/tips": {
+ "createTipsClient": "function"
+ },
+ "openvibe-sdk/tools": {
+ "ToolRunError": "class",
+ "createToolsClient": "function",
+ "isToolRunError": "function"
+ },
+ "openvibe-sdk/trade": {
+ "createTradeClient": "function"
+ },
+ "openvibe-sdk/usage": {
+ "createUsageReporter": "function",
+ "usageKey": "function",
+ "usageSample": "function",
+ "validateUsageSample": "function"
+ },
+ "openvibe-sdk/valkey": {
+ "createValkey": "function"
+ },
+ "openvibe-sdk/video": {
+ "createVideoClient": "function"
+ },
+ "openvibe-sdk/vip": {
+ "createVipCache": "function",
+ "createVipClient": "function"
+ },
+ "openvibe-sdk/wiki": {
+ "createWikiClient": "function"
+ },
+ "openvibe-sdk/work": {
+ "WORK_APPLICATION_STATUSES": "array",
+ "createWorkClient": "function"
+ },
+ "openvibe-sdk/zone": {
+ "createZoneClient": "function"
+ }
+}
@@ -410,7 +410,8 @@
@@ -410,7 +410,8 @@
"scripts": {
"scripts": {
"test": "node test/run.js",
"test": "node test/run.js",
"esm": "node scripts/esm.js",
"esm": "node scripts/esm.js",
- "browser-bundle": "node scripts/browser-bundle.js"
+ "browser-bundle": "node scripts/browser-bundle.js",
+ "surface": "node scripts/surface.js"
},
},
"engines": {
"engines": {
"node": ">=20"
"node": ">=20"
new file mode 100644
new file mode 100644
@@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * The public surface: every name each package entry exports, and what it is, in docs/api-surface.json. About 40
+ * services pin this package, so a name that disappears or changes kind breaks them on their next pin round; the file
+ * makes every such change a reviewed line in a diff, and test/surface.test.js fails when the exports and the file
+ * disagree.
+ *
+ * node scripts/surface.js write docs/api-surface.json from the exports
+ * node scripts/surface.js --check exit 1, listing what changed, when they disagree
+ *
+ * For each entry (`openvibe-sdk`, `openvibe-sdk/core`, …): name → "function" | "class" | "array" | "string" |
+ * "number" | "boolean" | "null", { "object": [its keys] } for a plain object (constants, error codes), one level
+ * deep, or "lazy" for a getter that loads an optional peer package (`openvibe-sdk/service`'s openvibe-shared parts):
+ * it is never called here. A name removed, a kind changed or an object key removed is breaking; anything new is an
+ * addition.
+ */
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const FILE = path.join(ROOT, 'docs', 'api-surface.json');
+// What the 1.0 freeze covers first (README, "Versioning"): a change there is marked on its own.
+const CORE = ['openvibe-sdk/core', 'openvibe-sdk/auth', 'openvibe-sdk/events', 'openvibe-sdk/db', 'openvibe-sdk/media'];
+
+function kindOf(v) {
+ if (typeof v === 'function') return /^class[\s{]/.test(Function.prototype.toString.call(v)) ? 'class' : 'function';
+ if (v === null) return 'null';
+ if (Array.isArray(v)) return 'array';
+ return typeof v;
+}
+
+function isPlainObject(v) {
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
+ const proto = Object.getPrototypeOf(v);
+ return proto === Object.prototype || proto === null;
+}
+
+/** One module's exports as the file records them. */
+function describe(mod) {
+ const out = {};
+ for (const name of Object.keys(mod).sort()) {
+ const d = Object.getOwnPropertyDescriptor(mod, name);
+ if (d && d.get) { out[name] = 'lazy'; continue; }
+ const v = mod[name];
+ out[name] = isPlainObject(v) ? { object: Object.keys(v).sort() } : kindOf(v);
+ }
+ return out;
+}
+
+/** Every entry in package.json "exports" (the browser bundle and package.json itself aside), by its import name. */
+function current() {
+ const pkg = require(path.join(ROOT, 'package.json'));
+ const surface = {};
+ for (const sub of Object.keys(pkg.exports).sort()) {
+ if (sub === './package.json' || sub.endsWith('.mjs')) continue;
+ const spec = sub === '.' ? 'openvibe-sdk' : `openvibe-sdk/${sub.slice(2)}`;
+ surface[spec] = describe(require(spec));
+ }
+ return surface;
+}
+
+const kindName = (k) => (typeof k === 'string' ? k : 'object');
+
+/** → { breaking: [...], added: [...] } between two surfaces. */
+function diff(before, after) {
+ const breaking = [];
+ const added = [];
+ const tag = (spec) => (CORE.includes(spec) ? ' [1.0 core]' : '');
+ for (const spec of Object.keys(before)) {
+ if (!(spec in after)) { breaking.push(`${spec}: the entry is gone${tag(spec)}`); continue; }
+ const was = before[spec];
+ const now = after[spec];
+ for (const name of Object.keys(was)) {
+ const a = was[name];
+ const b = now[name];
+ if (b === undefined) breaking.push(`${spec}: ${name} is gone${tag(spec)}`);
+ else if (a !== 'lazy' && b !== 'lazy' && kindName(a) !== kindName(b)) breaking.push(`${spec}: ${name} was a ${kindName(a)}, now a ${kindName(b)}${tag(spec)}`);
+ else if (typeof a !== 'string') {
+ for (const k of a.object) if (!b.object.includes(k)) breaking.push(`${spec}: ${name}.${k} is gone${tag(spec)}`);
+ for (const k of b.object) if (!a.object.includes(k)) added.push(`${spec}: ${name}.${k}`);
+ }
+ }
+ for (const name of Object.keys(now)) if (!(name in was)) added.push(`${spec}: ${name}`);
+ }
+ for (const spec of Object.keys(after)) if (!(spec in before)) added.push(`${spec} (a new entry)`);
+ return { breaking, added };
+}
+
+function render(surface) {
+ return `${JSON.stringify(surface, null, 2)}\n`;
+}
+
+function main(argv = process.argv.slice(2)) {
+ const now = current();
+ if (!argv.includes('--check')) {
+ fs.writeFileSync(FILE, render(now));
+ console.log(`wrote ${path.relative(ROOT, FILE)}: ${Object.keys(now).length} entries, ${Object.values(now).reduce((n, m) => n + Object.keys(m).length, 0)} names`);
+ return 0;
+ }
+ const recorded = fs.existsSync(FILE) ? JSON.parse(fs.readFileSync(FILE, 'utf8')) : {};
+ const { breaking, added } = diff(recorded, now);
+ if (!breaking.length && !added.length) return 0;
+ if (breaking.length) console.error(`Breaking (say so in CHANGELOG.md; from 1.0 on it needs a major release):\n ${breaking.join('\n ')}`);
+ if (added.length) console.error(`Added:\n ${added.join('\n ')}`);
+ console.error('Then run `node scripts/surface.js` to update docs/api-surface.json.');
+ return 1;
+}
+
+if (require.main === module) process.exitCode = main();
+
+module.exports = { current, describe, diff, render, CORE, FILE };
new file mode 100644
new file mode 100644
@@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
+'use strict';
+/**
+ * docs/api-surface.json (scripts/surface.js) matches what every entry exports, so a name that disappears or changes
+ * kind is a reviewed line in a diff, never a surprise on a service's next pin round; and the diff calls it breaking.
+ */
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const { run } = require('./helpers');
+const { current, describe, diff, render, CORE, FILE } = require('../scripts/surface');
+
+run([
+ ['docs/api-surface.json lists every export (node scripts/surface.js updates it)', async () => {
+ const recorded = JSON.parse(fs.readFileSync(FILE, 'utf8'));
+ const { breaking, added } = diff(recorded, current());
+ assert.deepEqual({ breaking, added }, { breaking: [], added: [] }, 'run `node scripts/surface.js` and say what changed in CHANGELOG.md');
+ assert.equal(fs.readFileSync(FILE, 'utf8'), render(current()), 'the file is exactly what the script writes');
+ }],
+
+ ['the 1.0 core entries are in it', async () => {
+ const recorded = JSON.parse(fs.readFileSync(FILE, 'utf8'));
+ for (const spec of CORE) assert.ok(Object.keys(recorded[spec] || {}).length > 0, spec);
+ assert.equal(recorded['openvibe-sdk/core'].createClient, 'function');
+ }],
+
+ ['what each kind of export records', async () => {
+ class Thing {}
+ const mod = { f() {}, Thing, list: [1], codes: { B: 1, A: 2 }, name: 'x', n: 1, on: true, none: null, map: new Map() };
+ Object.defineProperty(mod, 'optional', { enumerable: true, get() { throw new Error('never read'); } });
+ assert.deepEqual(describe(mod), {
+ Thing: 'class', codes: { object: ['A', 'B'] }, f: 'function', list: 'array', map: 'object', n: 'number', name: 'string', none: 'null', on: 'boolean', optional: 'lazy',
+ });
+ }],
+
+ ['a removal, a kind change or a removed key is breaking; anything new is an addition', async () => {
+ const before = { 'openvibe-sdk/core': { a: 'function', b: 'function', c: { object: ['X', 'Y'] }, d: 'lazy' }, 'openvibe-sdk/old': { z: 'function' } };
+ const after = { 'openvibe-sdk/core': { a: 'function', b: 'class', c: { object: ['X', 'Z'] }, d: 'function', e: 'function' }, 'openvibe-sdk/new': { y: 'function' } };
+ assert.deepEqual(diff(before, after), {
+ breaking: ['openvibe-sdk/core: b was a function, now a class [1.0 core]', 'openvibe-sdk/core: c.Y is gone [1.0 core]', 'openvibe-sdk/old: the entry is gone'],
+ added: ['openvibe-sdk/core: c.Z', 'openvibe-sdk/core: e', 'openvibe-sdk/new (a new entry)'],
+ });
+ assert.deepEqual(diff(before, before), { breaking: [], added: [] });
+ }],
+]);