Reference

Changelog — @bubblio/server

Reconstructed from the release history (#52); kept honest and current from 0.12.0 on. Versions below 0.9.0 predate this file — their story lives in the README's feature sections and the repository history.

0.13.0 — 2026-08-16

SDK papercuts from the stylica integration audit (#49). Additive — no breaking changes.

  • First-request door sync as a config line: sync: { door, callbackUrl, connectPath?, payments? } on createBubblioToolRoute wires the pattern every platform hand-rolled — build-safe (never at module scope; next build evaluates route modules), once per process, in-flight-deduped, retry-on-failure throttled to retrySeconds (default 60), never blocking a callback. door.sync() itself is unchanged for explicit control.
  • `maxActiveKeysPerUser` on createAgentKeyRoute (and mintAgentKey) — a per-user active-key quota enforced by Bubblio inside the mint path (it owns bl_agent_keys; a platform-side check-then-act can always lose the race, and stylica's did). Racing mints fail closed — a spurious at-cap 409 a retry resolves, never an overshoot — provided every mint for that user carries the cap: createAgentKeyRoute stamps it on every mint (grant path included), but an uncapped mint from another code path commits outside the enforcement and is never rolled back. At cap: calm 409 with code: 'key_cap_reached'; the phone-connect grant path mints through the same wall, so it is capped identically. BubblioApiError now carries the API's machine-readable code, and createAgentKeyRoute relays it to your UI.
  • API-key rotation no longer silently breaks managed-key identity: previousBubblioApiKeys on createBubblioToolRoute is the N/N-1 verify window — ctx tokens signed under the old key keep resolving their user while pre-rotation keys are live. Local and verification-only (the old key is never sent anywhere; it may already be revoked). verifyUserContext / userContextFromRequest now accept string | string[]. Full runbook in the README ("Rotating your Bubblio API key"). Deferred, by name: a server-assisted re-mint of stored ctx (PATCH /v1/agent-keys/:id { ctx }) — until it exists, a window shorter than the ctx lifetime means pre-rotation users re-mint through your route once the window closes.
  • Docs, corrected per the audit: the meta.idempotencyKey docs now say plainly that the hosted callback fires at most once per quote (no retry loop; duplicate confirms replay the receipt without reaching your endpoint) — handler-side replay caches are defense-in-depth for your own step-2 retries, not the primary double-execution defense; the primary double-spend vector is re-quoting, guarded platform-side by a per-identity unique claim. Teaching surfaces (README, docs site, examples/nextjs-door) now use the sync option instead of the hand-rolled syncOnce(), and door-only install docs no longer imply @bubblio/widget (the door never needs it).
  • OpenAPI spec for the door HTTP surfacedocs/agent-door.openapi.yaml in the repo: discovery, per-door + hub MCP endpoints, the approvals API, agent keys + connect grant, the signed push endpoints, the actions sync, and the outbound callback contract as an OpenAPI webhook. Derived from the routes; when code and spec disagree, code wins.

Release order for this wave (load-bearing, not a nicety): publish @bubblio/server 0.13.0 and deploy the API before (or with) the dashboard deploy. The dashboard's docs teach sync: (silently ignored by an installed 0.12.0 in plain JS — no error anywhere), and /agents + llms.txt advertise GET /agent/feed.json, which 404s until the API is live. Publishing/deploying in that order closes the window where public teaching surfaces describe features nothing serves.

0.12.0 — 2026-08-15

The money lane: paid actions on your own Stripe. Additive — no breaking changes.

  • `price` on declared actions (price: '$0.40' or { amount_cents, currency }, usd-only for now). Synced as shape; every discovery surface (manifest, llms.txt, list_actions, about) shows it, and each quote pins it — the charged amount always derives from the pin, never from anything a caller sent.
  • Charge-wrap: payments: { stripeSecretKey } on createBubblioToolRoute charges on your own Stripe account, then runs your handler, and auto-refunds on throw. The quote id is the Stripe idempotency key (redelivered callbacks replay the PaymentIntent instead of charging twice); refunds run under quoteId + ':refund'. The payment token is transit-only — never stored, logged, echoed, or handed to your handler (meta.payment is the settled summary).
  • Payment rails profile: payments: { rails, stripePublishableKey } on door.sync declares payment_link and/or stripe_spt. Publishable key (pk_…) only — sk_/rk_ values are refused by name, before anything is stored.
  • `completeBubblioCheckout(session, opts)` — call from your Stripe checkout.session.completed webhook to settle a payment-link quote; non-Bubblio sessions are ignored (safe on a shared webhook), and a payment Bubblio refuses to settle is auto-refunded (quoteId + ':refund:' + sessionId). `resumePaidQuote` is the primitive underneath.
  • `pushDoorResult` now carries the payment summary with a late result, so an upgraded receipt still shows the charge. A connection that dies mid-charge retries once under the same idempotency key, then reports "outcome unknown" — never a false "nothing was charged".
  • Declaring a price against an API without migration 0036 refuses the whole sync (fail-closed: serving a paid action unpriced is a money bug, not a degraded mode) — the SDK throws BubblioApiError; the previously-synced registry keeps serving.

0.11.0 — 2026-08-15

Phone connect and approval hints. Additive — no breaking changes.

  • Phone connect: connectPath on door.sync declares your login page (path-only — it resolves against callbackUrl's origin and may never point off your site). createAgentKeyRoute becomes grant-aware: POST { grant: 'g_…' } mints the bak_ key and forwards it into Bubblio's connect grant, answering { ok, connected, id } — never the raw key. On a forward failure the just-minted key is revoked (best-effort) and the error is reported; no credential ends up owned by nobody. Requires API migrations 0033–0035 — against an older API, sync warns and the forward answers 404/409.
  • `approvalSuggestion` ('auto' | 'mandate' | 'always') on declared actions — a hint for the owner's per-action approval control, exactly like policySuggestion: stored on first sync, shown in the dashboard, never applied by a deploy.
  • Docs: the 'mandate' control's live evaluation (standing mandates under per-action caps and a daily budget) documented as shipped.

0.10.0 — 2026-08-15

Identity moves out of the URL; late results get a push channel.

  • `body.ctx`: on Agent Door managed-key confirms the user-context JWT now arrives inside the HMAC-signed callback body instead of only as ?ctx= on the URL (where access logs, proxies, error trackers, and Referer headers see a 90-day act-as-user credential). createBubblioToolRoute and userContextFromRequest (new third argument: the verified body) read the body first and fall back to the query param — both directions of version skew keep resolving user. A platform-side deprecation window keeps echoing ?ctx= for SDKs ≤0.9.1.
  • `pushDoorResult`: actions that outlive the ≤20s confirm window can deliver their late result to the quote (V2-signed, direction reversed), upgrading an unreachable receipt to ok instead of inviting a re-quote. createBubblioToolRoute pushes automatically when a handler finishes after the window.
  • Doctrine, written down: spend mandates never ride in ctx — the ctx token carries identity only. If a key path becomes spend-linked, mint its ctx with a short expiresInSeconds.
  • Callback bodies are extensible: fields arrive additively (agent in 0.8, ctx and callbackTimeoutSeconds here) — never validate them with a closed schema.

0.9.1 — 2026-08-13

Docs and package metadata only — no code changes.

  • README reframed for npm (chat-first, hosted API URLs, support via email).
  • repository field dropped (private repo), bugs → email.

0.9.0 — 2026-08-09

Security release: the legacy signature is refused by default.

  • `verifyBubblioWebhook` and `createBubblioToolRoute` require the replay-protected pair X-Bubblio-Signature-V2 + X-Bubblio-Timestamp; a request carrying only the body-only X-Bubblio-Signature is answered 401 (that signature never expires, so a captured callback could replay forever). Escape hatch while you fix a stale platform or a header-stripping proxy: allowLegacySignature: true — temporary by design.
  • `onFailure` callback on verification: missing_signature, legacy_signature_rejected, stale_timestamp, bad_signature, invalid_json — the 401 body names the reason, but the platform's log doesn't, so put it in your own.
  • `meta.idempotencyKey` on Agent Door confirms (the quote id) when the platform sends it — treat undefined as "no dedupe anchor available", not "first delivery".