openvibers/openvibe.sdk Commits

OpenVibe Link: the new name in docs and comments (#139)

OP OpenVibers committed 5 days ago · 6aa3e907b4 · 19 files, +59 −59

OpenVibe Node is now OpenVibe Link. Pages, emails, notifications, docs, READMEs, error messages and tool descriptions say OpenVibe Link (or Link), and installer links point at openvibe.link. Code identifiers, the openvibe-node command, paths, contract ids and the repository name are unchanged.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a9331fc..4197410 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -91,19 +91,19 @@
idempotent per source and task ref) and `read()` gets one the caller may see. The Report service redacts every text
field; the client never sends a raw transcript.
- Add `examples/cli-agent-stream.js`: about 40 lines that start a stream, spawn any CLI command, post its steps and end
-- the stream with the command's result, using the same ticket path a Node desk uses.
++ the stream with the command's result, using the same ticket path a Link desk uses.
## 0.93.0 (2026-11-06)
- `openvibe-sdk/exchange`: the seller side gains the **endpoint form**, so any OpenAI-compatible server can sell, not
-- only an OpenVibe Node machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with
++ only an OpenVibe Link machine. `seller.offer({ endpoint, models, price, hours, concurrency, audience, name })` — with
no `machineId` — registers the server on OpenVibe.Exchange's public seller API
(`POST /api/v1/exchange/seller/offers`) with the same developer key the buyer side uses. The owner is the person the
key acts for (a developer app's project owner), never a body field, and the SDK refuses an endpoint that is not https
before anything is sent.
- The platform reaches that address only through its egress guard (https, a public DNS answer checked inside the
connection, never an IP literal, redirects judged again, a timeout and a response byte cap, no buyer identity in a
-- header) and verifies it exactly as a Node machine (canaries, the model fingerprint, about 1% spot re-runs, the free
++ header) and verifies it exactly as a machine running Link (canaries, the model fingerprint, about 1% spot re-runs, the free
tier until verified, self-dealing earns 0). Its answers are labelled `community-endpoint:<id>` on the receipt.
- `seller.endpointOffers()` lists the caller's endpoint offers with their verification;
`seller.removeEndpointOffer(id)` removes one. `machineId` still selects the Network registry form.
@@@ -137,7 +137,7 @@
- `state(id, { ticket })` is what the desk polls every ten seconds: `{ viewers, encode, curtain, ends_at }`. While `encode` is false the desk stops encoding and sends a snapshot every five seconds with `postFrame(id, bytes, { ticket, contentType })` (JPEG or WebP, at most 200 KB).
- `setCurtain(id, up, reason)` raises the desk's privacy screen (while it is up Live pauses every viewer's consumer, so a viewer sees "Private screen" and never why), `setVisibility(id, v)` switches who may watch, `end(id)` ends the stream (the card stays, with the last frame and the VOD) and `viewTicket(id)` mints the player's view ticket (ten minutes).
- The publish ticket is a bearer of its own: on the desk's routes (`state`, `postFrame`) the desk sends the ticket, never the person's token. Every body is checked in the module before it leaves, and a bad id, visibility, curtain reason or an oversized frame is refused with a TypeError.
-- - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Node principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream.
++ - The capability behind it is `live.agent_stream.publish` for the audience `openvibe.live` (Contracts 0.298.0): a Link principal acting for its owner, Codes or Actor. Never a developer-app token, never another person's stream.
## 0.90.0 (2026-11-06)
@@@ -158,7 +158,7 @@
- Add `openvibe-sdk/exchange`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders", item 7).
- The buyer side speaks the Exchange’s v1 API with a Network developer key: `chat()` (streaming and not, the OpenAI shape), `models()`, `offers(model)`, `catalog()`, `presets()` (list, create, update, delete), `usage()` and `receipt(id)`. Every call takes an `AbortSignal`, and a 429 or a 5xx is retried with a jittered backoff: a safe method always, a write only when it is idempotent, so a completion is never run twice.
-- - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe.Node’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime).
++ - The seller side registers an offer in the Network’s user-owned AI offers — the same registry OpenVibe Link’s "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability `network.resource.report`), so any server that speaks the OpenAI API can be a provider and not only a Node. The SDK sends the document that registry validates, field for field, and refuses an offer the registry would refuse before anything is sent; `earnings()` and `verification()` read the seller dashboard (the tokens served, the credits earned, the canaries, the spot re-runs, the reputation, the uptime).
- The webhooks (usage, earnings, an offer going down) carry `X-OpenVibe-Signature-V2`; `webhooks.verify` checks the raw body in constant time and refuses a timestamp more than 300 s either way.
## 0.88.0 (2026-11-06)
@@@ -381,8 +381,8 @@
of the step.
- **Streaming machines.** A step that asks for a GPU or 2 or more cores excludes an offer whose
`health.streaming` is `true`, with the reason "it is streaming". The field is not in
-- `platform.resource-offer@1` yet (Node reports `screen.state` in its own status, Bot's offer does not carry it),
-- so this release fixes the name the Node can start sending; a top-level `offer.streaming` is tolerated too.
++ `platform.resource-offer@1` yet (Link reports `screen.state` in its own status, Bot's offer does not carry it),
++ so this release fixes the name Link can start sending; a top-level `offer.streaming` is tolerated too.
- README and [docs/placement.md](docs/placement.md): the API, the rules, the estimate, escalation and the line
templates.
@@@ -391,8 +391,8 @@ release may change an API and says so here.
## 0.75.0 (2026-10-30)
--- **`openvibe-sdk/redact`: one secret scrubber for the Node and AI (Memory M2).**
-- - `redact(text)` → `{ text, count }` ports OpenVibe.Node's `internal/memory/redact.go` (Node #87): the same shapes,
++- **`openvibe-sdk/redact`: one secret scrubber for Link and AI (Memory M2).**
++ - `redact(text)` → `{ text, count }` ports OpenVibe Link's `internal/memory/redact.go` (Node #87): the same shapes,
the whole-text pass with a sentinel before the per-item pass, the unterminated private key block, Luhn card
numbers, and the deliberately wide environment-assignment rule. `onlySecret(text)` says an item was nothing but a
secret, so AI refuses it.
@@@ -485,13 +485,13 @@ release may change an API and says so here.
## 0.67.0 (2026-10-27)
--- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe.Node v0.27.0's ai plugin).
++- **`openvibe-sdk/bot`: a machine's own models** (AI on your own machines; OpenVibe Link v0.27.0's ai plugin).
- `bot.machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (bot.command@1
kind `ai`) and answers `{ model, message, usage?, ms? }`. A machine that does not answer throws an `OpenVibeError`
with code `bot.ai_nack`.
- `bot.machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
- Types: `Machine` for `machines.list()`, with `computer.ai` (the models and whether a server answers) and
-- `computer.ai_speed` (how fast the last model answered there, Node v0.29.0); `MachineChatInput` and `MachineChatResult`;
++ `computer.ai_speed` (how fast the last model answered there, Link v0.29.0); `MachineChatInput` and `MachineChatResult`;
`MachineCommandInput.kind` takes `ai`.
## 0.66.0 (2026-10-27)
@@@ -499,7 +499,7 @@ release may change an API and says so here.
- **`openvibe-sdk/bot`: machines and desks** (the Desks plan; the roadmap's SDK section). `bot.machines.list()`,
`.computer(id)`, `.command(id, { kind, value })` (bot.command@1's computer kinds) and `.deskTurn(id, desk)`.
- **`bot.machines.desk(id, { name?, kind?, profile? })`** is a desk of your own on a machine: a browser alone or a whole
-- screen, with its own files and sign-ins (OpenVibe.Node v0.19.0 to v0.26.0).
++ screen, with its own files and sign-ins (OpenVibe Link v0.19.0 to v0.26.0).
- `start`, `stop` and `status`.
- `browser`, `screen`, `input` and `exec` on that desk only, never on the person's own screen. `exec` takes
`detach` for an app that keeps running until the desk stops.
ddiff --git a/README.md b/README.md
iindex 53b6f94..3c0d07b 100644
---- a/README.md
++++ b/README.md
@@@ -206,7 +206,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/trade` | server | `createTradeClient(client)`: OpenVibe.Trade. `instruments.resolve(q)/list/get/observations(symbol, {metric, before, limit})/documents`, `sources()`; the person's `watchlists.list/get/create/rename/add/removeItem/remove` and `alerts.list/deliveries/create/remove`. Public capabilities since openvibe-contracts 0.208.0. Audience `openvibe.trade` |
| `openvibe-sdk/space` | server | `createSpaceClient(client)`: OpenVibe.Space, static sites for apps and CI. `deploys.uploadArchive(site, tarGz, {activate, preview, root})`, `uploadFiles(site, [{path, data}], …)`, `uploadSourceBuild(site, archive, {ref, commit_sha})`, `list/get/log/activate/rollback(site, {deploy_id})/remove`; `projects.list/create/get/remove/quota/sites/members.set(p, principal, role)/remove`; `sites.create/get/remove`, `sites.config.get/set/reset` (`{ headers, redirects, spa }`), `sites.source.get/set/remove`; `domains.list/add/verify/remove`. An app reaches only the projects a person added it to (deployer, maintainer or owner). Public capabilities since openvibe-contracts 0.209.0. Audience `openvibe.space` |
| `openvibe-sdk/host` | server | `createHostClient(client)`: OpenVibe.Host's public hosting catalog, sent without a token. `providers.list/get(slug)`, `plans.list({kind, min_vcpu, min_ram_mb, min_disk_gb, max_price, currency, location, provider, sort, cursor})/get(id)`, `promos.list`, `offers.list({sort, currency, max_monthly, source, priced, cursor})`, `domains.list/tld('dev')/check('example.dev')`, `benchmarks.list/get`. Any other filter is refused (422). Editing the catalog is first-party |
--| `openvibe-sdk/redact` | any | The shared secret scrubber (Memory M2): `redact(text)` → `{ text, count }` removes the tool and service keys, PEM private key blocks (including one with no END line), authorization headers, bearer tokens, chat webhooks, connection-string passwords, credential-named assignments and Luhn card numbers, keeping the words around them (`password=[redacted]`); `onlySecret(text)` says an item was nothing but a secret. The same shapes and corpus as OpenVibe.Node's `internal/memory/redact.go`, so the Node and AI cannot drift |
++| `openvibe-sdk/redact` | any | The shared secret scrubber (Memory M2): `redact(text)` → `{ text, count }` removes the tool and service keys, PEM private key blocks (including one with no END line), authorization headers, bearer tokens, chat webhooks, connection-string passwords, credential-named assignments and Luhn card numbers, keeping the words around them (`password=[redacted]`); `onlySecret(text)` says an item was nothing but a secret. The same shapes and corpus as OpenVibe Link's `internal/memory/redact.go`, so Link and AI cannot drift |
| `openvibe-sdk/explicit` | any | `isExplicit(...texts)`: true when AI text says a scene shows sexual content (body parts, sex acts, pornography, nudity); such a moment, clip or paste is not published |
| `openvibe-sdk/no-looks` | any | **Deprecated (2026-10-31):** AI text may describe people. `noLooks`, `noLooksTags` and `hasLooks` return what they are given; `isExplicit` is re-exported from `openvibe-sdk/explicit` |
| `openvibe-sdk/front-page` | any | What may lead a front page (a latest or most viewed list, a quote strip, a feed): `frontPageOk(text)` is false for a built-in slur category (OpenVibe.Chat's, spaced out or leet-spelled included), strong profanity in English, Portuguese, Spanish, French or German, or a hate topic out of context; `containsCoreSlur(text)` is the slur check alone. Leave such an item off the front page and show the next one; it stays on its own page |
@@@ -223,7 +223,7 @@ Until OpenVibe.Tools serves the gateway facade (Tools S6), point the jobs client
| `openvibe-sdk/usage` | server | `usageSample(fields)` builds a `platform.usage-sample@1` record (required `id`, `idempotency_key`, `service`, `operation`, `quantity`, `unit`; `at` defaults to now ISO, `source` to `openvibe-sdk/usage`; null fields stripped; money fields never invented); `usageKey(service, ...parts)` joins an idempotency key with `:`; `validateUsageSample(record)` → `{ok, errors}` (openvibe-contracts, required lazily). `createUsageReporter({ db, service, source, table, billingUrl, tokenClient, audience, requireProject, ... })` owns one service's readings: `key(...parts)`/`sample(fields)`, and `record(t, reading)` queues the reading idempotency-keyed in the outbox inside the caller's transaction, relayed to Billing's `billing.usage.record` with backoff (a retry is never dropped; only Billing refusing the reading itself — any 4xx but 401/403/404/408/425/429, which stay pending — marks a row rejected; a 401 drops the cached token). The reporter bills whatever reading it is given: never record first-party or sandbox traffic (pass `requireProject: true` to refuse a reading without a `prj_…` project). `openvibe-sdk/govern` builds its `onUsage` record with it |
| `openvibe-sdk/commerce` | server | `createCommerceClient(config, {caps, fetchImpl?, tokenClients?, timeoutMs?})`: the Billing client Tips and VIP share. `config` = `{billing: {url, audience, timeoutMs = 10000}, network: {internalUrl}, oauth: {clientId, clientSecret}}`; `caps` maps `intent`, `transfer`, `subscription`, `entitlement` (and optional `rates`, default `entitlement`) to capability strings, one lazily created `serviceAuth` token client per distinct string (`scope` = the string) unless `tokenClients` supplies one, keyed by logical key or capability. Methods: `createIntent` (`kind` purchase with `bits`, or subscription with `creator`/`tier`/`autoRenew`), `createTransfer`, `refundTransfer`, `subscribeWithCredit` (`tier`), `cancelSubscription`, `getSubscription`, `listSubscriptions`, `entitlement`, `rates`, `listTiers(creator)` and `setTier({creator, tier, priceCents, status?, key})` (a creator's tier prices, kept by Billing; optional `caps.tier` = `billing.tier.manage`, which `setTier` needs), `baseUrl`. `Idempotency-Key` is sent verbatim; a 401 invalidates the token and retries once with the same key; problem+json becomes `CommerceError` (`status`, `code`, `body`, `retryable`). `intentKey(prefix, parts)` joins the parts that are set; `receipt(kind, fields)` → `{kind, at, ...fields}` (null stripped, never posted). `openvibe-contracts` is required lazily |
| `openvibe-sdk/vibes` | server | `createVibesClient({appToken \| tokenProvider, billingUrl, fetchImpl?, timeoutMs?})`: Vibes Pay for developer apps (D12). `charges.create({amount, description, reference, return_url})` sends a whole number of Vibes with the reference as the `Idempotency-Key`, → `{id, pay_url, status, expires_at}`; `charges.get(id)`, `charges.refund(id, {reason?})`. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook`. A float, zero or negative amount is refused before any request; problem+json becomes `VibesError` (`status`, `code`, `body`, `retryable`), a 401 invalidates a token provider and retries once with the same key |
--| `openvibe-sdk/exchange` | server | `createExchangeClient({developerKey, baseUrl?, networkUrl?, fetchImpl?, timeoutMs?, maxRetries?, owner?, paths?})`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, item 7). Buyer: `chat(body, {signal})` → the OpenAI shape (`stream: true` answers an async iterable of chunks), `models()`, `offers(model)` (the offers that serve one model, with the price, the speed and the uptime), `catalog()`, `presets.list/create/update/remove` (a preset is stored under its name; there is no PATCH), `usage({days})` (the public aggregate; with `from`/`to`/`requester` the platform usage for that requester) and `receipt(id)`. Seller: `seller.offer({model, price, hours, concurrency, endpoint, machineId, owner})` registers the offer in the Network’s user-owned AI offers (the same registry OpenVibe.Node’s "Offer to the network" writes to: `POST /internal/ai/offers/report`, the capability `network.resource.report`), `seller.earnings()`, `seller.verification()` (canaries, spot re-runs, reputation, uptime) and `seller.offers()`. A 429 or a 5xx is retried with a jittered backoff (a write only when it is idempotent); every call takes an AbortSignal. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook` |
++| `openvibe-sdk/exchange` | server | `createExchangeClient({developerKey, baseUrl?, networkUrl?, fetchImpl?, timeoutMs?, maxRetries?, owner?, paths?})`: OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, item 7). Buyer: `chat(body, {signal})` → the OpenAI shape (`stream: true` answers an async iterable of chunks), `models()`, `offers(model)` (the offers that serve one model, with the price, the speed and the uptime), `catalog()`, `presets.list/create/update/remove` (a preset is stored under its name; there is no PATCH), `usage({days})` (the public aggregate; with `from`/`to`/`requester` the platform usage for that requester) and `receipt(id)`. Seller: `seller.offer({model, price, hours, concurrency, endpoint, machineId, owner})` registers the offer in the Network’s user-owned AI offers (the same registry OpenVibe Link’s "Offer to the network" writes to: `POST /internal/ai/offers/report`, the capability `network.resource.report`), `seller.earnings()`, `seller.verification()` (canaries, spot re-runs, reputation, uptime) and `seller.offers()`. A 429 or a 5xx is retried with a jittered backoff (a write only when it is idempotent); every call takes an AbortSignal. `webhooks.verify(rawBody, signatureHeader, secret, {toleranceSec = 300, now?})` checks `X-OpenVibe-Signature-V2` (`t=<unix>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">`, constant time, replay window) and the legacy `sha256=<hex>` body-only form; also exported as `verifyWebhook` |
| `openvibe-sdk/telemetry` | server | `telemetrySample(fields)` / `validateTelemetrySample(record)` for `platform.telemetry-sample@1`; `createTelemetry({service, instance?, sink, intervalMs = 15000, now?, log?, maxBuffered = 1000})` → `{record(name, value, labels), gauge(name, value, labels), count(name, labels), flush(), stop()}`: buffered samples flushed to `await sink(samples)` on an unref'd interval and on `stop()` (a `gracefulStop` stop step); `record` builds `{service, instance, operation, at, latency_ms, ...labels}` (a non-number value goes to `extra[name]`); a rejecting sink is logged once and its samples kept; past `maxBuffered` the oldest sample is dropped, with a warning at most once a minute; the per-service PostgreSQL sink: `telemetrySchema(table = 'telemetry_samples')` (migration DDL), `createPgTelemetrySink({db, table?, log?})` (one multi-row INSERT per flush, malformed samples dropped with a warning), `pruneTelemetry({db, table?, olderThanDays = 30})`, `startTelemetryRetention({db, table?, olderThanDays = 30, everyMs = 24 h, delayMs = 60 s, log?})` → `{run(), stop()}` (prunes soon after start and then daily on unref'd timers, so a service redeployed more than daily still prunes) |
| `openvibe-sdk/cost` | server | Build and validate `platform.rate-card@1` and `platform.cost-snapshot@1` records with `rateCard()`, `costSnapshot()`, `validateRateCard()`, and `validateCostSnapshot()`; `estimate()` calculates a charge from a rate card; the AI Exchange money math in integer nano-USD with `nanoUsd()`, `chargeTokens()`, `expectedCostNano()`, `costPerSuccessNano()`, `formatNanoUsd()` |
| `openvibe-sdk/runtime` | server | Build and validate `platform.runtime-offer@1` records with `runtimeOffer()` and `validateRuntimeOffer()`; `runtimeClass()`, `RUNTIME_CLASSES`, and `validateRuntimeClass()` handle known runtime classes |
ddiff --git a/STATUS.json b/STATUS.json
iindex 74e2ee5..9396313 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -14,11 +14,11 @@
"streams (openvibe-sdk/streams, S7): schedule(), listSchedules(), cancelSchedule() and remindSchedule() for scheduled public agent streams; postStep() and steps.post() from any harness; digest.read() and poll.open()/poll.result() for a Co-pilot service token; onControl() for pause, curtain, mute and end",
"reports (openvibe-sdk/reports, S7): write() a session report with the person's own token, private by default and idempotent per source and task ref, and read() one the caller may see; examples/cli-agent-stream.js streams any CLI command's run with steps",
"chat-apps (openvibe-sdk/chat-apps): the three chat apps (Telegram, Discord, Slack) in one place — the delivery checks (Telegram's secret token, Slack's v0 signing, Discord's Ed25519), the button callback (signed once, used once, expiring), each app's payload as the same shape and splitText() for a long answer",
-- "exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Node machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
++ "exchange (openvibe-sdk/exchange): the buyer side (chat, models, offers, presets, usage, receipts) and the seller side (offer, endpointOffers, removeEndpointOffer, earnings, verification) of OpenVibe.Exchange, with the platform webhook signature check; an offer is a Link machine's Network report or a standalone OpenAI-compatible endpoint registered on the public seller API",
"locker (openvibe-sdk/locker, openvibe-sdk/locker/server): the Locker envelope (locker.envelope@1) for end-to-end encryption, WebCrypto only, in Node and the browser: seal/open for text and bytes, sealStream/openStream, the three locks (people, password, link), person key pairs (ECDH P-256, ECDSA P-256), the eight-word fingerprint, wrapFor and rewrap, recoveryWords for the phrase to write down, opening a people lock by key, inspect for what an envelope says about itself, and the header checks a service runs without a key",
"the 1.0 core surface is frozen now, before 1.0 (core, auth, events, db, media): docs/one-point-oh.json gives every export its tier, test/surface.test.js pins the exact names, and removing or renaming one needs a major release; createCodeVerifier and pkceChallenge left in 0.86.0",
"share (openvibe-sdk/share): transactional local sharing, audience and membership checks, exact-name resolution, recipient-bound ownership transfers, audit, durable events/notifications, uniform API and agent client; locked items (Locker): the lock kind, per-member key slots checked against the key directory, a re-seal on removal through onReseal, and the transfer offer slot",
-- "redact (openvibe-sdk/redact): the shared secret scrubber for memory, the same shapes and corpus as OpenVibe.Node's internal/memory/redact.go (redact, onlySecret)",
++ "redact (openvibe-sdk/redact): the shared secret scrubber for memory, the same shapes and corpus as OpenVibe Link's internal/memory/redact.go (redact, onlySecret)",
"core client: retries, timeouts, problem+json errors, trace propagation",
"auth: service and app tokens, PKCE, offline verification, the Network signing keys (createNetworkKeys), revocation cutoffs",
"registry discovery and contracts version checks",
ddiff --git a/browser/openvibe-sdk.mjs b/browser/openvibe-sdk.mjs
iindex bcb94f0..c44a99d 100644
---- a/browser/openvibe-sdk.mjs
++++ b/browser/openvibe-sdk.mjs
@@@ -2663,7 +2663,7 @@ function subscribe(topics, onEvent, opts = {}) {
* for await (const { event, data, id } of parseSSE(res.body)) { … }
*
* `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks
-- * (a Node stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
++ * (a Link stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,
* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.
*/
@@@ -3300,13 +3300,13 @@ function (module, exports) {
* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).
* Every other POST carries an Idempotency-Key (the client generates one when retries are on).
*
-- * Machines (the OpenVibe Node's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
++ * Machines (the OpenVibe Link's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a
* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
*
-- * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):
++ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe Link v0.27.0's ai plugin):
* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
@@@ -3401,7 +3401,7 @@ function createBotClient(client, defaults = {}) {
},
};
-- // ── Machines (the OpenVibe Node's computer plugin) and desks of your own on them ──
++ // ── Machines (the OpenVibe Link's computer plugin) and desks of your own on them ──
const machines = {
/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */
list: (o = {}) => call({ path: '/api/v1/machines' }, o),
@@@ -4097,13 +4097,13 @@ module.exports = { createThemesClient, followTheme, readTheme, applyTheme, seaso
function (module, exports) {
'use strict';
/**
-- * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe.Node's
-- * internal/memory/redact.go (Node #87), so the Node and OpenVibe.AI run one list of shapes against one corpus
-- * (test/redact-corpus.json). The Node scrubs a memory file before it uploads it; AI scrubs every item again before it
++ * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe Link's
++ * internal/memory/redact.go (Node #87), so Link and OpenVibe.AI run one list of shapes against one corpus
++ * (test/redact-corpus.json). Link scrubs a memory file before it uploads it; AI scrubs every item again before it
* seals it, and refuses an item that was nothing but a secret.
*
* - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were
-- * removed. The whole-text pass uses an internal sentinel first, the way the Node scrubs a file before it splits it
++ * removed. The whole-text pass uses an internal sentinel first, the way Link scrubs a file before it splits it
* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted
* is left alone.
* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").
@@@ -4315,7 +4315,7 @@ function redactWith(text, marker) {
/**
* The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a
-- * fence, an .env block) is only seen whole, which is why the Node scrubs a file before it splits it. The sentinel is
++ * fence, an .env block) is only seen whole, which is why Link scrubs a file before it splits it. The sentinel is
* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.
*/
function redactDocument(text) {
ddiff --git a/docs/exchange.md b/docs/exchange.md
iindex aa2fdb8..53b7b66 100644
---- a/docs/exchange.md
++++ b/docs/exchange.md
@@@ -2,7 +2,7 @@
OpenVibe.Exchange, the buyer and the seller side of the model exchange (the plan of 2026-11-06, "For builders",
item 7). The buyer side speaks the Exchange's v1 API with a Network developer key; the seller side registers an
--offer in the Network's user-owned AI offers, the same registry OpenVibe.Node's "Offer to the network" uses.
++offer in the Network's user-owned AI offers, the same registry OpenVibe Link's "Offer to the network" uses.
```js
const { createExchangeClient } = require('openvibe-sdk/exchange');
@@@ -59,8 +59,8 @@ adult gate. A request names one as `model: '@preset/<name>'`.
## The seller side
--An offer has two forms. A **Node machine** reports its offer to the Network's user-owned AI offers — the same registry
--OpenVibe.Node's "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability
++An offer has two forms. A **Link machine** reports its offer to the Network's user-owned AI offers — the same registry
++OpenVibe Link's "Offer to the network" writes to (`POST /internal/ai/offers/report`, the capability
`network.resource.report`). A **standalone server** registers itself on the Exchange's public seller API with the
developer key, so it needs no Node at all. The SDK validates both before anything is sent.
@@@ -88,7 +88,7 @@ the OpenAI API is caught before the offer is registered (`probe: false` turns th
### A standalone server: the endpoint form
--A server that speaks the OpenAI Chat Completions API can sell without an OpenVibe Node. Give `endpoint` and no
++A server that speaks the OpenAI Chat Completions API can sell without an OpenVibe Link. Give `endpoint` and no
`machineId`; the offer is registered on the Exchange's public seller API
(`POST /api/v1/exchange/seller/offers`) with the same developer key as the buyer side.
@@@ -113,7 +113,7 @@ The owner is the person the developer key acts for (a developer app's project ow
the address only through its egress guard: https, a public DNS answer checked inside the connection (never an IP
literal), redirects judged again, a timeout, a response byte cap, and no buyer identity in any header. No bearer token
is sent, so the endpoint must accept the POST as it is (`endpointKey` only authenticates the SDK's own probe). The
--endpoint is verified exactly as a Node machine (the canaries, the model fingerprint, about 1% spot re-runs), serves the
++endpoint is verified exactly as a machine running Link (the canaries, the model fingerprint, about 1% spot re-runs), serves the
free tier only until verified, and earns 0 on the owner's own requests. An answer from it is labelled a community
endpoint on the receipt (`provider: community-endpoint:<id>`, `provider_access: COMMUNITY_ENDPOINT`). It sees the
prompt: only requests that opt in to community capacity reach it.
ddiff --git a/docs/placement.md b/docs/placement.md
iindex 1a8d5ec..2bd40b9 100644
---- a/docs/placement.md
++++ b/docs/placement.md
@@@ -77,7 +77,7 @@ five-class preference order.
**A streaming machine.** When an offer says its machine is streaming, a step that asks for a GPU or 2 or more cores
is excluded with `it is streaming`. The contract does not carry that field yet; `place()` reads
--`offer.health.streaming === true` (and tolerates a top-level `offer.streaming`), so OpenVibe.Node can start sending
++`offer.health.streaming === true` (and tolerates a top-level `offer.streaming`), so OpenVibe Link can start sending
it without another SDK change. Node's status already reports `screen.state`, but Bot's `platform.resource-offer@1`
does not project it today.
ddiff --git a/docs/redact.md b/docs/redact.md
iindex 807fa33..d148eff 100644
---- a/docs/redact.md
++++ b/docs/redact.md
@@@ -1,7 +1,7 @@
# openvibe-sdk/redact
--The shared secret scrubber for memory (Memory M2). It is the JavaScript port of OpenVibe.Node's
--`internal/memory/redact.go` (Node #87), so the Node and OpenVibe.AI run one shape list against one corpus.
++The shared secret scrubber for memory (Memory M2). It is the JavaScript port of OpenVibe Link's
++`internal/memory/redact.go` (Node #87), so Link and OpenVibe.AI run one shape list against one corpus.
```js
const { redact, onlySecret } = require('openvibe-sdk/redact');
@@@ -32,13 +32,13 @@ A text that was already redacted is left alone. The count is the number of secre
## Why the whole-text pass exists
--The Node scrubs a whole file with a sentinel before it splits the file into items, then scrubs each item. A key block
++Link scrubs a whole file with a sentinel before it splits the file into items, then scrubs each item. A key block
or an `.env` block that spans two items is only seen whole. `redact()` runs the same two passes on one string: the
sentinel keeps the place of each secret until the text is resolved to `[redacted]`. This is also why AI runs the
--scrubber again before sealing an item: the Node is not the only writer.
++scrubber again before sealing an item: Link is not the only writer.
## Corpus
`test/redact-corpus.json` is the shared list: every shape, the split and unterminated key blocks, the `.env` block and
--the harmless texts that must not change. `test/redact.test.js` runs it here; OpenVibe.Node copies the file into
++the harmless texts that must not change. `test/redact.test.js` runs it here; OpenVibe Link copies the file into
`internal/memory/testdata/` and runs the same cases against its Go `Redact`.
ddiff --git a/docs/streams.md b/docs/streams.md
iindex 4792f65..de0a5be 100644
---- a/docs/streams.md
++++ b/docs/streams.md
@@@ -21,7 +21,7 @@ const { stream, publish } = await streams.startAgentStream({
## The desk's loop
--The desk (OpenVibe.Node) polls Live every ten seconds and keeps publishing while Live wants video:
++The desk (OpenVibe Link) polls Live every ten seconds and keeps publishing while Live wants video:
```js
const s = await streams.state(stream.id, { ticket: publish.ticket });
@@@ -65,7 +65,7 @@ await streams.end(stream.id); // the card stays, with t
## Who may call
--The person's own session token, or a first-party service token acting for that person: a Node principal acting for its
++The person's own session token, or a first-party service token acting for that person: a Link principal acting for its
owner (Network issues the node token with `live.agent_stream.publish` for the audience `openvibe.live` and names its
owner in `on_behalf_of`), Codes or Actor. Never a developer-app token, never another person's stream.
ddiff --git a/examples/cli-agent-stream.js b/examples/cli-agent-stream.js
iindex d7cd055..1061476 100644
---- a/examples/cli-agent-stream.js
++++ b/examples/cli-agent-stream.js
@@@ -5,7 +5,7 @@
*
* OPENVIBE_TOKEN=… node examples/cli-agent-stream.js --title "Fix the login page" -- npm test
*
-- * The same path a Node desk uses: startAgentStream gives the stream and its publish ticket, every line the command
++ * The same path a Link desk uses: startAgentStream gives the stream and its publish ticket, every line the command
* writes is a step, and the stream ends with the command's result. Private by default; pass --public to list it.
*/
const { spawn } = require('node:child_process');
ddiff --git a/src/bot.js b/src/bot.js
iindex 092866a..a6001b4 100644
---- a/src/bot.js
++++ b/src/bot.js
@@@ -15,13 +15,13 @@
* and the call is never retried (a code is one-time, so a lost answer cannot be safely repeated).
* Every other POST carries an Idempotency-Key (the client generates one when retries are on).
*
-- * Machines (the OpenVibe Node's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
++ * Machines (the OpenVibe Link's computer plugin): `machines.list`, `machines.computer`, `machines.command` (bot.command@1's
* computer kinds) and `machines.deskTurn`; `machines.desk(id, { name?, kind?, profile? })` is a desk of your own on a
* machine (the Desks plan: its own screen or browser, files and sign-ins), with start/stop/status and browser, screen,
* input and exec on it. A desk command that the machine does not do throws an OpenVibeError with code `bot.desk_nack`
* (the machine's own fault code is `fault`), so a desk reads as plain awaits.
*
-- * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe.Node v0.27.0's ai plugin):
++ * A machine's own models (AI on your own machines, contracts 0.219.0, OpenVibe Link v0.27.0's ai plugin):
* `machines.chat(id, { model, messages, max_tokens?, temperature? })` asks one of them for one reply (kind ai) and answers
* `{ model, message: { role, content }, usage?, ms? }`; a machine that does not answer throws `bot.ai_nack`.
* `machines.aiModels(id, on)` is the owner's "Let my AI use this computer's models".
@@@ -116,7 +116,7 @@ function createBotClient(client, defaults = {}) {
},
};
-- // ── Machines (the OpenVibe Node's computer plugin) and desks of your own on them ──
++ // ── Machines (the OpenVibe Link's computer plugin) and desks of your own on them ──
const machines = {
/** { machines } — the person's machines with their computer capabilities (a service sees the ones opened to it). */
list: (o = {}) => call({ path: '/api/v1/machines' }, o),
ddiff --git a/src/exchange.js b/src/exchange.js
iindex cda71ef..dd5abd9 100644
---- a/src/exchange.js
++++ b/src/exchange.js
@@@ -12,12 +12,12 @@
* // { id: 'chatcmpl-...', object: 'chat.completion', choices, usage, x_openvibe: { run_id, cost_usd, ... } }
* for await (const chunk of exchange.chat({ model: 'openvibe', messages, stream: true })) { ... }
*
-- * The seller side registers an offer in one of two forms. With `machineId`, it reports a Node machine's offer to the
++ * The seller side registers an offer in one of two forms. With `machineId`, it reports a Link machine's offer to the
* Network's user-owned AI offers (POST /internal/ai/offers/report, the capability network.resource.report). With
* `endpoint` and no machine, it registers a standalone OpenAI-compatible server on the Exchange's public seller API
* (POST /api/v1/exchange/seller/offers) with the same developer key the buyer side uses; AI reaches that address only
* through its egress guard (https, a public DNS answer, never an IP literal, a timeout, a response cap, no buyer
-- * identity in a header) and verifies it exactly as a Node machine (canaries, fingerprint, spot re-runs, free tier only
++ * identity in a header) and verifies it exactly as a machine running Link (canaries, fingerprint, spot re-runs, free tier only
* until verified, self-dealing earns 0).
*
* const offer = await exchange.seller.offer({
@@@ -378,7 +378,7 @@ function createExchangeClient({
/**
* Register a community offer: what is served, at what price, when, and how many answers at once.
*
-- * With `machineId`, the offer is reported to the Network's user-owned AI offers - the registry OpenVibe.Node's
++ * With `machineId`, the offer is reported to the Network's user-owned AI offers - the registry OpenVibe Link's
* "Offer to the network" uses - with the document that registry validates.
* offer({ model, price, hours, concurrency, endpoint, machineId, owner, audience, group, ready, health })
*
ddiff --git a/src/node.js b/src/node.js
iindex 3de3944..30793ef 100644
---- a/src/node.js
++++ b/src/node.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a Node descriptor to a
++ * openvibe-sdk/node — platform.node-capabilities@1, plus nodeOffers() that maps a node descriptor to a
* platform.resource-offer@1 (kind "node") and a platform.runtime-offer@1.
* Pure construction and validation, no I/O and no dependency at require time.
*
@@@ -32,7 +32,7 @@ function nodeCapabilities(fields) {
}
/**
-- * Map a Node descriptor (platform.node-capabilities@1) to two offers:
++ * Map a node descriptor (platform.node-capabilities@1) to two offers:
* - resource: a platform.resource-offer@1 with kind "node", capabilities including every advertised
* worker capability prefixed "worker:<name>", trust from opts.trust or "first-party";
* - runtime: a platform.runtime-offer@1 with kind "container" (the default runtime a node offers),
ddiff --git a/src/realtime.js b/src/realtime.js
iindex 259450f..984c8ae 100644
---- a/src/realtime.js
++++ b/src/realtime.js
@@@ -183,7 +183,7 @@ function subscribe(topics, onEvent, opts = {}) {
* for await (const { event, data, id } of parseSSE(res.body)) { … }
*
* `body` is a ReadableStream<Uint8Array> (fetch), or any async iterable of Uint8Array/string chunks
-- * (a Node stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
++ * (a Link stream). `event` defaults to 'message'; `id` is the id field of that event (undefined when
* it had none); comments are skipped; `retry: <ms>` is passed to onRetry. A line split across chunks,
* including a \r\n pair, is reassembled. Breaking out of the loop releases the stream.
*/
ddiff --git a/src/redact.js b/src/redact.js
iindex 6fa4440..7139f41 100644
---- a/src/redact.js
++++ b/src/redact.js
@@@ -1,12 +1,12 @@
'use strict';
/**
-- * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe.Node's
-- * internal/memory/redact.go (Node #87), so the Node and OpenVibe.AI run one list of shapes against one corpus
-- * (test/redact-corpus.json). The Node scrubs a memory file before it uploads it; AI scrubs every item again before it
++ * openvibe-sdk/redact: the shared secret scrubber (Memory M2). It is the JavaScript port of OpenVibe Link's
++ * internal/memory/redact.go (Node #87), so Link and OpenVibe.AI run one list of shapes against one corpus
++ * (test/redact-corpus.json). Link scrubs a memory file before it uploads it; AI scrubs every item again before it
* seals it, and refuses an item that was nothing but a secret.
*
* - redact(text) -> { text, count }: every secret shape is replaced with [redacted], and the count says how many were
-- * removed. The whole-text pass uses an internal sentinel first, the way the Node scrubs a file before it splits it
++ * removed. The whole-text pass uses an internal sentinel first, the way Link scrubs a file before it splits it
* into items; the sentinel is resolved to [redacted] before the result is returned. A text that is already redacted
* is left alone.
* - onlySecret(text): true when nothing but redacted secrets is left ("the item was only a secret").
@@@ -218,7 +218,7 @@ function redactWith(text, marker) {
/**
* The whole-text pass with the sentinel, then the per-item pass. A secret that spans two items (a key block in a
-- * fence, an .env block) is only seen whole, which is why the Node scrubs a file before it splits it. The sentinel is
++ * fence, an .env block) is only seen whole, which is why Link scrubs a file before it splits it. The sentinel is
* resolved to [redacted] before the per-item pass, which catches anything only visible once the text is on its own.
*/
function redactDocument(text) {
ddiff --git a/src/streams.js b/src/streams.js
iindex 98472da..de6d75a 100644
---- a/src/streams.js
++++ b/src/streams.js
@@@ -3,7 +3,7 @@
* openvibe-sdk/streams — the Agent Streams plan (S1): an agent's own stream on OpenVibe.Live.
*
* An agent stream is a Live stream of kind 'agent' (id ags_…): it has no stream key and takes no slot, and it is
-- * published with a ticket (wpt_…) instead of one. The desk (OpenVibe.Node) or a person's browser (a Codes task page)
++ * published with a ticket (wpt_…) instead of one. The desk (OpenVibe Link) or a person's browser (a Codes task page)
* creates it, keeps the ticket and publishes through it; the owner watches the stream on the page Live answers with.
*
* const streams = createStreamsClient(createClient({ tokenProvider }));
ddiff --git a/test/redact.test.js b/test/redact.test.js
iindex 0d90938..7a696aa 100644
---- a/test/redact.test.js
++++ b/test/redact.test.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * openvibe-sdk/redact: the shared corpus of secret shapes (Memory M2). It is the same file OpenVibe.Node copies into
-- * its internal/memory/testdata, so the Node's Go scrubber and this port cannot drift: every case says what must be
++ * openvibe-sdk/redact: the shared corpus of secret shapes (Memory M2). It is the same file OpenVibe Link copies into
++ * its internal/memory/testdata, so Link's Go scrubber and this port cannot drift: every case says what must be
* gone and what must stay.
*/
const assert = require('node:assert/strict');
ddiff --git a/types/bot.d.ts b/types/bot.d.ts
iindex c0e47f8..e78018e 100644
---- a/types/bot.d.ts
++++ b/types/bot.d.ts
@@@ -82,7 +82,7 @@ export interface PairInput {
name?: string;
}
export interface MachineCommandInput { id?: string; kind: 'exec' | 'fs' | 'screen' | 'input' | 'browser' | 'probe' | 'desk' | 'ai'; value: Record<string, unknown>; ms?: number; }
--/** A machine's own models (OpenVibe.Node's ai plugin): what it offers and how fast its last model answered there. */
++/** A machine's own models (OpenVibe Link's ai plugin): what it offers and how fast its last model answered there. */
export interface MachineAi { models: string[]; ready: boolean; max_tokens?: number; }
export interface MachineAiSpeed { model: string; tokens_per_s: number; }
export interface Machine {
ddiff --git a/types/exchange.d.ts b/types/exchange.d.ts
iindex 0d500d8..d1fd34f 100644
---- a/types/exchange.d.ts
++++ b/types/exchange.d.ts
@@@ -353,7 +353,7 @@ export type ExchangeClient = {
/** The receipt of one answered request. */
receipt(id: string, opts?: { signal?: AbortSignal }): Promise<ExchangeReceipt>;
seller: {
-- /** Register a Node machine's offer in the Network's user-owned AI offers, or an endpoint with the Exchange. */
++ /** Register a Link machine's offer in the Network's user-owned AI offers, or an endpoint with the Exchange. */
offer(input: ExchangeOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeOfferResult>;
offer(input: ExchangeEndpointOfferInput, opts?: { signal?: AbortSignal }): Promise<ExchangeEndpointOfferResult>;
/** The caller's own endpoint offers, with their verification. */
ddiff --git a/types/machines.d.ts b/types/machines.d.ts
iindex 22710f6..fd1d41f 100644
---- a/types/machines.d.ts
++++ b/types/machines.d.ts
@@@ -20,7 +20,7 @@ export interface Machine {
os?: string;
/** When the machine was last seen. */
last_seen_at?: string;
-- /** The OpenVibe Node version the machine reports. */
++ /** The OpenVibe Link version the machine reports. */
node_version?: string;
}