Reference

Changelog — @bubblio/door

1.0.1 — 2026-08-22

README-only patch — no code changes. The Guides links pointed at two docs routes that no longer exist (/docs/agent-door, /docs/nextjs); they now point at the real pages (/docs, /docs/quickstart). npm freezes a version's README at publish, which is why the fix needs a release.

1.0.0 — 2026-08-21

Renamed from @bubblio/server. Bubblio dropped its customer-service product — the real-time AI-avatar widget and the session SDK that bootstrapped it — entirely, so this package no longer carries it. Breaking:

  • Removed: createBubblioSession, BubblioSessionConfig, PlatformConfig, SelfHostedConfig, BubblioTool, BubblioToolConfig, BubblioSession, BubblioTransport, BubblioClientEvent, withUserContext, BubblioToolInput — the whole session-bootstrap surface (platform mode and self-hosted mode alike) and its config/return types. If you used any of these to run a Bubblio character widget, that product is gone; there is no successor in this package. The @runwayml/sdk dependency and the @runwayml/avatars-node-rpc optional peer leave with it — this package no longer touches Runway, or any avatar provider, at all.
  • @bubblio/door is agent-door-only from here on. Everything that made the door work is unchanged: defineDoorActions, createAgentKeyRoute, mintAgentKey / listAgentKeys / revokeAgentKey, createBubblioToolRoute, the payments lane (BubblioPaymentsClient / BubblioPaymentsConfig, completeBubblioCheckout, resumePaidQuote, pushDoorResult), and the shared verification/context primitives (verifyWebhookSignature, verifyBubblioWebhook, signUserContext / verifyUserContext / userContextFromRequest, deriveUserContextSecret, publicOrigin, BubblioApiError) all kept their exact contracts — a door integration needs only its import name updated, nothing else.
  • Migration: npm uninstall @bubblio/server && npm install @bubblio/door, then change from '@bubblio/server' to from '@bubblio/door' everywhere. If your callback route also imported one of the removed session symbols — some integrations shared one route between the widget and the door — that code has no replacement here; the customer-service product it served no longer exists.

Formerly published as @bubblio/server; its full release history (0.1 through 0.14) is preserved below, unedited — reconstructed from the release history (#52) and 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.14.0 — 2026-08-16

Member parity (#56): quote-time price resolution. Additive — no breaking changes.

  • priceFor on declared actions — an optional resolver (args, user, meta) => { covered: true, reason? } | { price: '$0.25' } | null answering Bubblio's quote-time question on keyed quotes of priced actions: what does THIS user pay? user is the agent key's minted identity, resolved exactly as handler calls resolve it; meta carries the pinned quote facts (tool, quoteId, listedPriceCents, currency, argsHash). covered: true pins $0 with price_source 'platform_covered' — the only path to a $0 quote, rendered honestly on the approval screen ("covered by your <label> account"). A member price parses through the same parser as the declared price and can only discount: Bubblio clamps anything above listed back to listed.
  • Fail-to-listed, never to free: no resolver on the action, a resolver throw, an unparseable price ('$0.00' included — free is only ever said as covered: true), a malformed request, an unknown tool — the route answers {} and the listed price stands. Bubblio's side of the same doctrine covers non-200s, the 4-second single-attempt timeout, and SDKs older than this release (they answer the wire as an unknown tool → listed).
  • The resolver travels on the handlers record (tools: { ...door.handlers } is unchanged), so member pricing needs zero new route config. priceFor without price is refused eagerly at defineDoorActions — price_for only fires for priced actions, so a resolver on an unpriced action is dead code, not a degraded mode.

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".