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/sdkdependency and the@runwayml/avatars-node-rpcoptional peer leave with it — this package no longer touches Runway, or any avatar provider, at all. @bubblio/dooris 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 changefrom '@bubblio/server'tofrom '@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.
priceForon declared actions — an optional resolver(args, user, meta) => { covered: true, reason? } | { price: '$0.25' } | nullanswering Bubblio's quote-time question on keyed quotes of priced actions: what does THIS user pay?useris the agent key's minted identity, resolved exactly as handler calls resolve it;metacarries the pinned quote facts (tool,quoteId,listedPriceCents,currency,argsHash).covered: truepins $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 declaredpriceand 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 ascovered: 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.priceForwithoutpriceis refused eagerly atdefineDoorActions— 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? }oncreateBubblioToolRoutewires the pattern every platform hand-rolled — build-safe (never at module scope;next buildevaluates route modules), once per process, in-flight-deduped, retry-on-failure throttled toretrySeconds(default 60), never blocking a callback.door.sync()itself is unchanged for explicit control. maxActiveKeysPerUseroncreateAgentKeyRoute(andmintAgentKey) — a per-user active-key quota enforced by Bubblio inside the mint path (it ownsbl_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:createAgentKeyRoutestamps 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 withcode: 'key_cap_reached'; the phone-connect grant path mints through the same wall, so it is capped identically.BubblioApiErrornow carries the API's machine-readablecode, andcreateAgentKeyRouterelays it to your UI.- API-key rotation no longer silently breaks managed-key identity:
previousBubblioApiKeysoncreateBubblioToolRouteis 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/userContextFromRequestnow acceptstring | 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.idempotencyKeydocs 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 thesyncoption instead of the hand-rolledsyncOnce(), and door-only install docs no longer imply@bubblio/widget(the door never needs it). - OpenAPI spec for the door HTTP surface —
docs/agent-door.openapi.yamlin 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.
priceon 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 }oncreateBubblioToolRoutecharges 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 underquoteId + ':refund'. The payment token is transit-only — never stored, logged, echoed, or handed to your handler (meta.paymentis the settled summary). - Payment rails profile:
payments: { rails, stripePublishableKey }ondoor.syncdeclarespayment_linkand/orstripe_spt. Publishable key (pk_…) only —sk_/rk_values are refused by name, before anything is stored. completeBubblioCheckout(session, opts)— call from your Stripecheckout.session.completedwebhook 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).resumePaidQuoteis the primitive underneath.pushDoorResultnow 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
priceagainst 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 throwsBubblioApiError; the previously-synced registry keeps serving.
0.11.0 — 2026-08-15
Phone connect and approval hints. Additive — no breaking changes.
- Phone connect:
connectPathondoor.syncdeclares your login page (path-only — it resolves againstcallbackUrl's origin and may never point off your site).createAgentKeyRoutebecomes grant-aware:POST { grant: 'g_…' }mints thebak_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 likepolicySuggestion: 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, andRefererheaders see a 90-day act-as-user credential).createBubblioToolRouteanduserContextFromRequest(new third argument: the verified body) read the body first and fall back to the query param — both directions of version skew keep resolvinguser. 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 anunreachablereceipt tookinstead of inviting a re-quote.createBubblioToolRoutepushes 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 (
agentin 0.8,ctxandcallbackTimeoutSecondshere) — 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).
repositoryfield dropped (private repo),bugs→ email.
0.9.0 — 2026-08-09
Security release: the legacy signature is refused by default.
verifyBubblioWebhookandcreateBubblioToolRouterequire the replay-protected pairX-Bubblio-Signature-V2+X-Bubblio-Timestamp; a request carrying only the body-onlyX-Bubblio-Signatureis 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.onFailurecallback 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.idempotencyKeyon Agent Door confirms (the quote id) when the platform sends it — treatundefinedas "no dedupe anchor available", not "first delivery".