@buildbase/sdk 0.0.53 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -72,8 +72,8 @@ Config also accepts: `timeout`, `maxRetries`, `debug`, `headers`, `onError(err,
72
72
  - **Factory:** `BuildBase` (also default export) → `{ auth, workspace, subscription, usage, credits, plans, invoices, users, features, settings, notification, permissions, withSession, client }`.
73
73
  - **API classes:** `BaseApi` (extend for custom APIs), `AuthApi`, `UserApi`, `WorkspaceApi`, `SettingsApi`, `PushApi`.
74
74
  - **Webhook verification (runtime-agnostic — Node, edge, Deno, Bun, browsers):** `verifyWebhookSignature`, `parseWebhookEvent`. Verify against the **raw** request body (`await req.text()`), not the parsed JSON.
75
- - **Agent readiness (framework-agnostic, returns plain `DiscoveryDocument`):** **App owns discovery content, platform owns auth.** All content — `llms.txt`, `robots.txt`, `sitemap`, `protectedResources` (RFC 9728), API catalog, skills — is defined locally in `AgentReadyConfig` and served with no platform round-trip. The platform bundle (`fetchAgentReadiness`, fail-soft/cached) supplies ONLY `{ enabled, authorizationServer }` (the OAuth AS pointer). Functions: `resolveAgentPath` (dispatch ALL agent paths — root docs + `.well-known/*`; prefer for new wiring), `resolveWellKnown` (`.well-known/*` only), `clearAgentReadinessCache`, `buildAgentCard`, `buildProtectedResourceMetadata(path, config)` (local), `buildAgentSkillsIndex`, `buildSkillMd`, `buildSecurityTxt`, `buildLlmsTxt(config)` (from `config.llmsTxt`), `buildRobotsTxt`, `buildSitemap`, `buildApiCatalog`, `buildMcpServerCard`, `buildAuthMd`, `buildDiscoveryLinkHeader`, `wantsMarkdown`/`negotiateMarkdown`, `sha256Digest`, `AI_BOT_USER_AGENTS`. Types: `AgentReadyConfig`, `AgentReadinessBundle`, `AgentSkill`, `ApiCatalogApi`, `McpServerCard`, `RobotsConfig`, `RobotsPolicy`, `ContentSignals`, `SitemapUrl`, `DiscoveryDocument`.
76
- - **OAuth2 app-bridge:** `handleAppTokenRequest`, `handleAppRevokeRequest` (one-call handlers), `bearerChallenge` (RFC 9728/6750 401; header values are quoted-string-escaped + CR/LF-stripped), `verifyAppTokenRequest`, `verifyAppRevokeRequest`, `verifyClientJwt`, `signClientJwt` (HS256 mint — symmetric counterpart of verify), `extractBearerToken`, `appTokenSuccess`, `appTokenFailure`, `AppBridgeError`. Types: `AppTokenRequestClaims`, `AppRevokeRequestClaims`, `AppTokenResult`, `AppTokenResponseBody`, `HandlerResult`, `VerifyClientJwtOptions`. `verifyClientJwt(token, secret, opts?)` **requires a numeric `exp` by default** (`requireExp: false` to allow non-expiring tokens); optional `issuer`/`audience` pinning; 3rd arg still accepts a bare `clockToleranceSec` number for back-compat.
75
+ - **Agent readiness (framework-agnostic, returns plain `DiscoveryDocument`):** **App owns discovery content, platform owns auth.** All content — `llms.txt`, `robots.txt`, `sitemap`, `protectedResources` (RFC 9728), API catalog, skills — is defined locally in `AgentReadyConfig` and served with no platform round-trip. The platform bundle (`fetchAgentReadiness`, fail-soft/cached) supplies ONLY `{ enabled, authorizationServer }` (the OAuth AS pointer). Functions: `resolveAgentPath` (dispatch ALL agent paths — root docs + `.well-known/*`; prefer for new wiring), `resolveWellKnown` (`.well-known/*` only), `clearAgentReadinessCache`, `buildAgentCard`, `buildA2AAgentCard` (A2A card at `/.well-known/agent-card.json`; served by DEFAULT, derived from `site`+`skills`; `a2aCard: false` disables, object overrides), `buildProtectedResourceMetadata(path, config)` (local), `buildAgentSkillsIndex`, `buildSkillMd`, `buildSecurityTxt`, `buildLlmsTxt(config)` (from `config.llmsTxt`), `buildLlmsFullTxt` (from `config.llmsFullTxt`), `buildRobotsTxt`, `buildSitemap`, `buildApiCatalog`, `buildMcpServerCard`, `buildAuthMd`, `buildWebBotAuthDirectory` (JWKS at `/.well-known/http-message-signatures-directory` from `config.webBotAuth.keys`; opt-in), `buildDnsAidRecords` (suggested `_agents` DNS records — publish at your DNS provider, can't be served over HTTP), `buildDiscoveryLinkHeader`, `wantsMarkdown`/`negotiateMarkdown`, `sha256Digest`, `AI_BOT_USER_AGENTS`. `config.extraPaths` serves ANY literal document by exact path (commerce discovery: x402/UCP/ACP/MPP, `/openapi.json`, future `.well-known` docs) and takes precedence over built-ins. Types: `AgentReadyConfig`, `AgentReadinessBundle`, `AgentSkill`, `ApiCatalogApi`, `A2ACardConfig`, `A2ACardSkill`, `DnsAidRecord`, `McpServerCard`, `RobotsConfig`, `RobotsPolicy`, `ContentSignals`, `SitemapUrl`, `DiscoveryDocument`.
76
+ - **OAuth2 app-bridge:** `handleAppTokenRequest`, `handleAppRevokeRequest` (one-call handlers), `bearerChallenge` (RFC 9728/6750 401; header values are quoted-string-escaped + CR/LF-stripped), `verifyAppTokenRequest`, `verifyAppRevokeRequest`, `verifyClientJwt`, `signClientJwt` (HS256 mint — symmetric counterpart of verify), `extractBearerToken`, `appTokenSuccess`, `appTokenFailure`, `AppBridgeError`. Types: `AppTokenRequestClaims`, `AppRevokeRequestClaims`, `AppTokenResult`, `AppTokenResponseBody`, `HandlerResult`, `VerifyClientJwtOptions`. `verifyClientJwt(token, secret, opts?: VerifyClientJwtOptions)` **requires a numeric `exp` by default** (`requireExp: false` to allow non-expiring tokens); optional `clockToleranceSec` / `issuer` / `audience`. **Presets** (also in core): `mintAgentToken` (the whole mint in one call — HS256 with the app's secret, `aud` from the grant's resource, encrypted `sid`) + `buildbaseAuth` (the whole verify) + `createSessionRefCrypto`. Security invariant: the platform relays the minted token but never sees the app secret — it can neither forge nor decode app tokens, and the BuildBase session is never stored and never plaintext in a token.
77
77
  - **Currency & pricing utils (pure):** `formatCents`, `formatOverageRate`, `formatOverageRateWithLabel`, `formatQuotaIncludedOverage`, `formatQuotaWithPrice`, `getCurrencySymbol`, `getCurrencyFlag`, `getQuotaUnitLabelFromName`, `getQuotaDisplayValue`, `CURRENCY_DISPLAY`, `CURRENCY_FLAG`, `PLAN_CURRENCY_CODES`, `PLAN_CURRENCY_OPTIONS`.
78
78
  - **Pricing variants (multi-currency):** `getPricingVariant`, `getBasePriceCents`, `getStripePriceIdForInterval`, `getQuotaOverageCents`, `getQuotaDisplayWithVariant`, `getAvailableCurrenciesFromPlans`, `getDisplayCurrency`, `getBillingIntervalAndCurrencyFromPriceId`.
79
79
  - **Seat pricing:** `calculateBillableSeats`, `calculateSeatOverageCents`, `calculateTotalSubscriptionCents`, `getPerSeatPriceCents`, `getSeatPricing`.
@@ -109,12 +109,18 @@ Config also accepts: `timeout`, `maxRetries`, `debug`, `headers`, `onError(err,
109
109
 
110
110
  ### `@buildbase/sdk/mcp` — MCP server (server-only, zero React)
111
111
 
112
- Expose a live MCP server (stateless Streamable HTTP; Node 18+/edge) with built-in BuildBase tools + custom zod-schema tools.
112
+ **Full guide: `docs/MCP-AND-AGENT-READINESS.md`** (mental model, fast path, auth, scope/resource customization).
113
113
 
114
- - **Handler:** `createMcpHandler({ buildbase?, auth, tools?, builtinTools?, context?, serverInfo?, instructions?, allowedOrigins?, maxRequestBytes?, rateLimit?, formatToolError?, onError? })` → `{ handle(McpHttpRequest), fetch(Request), listTools(), serverCard({ endpoint }) }`. `fetch` is the Web adapter (Next.js route: `export const POST = (req) => mcp.fetch(req)`); `handle` is the pure form (Express). `auth.verify(token) → McpAuthInfo { sessionId, userId?, workspaceId?, scopes? }` maps the app's Bearer token (any format) to the BuildBase session tools run under; `auth.resourceMetadataUrl` makes 401s carry the RFC 9728 challenge. `auth: false` = dev only. **Hardening:** `maxRequestBytes` (default 1 MiB → 413), `rateLimit(auth, req)` gate (after auth; `false`/`{ok:false,retryAfter}` → 429, fails closed), `allowedOrigins` (non-matching browser Origin → 403; also drives CORS — `OPTIONS` preflight → 204 + `Access-Control-*`, matching Origin echoed on responses), `formatToolError(err, {tool})` (redact client-facing tool-error text; full error still hits `onError`). **No lock-in:** `buildbase` is optional (standalone server, only your tools); `context(auth, req)` injects your own per-request context as `ctx.custom`; custom tools override same-named built-ins; raw JSON Schema tools skip zod.
114
+ Expose a live MCP server (stateless Streamable HTTP; Node 18+/edge) with built-in BuildBase tools + custom zod-schema tools. **Spec-compliant to MCP 2025-06-18**: server card SEP-1649 v1.0 (`$schema`, `version:"1.0"`, `protocolVersion`, `transport:{type,url}`, boolean caps), discovery manifest SEP-1960, auth via RFC 9728→8414→8707+PKCE. The platform issues **public clients** (RFC 7591 `token_endpoint_auth_method:"none"`, no secret, PKCE-only) so Claude Desktop/Cursor/ChatGPT/Inspector connect with zero manual config.
115
+
116
+ - **Handler:** `createMcpHandler({ buildbase?, auth, tools?, builtinTools?, context?, serverInfo?, instructions?, allowedOrigins?, maxRequestBytes?, rateLimit?, formatToolError?, onError? })` → `{ handle(McpHttpRequest), fetch(Request), listTools(), serverCard({ endpoint }) }`. `fetch(Request)` is the Web adapter (Next.js App Router, Hono, Bun, Deno, Cloudflare Workers, edge); `handle({method,headers,body})` is the pure form (Express, Fastify, Next.js Pages Router, Node http). Per-framework copy-paste recipes + mint route + production checklist in `docs/MCP-AND-AGENT-READINESS.md`. `auth.verify(token) → McpAuthInfo { sessionId, userId?, workspaceId?, scopes? }` maps the app's Bearer token (any format) to the BuildBase session tools run under; `auth.resourceMetadataUrl` makes 401s carry the RFC 9728 challenge. `auth: false` = dev only. **Hardening:** `maxRequestBytes` (default 1 MiB → 413), `rateLimit(auth, req)` gate (after auth; `false`/`{ok:false,retryAfter}` → 429, fails closed), `allowedOrigins` (non-matching browser Origin → 403; also drives CORS — `OPTIONS` preflight → 204 + `Access-Control-*`, matching Origin echoed on responses), `formatToolError(err, {tool})` (redact client-facing tool-error text; full error still hits `onError`). **No lock-in:** `buildbase` is optional (standalone server, only your tools); `context(auth, req)` injects your own per-request context as `ctx.custom`; custom tools override same-named built-ins; raw JSON Schema tools skip zod.
115
117
  - **Tools:** `defineMcpTool` (typed custom tools), `builtinMcpTools`, `selectBuiltinTools`. Built-ins cover the **full BuildBase surface** (42 tools: workspaces/users/subscription/plans/invoices/usage/credits/features/permissions/settings — reads + writes + destructive). **Default `builtinTools: 'readonly'`** (least privilege) — reads only until you opt into `'all'` (or `{ include }`); narrow further with `false` | `{ exclude }`. Every tool runs under the user's session → platform enforces the user's real permissions (agent can't exceed the user). Built-ins carry **no** scope requirement (agent = authenticated app user; read/write boundary is `builtinTools`, not agent scopes). Per-scope gating is opt-in on custom tools via `requiredScopes` + the `scopes` returned from `auth.verify` (your scope names); a token with no scopes sees only tools that require none.
118
+ - **One-config stack:** `createAgentStack({ serverUrl, orgId, siteUrl, site, secret, mcp?, discovery? })` → `{ mcp, mcpEndpoint, config, linkHeader, resolvePath, serveAgentPath, routes }` — derives the MCP endpoint, server card, RFC 9728 protected resource, API catalog entry, BuildBase client, `buildbaseAuth`, and CORS from ONE object. Next.js wiring: `export const { GET, POST, DELETE, OPTIONS } = agent.routes` for `/api/mcp`, `export const GET = agent.serveAgentPath` for discovery routes. Everything derived is overridable via `mcp.*` / `discovery.*`.
119
+ - **BuildBase auth presets** (pure local crypto — the app's secret and tokens NEVER reach the platform): `mintAgentToken({ claims, secret, expiresInSec?, extraClaims? })` (drop into `handleAppTokenRequest.mintToken`; signs HS256 with YOUR secret, binds `aud` to the RFC 8707 resource, embeds the per-user session as an encrypted `sid` claim) and `buildbaseAuth({ secret, resource?, resourceMetadataUrl?, requireAudience?, verify? })` (the `auth` config for `createMcpHandler`: verify + RFC 8707 audience binding + `sid` decryption + derived `resourceMetadataUrl`; `resource` accepts `string|string[]`; `MCP_AUTH_DEBUG=1` logs received-vs-expected `aud` on rejection). `createSessionRefCrypto(secret)` exposes the underlying AES-256-GCM `sid` crypto (WebCrypto; wire format `base64url(iv|tag|ciphertext)`, key `SHA-256(secret+":bb-session")` — byte-compatible with Node `createCipheriv` implementations of the same layout).
120
+ - **Scopes & resources (restrict or open up) — ALL app-owned; the shared AS is scope/resource-agnostic:** THREE scope levers — (1) declare a catalog `scopes:[{name,description}]` on `AgentReadyConfig`/`createAgentStack` → `scopes_supported` in the app's OWN RFC 9728 protected-resource metadata (served from the app origin); (2) gate per custom tool via `requiredScopes` (hidden/refused unless the token carries all — YOUR scope names); (3) the floor — every tool runs under the user's session so the platform enforces the user's real permissions (agent ⊆ user). Built-ins are restricted by `builtinTools`, not scopes. TWO resource levers — declare `protectedResources` (RFC 9728: stack derives root + `<host>/mcp` + endpoint) and bind the token `aud` via `buildbaseAuth({ resource, requireAudience })` (RFC 8707) — the audience check at YOUR resource is the whole gate (token for A rejected at B); the AS just passes `resource` through opaquely (no per-org scope/resource lists, no `invalid_target`). Defaults are least-privilege; widen deliberately. RFC 9728 convention: canonical MCP resource is `<host>/mcp` (metadata at `/.well-known/oauth-protected-resource/mcp`), 401 `WWW-Authenticate` points there.
121
+ - **CORS:** on by default (`Access-Control-Allow-Origin: *` — safe for a Bearer-token API; reflects `allowedOrigins` when set). Narrow with `cors: [origins]` or disable with `cors: false`. Preflight answered automatically — never wrap the route in your own CORS.
116
122
  - **Auth helpers re-exported:** `signClientJwt`, `verifyClientJwt`, `extractBearerToken`, `bearerChallenge`, `AppBridgeError`.
117
- - Types: `CreateMcpHandlerConfig`, `McpHandler`, `McpToolDefinition`, `McpToolContext`, `McpAuthInfo`, `McpHttpRequest`, `McpHttpResponse`, `McpToolAnnotations`, `McpBuildBaseClient`, `BuiltinMcpToolName`.
123
+ - Types: `CreateMcpHandlerConfig`, `McpHandler`, `McpToolDefinition`, `McpToolContext`, `McpAuthInfo`, `McpHttpRequest`, `McpHttpResponse`, `McpToolAnnotations`, `McpBuildBaseClient`, `BuiltinMcpToolName`, `AgentStack`, `AgentStackConfig`, `BuildBaseAuthOptions`, `MintAgentTokenOptions`, `SessionRefCrypto`.
118
124
 
119
125
  ## Server actions cheat-sheet
120
126
 
package/CHANGELOG.md CHANGED
@@ -5,6 +5,52 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [0.0.54] - 2026-07-10
11
+
12
+ MCP + agent-readiness brought to **MCP 2025-06-18** compliance, a one-config setup path, first-class auth presets, and a full per-framework guide. **Behavior changes** are marked ⚠️.
13
+
14
+ ### Added
15
+
16
+ - **`createAgentStack(config)` — the whole MCP + agent-readiness surface from one config object** (`@buildbase/sdk/mcp`). Derives the MCP handler, the SEP-1649 server card, the RFC 9728 protected-resource metadata (root + canonical `<host>/mcp` + endpoint), the API-catalog entry, the BuildBase client, `buildbaseAuth`, CORS, and the discovery `Link` header. Returns `{ mcp, mcpEndpoint, config, linkHeader, resolvePath, serveAgentPath, routes }` — Next.js wires `export const { GET, POST, DELETE, OPTIONS } = agent.routes` + `export const GET = agent.serveAgentPath`. Everything derived is overridable via `mcp.*` / `discovery.*`.
17
+ - **Auth presets — the app mints & verifies its own tokens with pure local crypto; the platform never sees the secret.** `mintAgentToken({ claims, secret })` (drop into `handleAppTokenRequest.mintToken` — HS256 with your secret, `aud` from the granted RFC 8707 resource, per-user BuildBase session embedded as an encrypted `sid`), `buildbaseAuth({ secret, resource, requireAudience })` (the `auth` config for `createMcpHandler` — verify + audience binding + `sid` decrypt + derived `resourceMetadataUrl`; `resource` accepts `string | string[]`), and `createSessionRefCrypto(secret)` (the underlying AES-256-GCM `sid` crypto, WebCrypto, wire-format `base64url(iv|tag|ciphertext)`, key `SHA-256(secret+":bb-session")` — byte-compatible with a Node `createCipheriv` layout). Also exported from the core entry. `MCP_AUTH_DEBUG=1` logs received-vs-expected `aud` on rejection.
18
+ - **A2A Agent Card** at `/.well-known/agent-card.json` (`buildA2AAgentCard`) — served **by default**, derived from `site` + `skills`; `a2aCard: false` disables, an object overrides. Advertised on the Agent Card capabilities and the discovery `Link` header.
19
+ - **`AgentReadyConfig.scopes` — an app scope catalog** (`AppScope[]`, `{ name, description }`) that drives `scopes_supported` in your RFC 9728 protected-resource metadata. Scopes are app-owned.
20
+ - **More discovery builders / paths:** `buildLlmsFullTxt` (`/llms-full.txt`), `buildWebBotAuthDirectory` (JWKS at `/.well-known/http-message-signatures-directory`, opt-in), `buildMcpDiscoveryManifest` (SEP-1960 `/.well-known/mcp.json`), `buildDnsAidRecords` (suggested `_agents` DNS records to publish at your DNS provider), and `config.extraPaths` (serve any literal document by exact path — commerce discovery x402/UCP/ACP/MPP, `/openapi.json`, future well-knowns; takes precedence over built-ins).
21
+ - **Per-framework guide** — `docs/MCP-AND-AGENT-READINESS.md`: the complete flows (cold-start, mint, verify), the scope/resource model, and copy-paste recipes for Next.js (App + Pages Router), Express, Fastify, Hono, Bun, Deno, Cloudflare Workers, and React/SPA (WebMCP), plus a production checklist.
22
+
23
+ ### Changed
24
+
25
+ - **MCP Server Card is now SEP-1649 v1.0.** ⚠️ **Output shape change.** `/.well-known/mcp/server-card.json` now emits `$schema`, `version: "1.0"`, `protocolVersion: "2025-06-18"`, `transport: { type, url }` (was `transport.endpoint`), and **boolean** `capabilities` (`{ tools: true, resources: false, prompts: false }`; a legacy `{ tools: {} }`-style object is normalized). `/.well-known/mcp.json` (SEP-1960) gains `$schema` + `version: "1.0"` and a per-server `transport` object. `McpServerCard` gains an optional `protocolVersion`.
26
+ - **CORS is on by default on `createMcpHandler`.** ⚠️ **Behavior change.** Responses now carry `Access-Control-Allow-Origin: *` (safe for a Bearer-token API — no cookies) and `OPTIONS` is answered automatically, so browser MCP clients work with zero config. New `cors` option: `cors: [origins]` narrows to those origins (reflected), `cors: false` disables. `allowedOrigins`, when set, still restricts and drives reflection.
27
+ - **`buildbaseAuth` audience binding accepts multiple resources** and takes an explicit `resourceMetadataUrl`; `createAgentStack` binds both the canonical `<host>/mcp` and the literal endpoint URL and points the 401 challenge at `/.well-known/oauth-protected-resource/mcp`.
28
+
29
+ ### Security
30
+
31
+ - **API retries are restricted to idempotent methods.** ⚠️ **Behavior change.** Automatic retries (network error / 5xx) now apply only to GET/HEAD/OPTIONS/PUT/DELETE. POST/PATCH are never replayed — the server may have processed a request whose response was lost, so replaying `purchase_credits`, `consume_credits`, or `create_checkout_session` risked double-charging. The retry backoff also respects the abort signal now (an unmount or timeout ends the wait immediately).
32
+ - **Open-redirect hardening in `validateRedirectUrl` / `safeRedirect`.** Relative paths (`/dashboard`) are now accepted (protocol-relative `//host` and backslash `/\host` forms are not); new `RedirectValidationOptions` (`{ sameOrigin, allowedOrigins }`) restricts absolute URLs; `safeRedirect` validates its fallback URL too. ⚠️ Auth return URLs (`saveAuthIntent`/`consumeAuthIntent`) are now **same-origin only** — a tampered localStorage intent can no longer redirect off-site. Stripe/OAuth cross-origin `https:` targets are unaffected by default. Also fixed: the `http://[::1]` localhost allowance never matched (`URL.hostname` includes the brackets).
33
+ - **`verifyClientJwt` pins the JWT `alg` before computing the HMAC** (was checked after) — hardens the verifier against algorithm-confusion if it is ever extended beyond HS256.
34
+
35
+ ### Fixed
36
+
37
+ - **Zero-decimal currencies (JPY, KRW, …) format correctly.** `formatCents(1000, 'jpy')` now renders `¥1,000` (was `¥10.00` — a ÷100 on a currency with no minor unit). Fixed across `formatCents`, `formatOverageRate`, `formatOverageRateWithLabel`, `formatQuotaWithPrice`, and `getQuotaDisplayParts`; new `isZeroDecimalCurrency(currency)` and `minorAmountToDisplay(amount, currency)` are exported.
38
+ - **Stale-response races in the workspace data hooks.** All 15 read hooks (`useSubscription`, `useAllQuotaUsage`, `useCreditBalance`, `usePlanGroup`, `useInvoices`, `useUsageLogs`, `usePublicPlans`, and the rest of the subscription/credit read surface) now drop superseded responses — a workspace switch can no longer render the previous workspace's subscription, quotas, credits, or invoices. Only the latest request clears `loading`; unmount aborts.
39
+ - **`useSaaSSettings` no longer loses org settings for the session under React StrictMode.** The deduplicated global settings fetch was bound to the first caller's abort signal, so StrictMode's throwaway first mount aborted it for every waiter with no retry. The shared fetch is now detached — its result lands in the global store regardless of any one component's lifetime.
40
+ - **`UserProvider` state split per resource.** Attributes and features now have independent loading/error (`attributesLoading`/`featuresLoading`, `attributesError`/`featuresError`; the combined `isLoading`/`error` remain for back-compat) — a features failure no longer surfaces as an attributes error, stale errors clear when a new request starts, and `useUserFeatures().isLoading/error` now reflect the features pipeline only, as documented.
41
+ - **`createBBUrl` throws on an invalid explicit base URL** instead of silently substituting `https://localhost` (which sent Stripe success/cancel URLs to localhost with no error). The localhost default remains only for the no-argument server-side case.
42
+ - **`createWorkspace`'s delayed plan-picker open is cancelled on unmount** — it can no longer pop open after the user navigated away or signed out during the 300ms delay.
43
+ - **`workspaceSettingsManager.clearParams()` notifies subscribers** (React state no longer diverges from the manager), and `getState()` returns a stable snapshot reference.
44
+
45
+ ### Removed
46
+
47
+ - **`verifyClientJwt`'s legacy bare-`number` third argument.** ⚠️ **Behavior change.** The third parameter is now `VerifyClientJwtOptions` only (`{ clockToleranceSec?, requireExp?, issuer?, audience? }`); pass `{ clockToleranceSec: n }` instead of a bare number.
48
+
49
+ ### Internal
50
+
51
+ - Centralized the duplicated base64url codec into `src/lib/base64url.ts` (was copied in `agent-bridge.ts` and `agent-auth.ts`).
52
+ - **Test infrastructure (vitest): 113 tests** across sha256/HMAC vectors, webhook verification, agent-bridge JWT (round-trip, alg-confusion, exp/nbf, issuer/audience pinning, header-injection escaping), agent-auth session crypto (Node-crypto compatibility both directions), agent stack, permissions revoke/unknown-role semantics, redirect validation, URL params, API retry idempotency + abortable backoff, zero-decimal currency math — plus a **packaging test** asserting every value declared in each entry point's `.d.ts` exists in the runtime bundle (would have auto-caught the 0.0.51 `/react` phantom-exports bug). `@types/node` added; test files are now typechecked (`tsconfig.json` no longer excludes them; the build still does via `tsconfig.build.json`).
53
+
8
54
  ## [0.0.53] - 2026-07-09
9
55
 
10
56
  Production-readiness pass — MCP + agent-bridge hardening, permission-resolution correctness, auth-session teardown, and settings/a11y/i18n fixes. **Behavior changes** are marked ⚠️ (`builtinTools` default; permission-override semantics); the rest are additive or internal.
package/README.md CHANGED
@@ -2935,7 +2935,11 @@ app.post('/webhooks/buildbase', (req, res) => {
2935
2935
 
2936
2936
  ## Agent Readiness (Discovery)
2937
2937
 
2938
- Make a consuming app [agent ready](https://isitagentready.com) by serving the standard machine-readable discovery documents an AI agent looks for — Agent Card, OAuth metadata (RFC 8414/9728), Agent Skills, API Catalog (RFC 9727), MCP Server Card, `robots.txt` with AI-bot rules + Content Signals, `sitemap.xml`, `security.txt` (RFC 9116), `llms.txt`, and `auth.md` — with the BuildBase server as the source of truth. Framework-agnostic and zero React: every function returns a plain `DiscoveryDocument` (`{ status, contentType, body, cacheControl, vary? }`); you wire it into your own router.
2938
+ > **Full guide:** [`docs/MCP-AND-AGENT-READINESS.md`](docs/MCP-AND-AGENT-READINESS.md) covers the whole surface — the fast path, the auth model, and the scope/resource customization spectrum. This section is the overview.
2939
+
2940
+ Make a consuming app [agent ready](https://isitagentready.com) by serving the machine-readable discovery documents an AI client looks for — Agent Card, A2A card, OAuth metadata (RFC 8414/9728), Agent Skills, API Catalog (RFC 9727), MCP Server Card (SEP-1649 v1.0) + discovery manifest (SEP-1960), `robots.txt` with AI-bot rules + Content Signals, `sitemap.xml`, `security.txt`, `llms.txt`/`llms-full.txt`, and `auth.md`. Framework-agnostic and zero React: every function returns a plain `DiscoveryDocument` (`{ status, contentType, body, cacheControl, vary? }`).
2941
+
2942
+ The fast path is [`createAgentStack`](docs/MCP-AND-AGENT-READINESS.md#fast-path--createagentstack) — one config derives the MCP handler **and** the whole discovery surface. To wire the discovery layer on its own, use `resolveAgentPath`:
2939
2943
 
2940
2944
  ```ts
2941
2945
  // lib/agent-ready.ts
@@ -2947,11 +2951,15 @@ export const agentConfig: AgentReadyConfig = {
2947
2951
  siteUrl: 'https://imejis.io',
2948
2952
  site: { name: 'Imejis', description: 'Generate images from templates via API.' },
2949
2953
  // Discovery content is YOURS — defined here in code, not in the platform:
2954
+ scopes: [
2955
+ { name: 'designs:read', description: 'View designs' },
2956
+ { name: 'render:execute', description: 'Render images' },
2957
+ ],
2950
2958
  llmsTxt: '# Imejis\n\n> Generate images from templates via API.',
2951
2959
  robots: { contentSignals: { search: true, aiInput: true, aiTrain: false } },
2952
2960
  sitemap: { urls: ['/', '/pricing'] }, // omit if you already generate sitemaps
2953
- protectedResources: [{ scopes: ['read:profile', 'write:posts'] }], // RFC 9728
2954
- // skills, security, apiCatalog, mcpServerCard, authMd, cacheTtlSeconds — all optional
2961
+ protectedResources: [{ resource: 'https://imejis.io/mcp' }], // scopes inherited from catalog
2962
+ // skills, security, apiCatalog, mcpServerCard, a2aCard, extraPaths — all optional
2955
2963
  };
2956
2964
 
2957
2965
  export async function serveAgentPath(req: Request): Promise<Response> {
@@ -3031,21 +3039,61 @@ Clean split, no duplication: **your app owns discovery content, the platform own
3031
3039
  | llms.txt | `buildLlmsTxt` |
3032
3040
  | MCP Server Card + live MCP server | `buildMcpServerCard` + [`@buildbase/sdk/mcp`](#mcp-server-buildbasesdkmcp) |
3033
3041
  | Agent Skills / API Catalog | `buildAgentSkillsIndex` / `buildApiCatalog` |
3042
+ | A2A Agent Card | `buildA2AAgentCard` (served by default from `site` + `skills`) |
3034
3043
  | OAuth discovery (RFC 8414) + protected resource (RFC 9728) | `resolveAgentPath` (platform-backed) |
3035
- | DNS-AID | out of SDK scope — add the DNS record at your registrar |
3036
- | Web Bot Auth, commerce (x402/ACP/UCP) | not yet — planned |
3044
+ | Web Bot Auth | `buildWebBotAuthDirectory` (opt-in — publish your JWKS) |
3045
+ | DNS-AID | `buildDnsAidRecords` (returns records — publish at your DNS provider) |
3046
+ | Commerce (x402/ACP/UCP/MPP), openapi.json | `config.extraPaths` (serve any literal document by path) |
3037
3047
 
3038
- **Functions:** `resolveAgentPath`, `resolveWellKnown`, `fetchAgentReadiness`, `clearAgentReadinessCache`, `buildAgentCard`, `buildProtectedResourceMetadata`, `buildAgentSkillsIndex`, `buildSkillMd`, `buildSecurityTxt`, `buildLlmsTxt`, `buildRobotsTxt`, `buildSitemap`, `buildApiCatalog`, `buildMcpServerCard`, `buildAuthMd`, `buildDiscoveryLinkHeader`, `wantsMarkdown`, `negotiateMarkdown`, `provideWebMcpTools`, `sha256Digest`. **Constants:** `AI_BOT_USER_AGENTS`. **Types:** `AgentReadyConfig`, `AgentReadinessBundle`, `AgentSkill`, `ApiCatalogApi`, `McpServerCard`, `RobotsConfig`, `RobotsPolicy`, `ContentSignals`, `SitemapUrl`, `WebMcpTool`, `DiscoveryDocument`.
3048
+ **Functions:** `resolveAgentPath`, `resolveWellKnown`, `fetchAgentReadiness`, `clearAgentReadinessCache`, `buildAgentCard`, `buildA2AAgentCard`, `buildProtectedResourceMetadata`, `buildAgentSkillsIndex`, `buildSkillMd`, `buildSecurityTxt`, `buildLlmsTxt`, `buildLlmsFullTxt`, `buildRobotsTxt`, `buildSitemap`, `buildApiCatalog`, `buildMcpServerCard`, `buildMcpDiscoveryManifest`, `buildAuthMd`, `buildWebBotAuthDirectory`, `buildDnsAidRecords`, `buildDiscoveryLinkHeader`, `wantsMarkdown`, `negotiateMarkdown`, `provideWebMcpTools`, `sha256Digest`. **Constants:** `AI_BOT_USER_AGENTS`. **Types:** `AgentReadyConfig`, `AgentReadinessBundle`, `AgentSkill`, `AppScope`, `ApiCatalogApi`, `A2ACardConfig`, `A2ACardSkill`, `DnsAidRecord`, `McpServerCard`, `RobotsConfig`, `RobotsPolicy`, `ContentSignals`, `SitemapUrl`, `WebMcpTool`, `DiscoveryDocument`.
3039
3049
 
3040
3050
  ## MCP Server (`@buildbase/sdk/mcp`)
3041
3051
 
3042
- Turn your app into a **live MCP server** so AI agents can operate it: the built-in tools expose your BuildBase state (workspaces, subscription, quotas, credits, feature flags, permissions) and you add your own product tools with zod schemas. Stateless Streamable HTTP — pure functions plus a Web-standard `Request`/`Response` adapter, so it runs on Node 18+, edge, Deno, and Bun. Server-only, zero React; split from the core entry because it uses `zod` at runtime.
3052
+ > **Full guide:** [`docs/MCP-AND-AGENT-READINESS.md`](docs/MCP-AND-AGENT-READINESS.md).
3053
+
3054
+ Turn your app into a **live MCP server** so AI agents can operate it: the built-in tools expose your BuildBase state (workspaces, subscription, quotas, credits, feature flags, permissions) and you add your own product tools with zod schemas. Stateless Streamable HTTP (MCP 2025-06-18) — pure functions plus a Web-standard `Request`/`Response` adapter, so it runs on Node 18+, edge, Deno, and Bun. Server-only, zero React; split from the core entry because it uses `zod` at runtime.
3055
+
3056
+ The fast path bundles the server + the whole discovery surface from one config:
3057
+
3058
+ ```ts
3059
+ // lib/agent.ts
3060
+ import { createAgentStack, defineMcpTool } from '@buildbase/sdk/mcp';
3061
+ import { z } from 'zod';
3062
+
3063
+ export const agent = createAgentStack({
3064
+ serverUrl: process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL!,
3065
+ orgId: process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID!,
3066
+ siteUrl: 'https://imejis.io',
3067
+ site: { name: 'Imejis', description: 'Generate images from templates via API.' },
3068
+ secret: process.env.SYSTEM_SECRET!, // derives buildbaseAuth (verify + aud + sid decrypt)
3069
+ scopes: [{ name: 'render:execute', description: 'Render images' }],
3070
+ mcp: {
3071
+ builtinTools: 'readonly',
3072
+ tools: [
3073
+ defineMcpTool({
3074
+ name: 'generate_image',
3075
+ description: 'Render an image from a template',
3076
+ inputSchema: z.object({ templateId: z.string(), data: z.record(z.string(), z.any()) }),
3077
+ requiredScopes: ['render:execute'],
3078
+ execute: async (input, { bb, workspaceId }) => {
3079
+ await bb.usage.record(workspaceId!, { quotaSlug: 'images', quantity: 1 });
3080
+ return renderTemplate(input.templateId, input.data);
3081
+ },
3082
+ }),
3083
+ ],
3084
+ },
3085
+ });
3086
+
3087
+ // app/api/mcp/route.ts
3088
+ export const { GET, POST, DELETE, OPTIONS } = agent.routes;
3089
+ ```
3090
+
3091
+ Prefer the primitives (own token format, unified web+agent auth)? Wire `createMcpHandler` + `buildbaseAuth` directly — `buildbaseAuth` does the full verify (HS256, RFC 8707 audience binding, `sid` decrypt) so you never hand-roll it:
3043
3092
 
3044
3093
  ```ts
3045
3094
  // lib/mcp.ts
3046
3095
  import BuildBase from '@buildbase/sdk';
3047
- import { createMcpHandler, defineMcpTool, verifyClientJwt } from '@buildbase/sdk/mcp';
3048
- import { z } from 'zod';
3096
+ import { createMcpHandler, defineMcpTool, buildbaseAuth } from '@buildbase/sdk/mcp';
3049
3097
 
3050
3098
  const buildbase = BuildBase({
3051
3099
  serverUrl: process.env.BUILDBASE_URL!,
@@ -3055,50 +3103,29 @@ const buildbase = BuildBase({
3055
3103
  export const mcp = createMcpHandler({
3056
3104
  buildbase,
3057
3105
  serverInfo: { name: 'imejis', version: '1.0.0' },
3058
- auth: {
3059
- // Verify the Bearer token your app minted in the OAuth2 app-bridge flow
3060
- // and map it to the BuildBase session the tools run under.
3061
- verify: token => {
3062
- const c = verifyClientJwt(token, process.env.BUILDBASE_CLIENT_SECRET!);
3063
- return { sessionId: String(c.sid), userId: String(c.sub), scopes: c.scope as string[] };
3064
- },
3065
- // Unauthenticated requests get a 401 whose WWW-Authenticate header points
3066
- // here, so agents bootstrap the whole OAuth discovery flow (RFC 9728).
3067
- resourceMetadataUrl: 'https://imejis.io/.well-known/oauth-protected-resource',
3068
- },
3069
- builtinTools: 'readonly', // default; 'all' opts into the mutating tools
3106
+ auth: buildbaseAuth({
3107
+ secret: process.env.SYSTEM_SECRET!,
3108
+ resource: ['https://imejis.io/mcp', 'https://imejis.io/api/mcp'],
3109
+ requireAudience: true,
3110
+ }),
3111
+ builtinTools: 'readonly',
3070
3112
  tools: [
3071
- defineMcpTool({
3072
- name: 'generate_image',
3073
- description: 'Render an image from a template',
3074
- inputSchema: z.object({ templateId: z.string(), data: z.record(z.string(), z.any()) }),
3075
- annotations: { readOnlyHint: false },
3076
- execute: async (input, { bb, workspaceId }) => {
3077
- await bb.usage.record(workspaceId!, { quotaSlug: 'images', quantity: 1 });
3078
- return renderTemplate(input.templateId, input.data);
3079
- },
3080
- }),
3113
+ /* defineMcpTool(...) */
3081
3114
  ],
3082
3115
  });
3083
3116
  ```
3084
3117
 
3085
3118
  ```ts
3086
- // app/api/mcp/route.ts (Next.js App Router)
3087
- import { mcp } from '@/lib/mcp';
3088
- export const POST = (req: Request) => mcp.fetch(req);
3089
- export const GET = (req: Request) => mcp.fetch(req); // 405 per spec (no SSE)
3090
- export const DELETE = (req: Request) => mcp.fetch(req); // 405 (stateless)
3091
- ```
3092
-
3093
- ```ts
3094
- // Express — the pure form
3119
+ // Express / Fastify / Node — the pure form
3095
3120
  app.post('/mcp', async (req, res) => {
3096
3121
  const r = await mcp.handle({ method: req.method, headers: req.headers, body: req.body });
3097
3122
  res.status(r.status).set(r.headers).send(r.body);
3098
3123
  });
3099
3124
  ```
3100
3125
 
3101
- Advertise it via the server card so agents find the endpoint:
3126
+ > **Copy-paste recipes for every framework** — Next.js (App + Pages Router), Express, Fastify, Hono, Bun, Deno, Cloudflare Workers, and React/SPA (WebMCP) — plus the per-framework mint route and a production checklist: [`docs/MCP-AND-AGENT-READINESS.md`](docs/MCP-AND-AGENT-READINESS.md#framework-recipes). Two adapters cover them all: `mcp.fetch(Request)` (web standard — App Router, Hono, Bun, Deno, edge) and `mcp.handle({method,headers,body})` (pure — Express, Fastify, Pages Router).
3127
+
3128
+ Advertise it via the server card so agents find the endpoint (the stack sets this automatically):
3102
3129
 
3103
3130
  ```ts
3104
3131
  export const agentConfig: AgentReadyConfig = {
@@ -3187,21 +3214,25 @@ The SDK accelerates; it never gates. Everything BuildBase-specific is optional:
3187
3214
 
3188
3215
  ### Auth in one line of plumbing
3189
3216
 
3190
- The OAuth2 app-bridge flow already mints your app's Bearer tokens (see [OAuth2 App Bridge](#oauth2-app-bridge)); `signClientJwt(payload, secret)` is the symmetric counterpart of `verifyClientJwt` for exactly that. The mint claims include `sessionId` — a per-user BuildBase session bound to the grant (fresh on every mint, including refresh). Embed it **encrypted** (JWTs are signed, not encrypted — the agent can read its own token) and keep it out of any storage, so it simply expires with the token; your MCP `auth.verify` decrypts it back into `McpAuthInfo.sessionId`:
3217
+ The OAuth2 app-bridge flow mints your app's Bearer tokens (see [OAuth2 App Bridge](#oauth2-app-bridge)). Use the presets and neither the mint nor the verify is hand-rolled — the per-user BuildBase session rides as an **encrypted `sid`** claim (JWTs are signed, not encrypted), never stored, expiring with the token:
3191
3218
 
3192
3219
  ```ts
3193
- mintToken: (user) => ({
3194
- token: signClientJwt(
3195
- { sub: user.id, scope: user.scope, aud: user.resource, sid: encrypt(user.sessionId) },
3196
- process.env.BUILDBASE_CLIENT_SECRET!
3197
- ),
3198
- expiresIn: 3600,
3199
- }),
3220
+ // mint (applicationTokenUrl) — HS256 with your secret, aud from the grant, encrypted sid
3221
+ import { mintAgentToken } from '@buildbase/sdk';
3222
+ mintToken: (claims) => mintAgentToken({ claims, secret: process.env.SYSTEM_SECRET! }),
3223
+
3224
+ // verify (MCP server) — HS256 + RFC 8707 audience + sid decrypt → McpAuthInfo
3225
+ import { buildbaseAuth } from '@buildbase/sdk/mcp';
3226
+ auth: buildbaseAuth({ secret: process.env.SYSTEM_SECRET!, resource: 'https://imejis.io/mcp' }),
3200
3227
  ```
3201
3228
 
3202
- Set `auth: false` to disable auth entirely (local development only). Test with the MCP Inspector: `npx @modelcontextprotocol/inspector --cli http://localhost:3000/api/mcp --transport http --header "Authorization: Bearer <token>" --method tools/list`.
3229
+ Bringing your own token format? `auth.verify(token, req) → McpAuthInfo` accepts any Bearer token; set `auth.resourceMetadataUrl` so 401s carry the RFC 9728 challenge. Set `auth: false` to disable auth (local dev only). Debug audience mismatches with `MCP_AUTH_DEBUG=1`. Test with the MCP Inspector: `npx @modelcontextprotocol/inspector --cli http://localhost:3000/api/mcp --transport http --header "Authorization: Bearer <token>" --method tools/list`.
3203
3230
 
3204
- **Exports:** `createMcpHandler`, `defineMcpTool`, `builtinMcpTools`, `selectBuiltinTools`, plus re-exported auth helpers (`signClientJwt`, `verifyClientJwt`, `extractBearerToken`, `bearerChallenge`, `AppBridgeError`). **Types:** `CreateMcpHandlerConfig`, `McpHandler`, `McpToolDefinition`, `McpToolContext`, `McpAuthInfo`, `McpHttpRequest`, `McpHttpResponse`, `McpToolAnnotations`, `McpBuildBaseClient`, `BuiltinMcpToolName`, `McpServerCard`, `ScopedActions`.
3231
+ **Exports:** `createAgentStack`, `createMcpHandler`, `defineMcpTool`, `builtinMcpTools`, `selectBuiltinTools`, `buildbaseAuth`, `mintAgentToken`, `createSessionRefCrypto`, plus re-exported auth helpers (`signClientJwt`, `verifyClientJwt`, `extractBearerToken`, `bearerChallenge`, `AppBridgeError`). **Types:** `AgentStack`, `AgentStackConfig`, `CreateMcpHandlerConfig`, `McpHandler`, `McpToolDefinition`, `McpToolContext`, `McpAuthInfo`, `McpHttpRequest`, `McpHttpResponse`, `McpToolAnnotations`, `McpBuildBaseClient`, `BuiltinMcpToolName`, `BuildBaseAuthOptions`, `MintAgentTokenOptions`, `SessionRefCrypto`, `McpServerCard`, `ScopedActions`.
3232
+
3233
+ ### Scopes & resources — restrict or open up
3234
+
3235
+ Scopes and resources are **app-owned** — the shared BuildBase authorization server stays scope/resource-agnostic. Scopes have three levers: **declare** a catalog (`scopes: [{ name, description }]` → `scopes_supported` in your own RFC 9728 protected-resource metadata), **gate per tool** (`requiredScopes` on a custom tool → hidden/refused unless the token carries them), and the **floor** (every tool runs under the user's BuildBase session, so the platform enforces the user's real permissions — an agent never exceeds the user). Resources have two: **declare** `protectedResources` (RFC 9728) and **bind** the token audience via `buildbaseAuth({ resource, requireAudience })` (RFC 8707) — the audience check at your resource is the whole gate. Built-ins are restricted with `builtinTools`, not scopes. Public clients (Claude Desktop, Cursor, Inspector) self-register with PKCE and no secret. Full model: [the guide](docs/MCP-AND-AGENT-READINESS.md#scopes--resources-restrict-or-open-up).
3205
3236
 
3206
3237
  ## OAuth2 App Bridge
3207
3238
 
@@ -3212,6 +3243,8 @@ BuildBase runs the full OAuth2 authorization flow (login, consent, code, PKCE, r
3212
3243
 
3213
3244
  Both requests carry `Authorization: Bearer <JWT>`, an HS256 JWT signed with your client secret. This toolkit verifies those requests (timing-safe, no `alg` confusion) and shapes the exact response body the platform expects, so you write only the part that's yours: minting/invalidating a token in your own format.
3214
3245
 
3246
+ > For the common case, `mintToken: (claims) => mintAgentToken({ claims, secret })` is the preset — it signs HS256 with your secret, binds `aud` to the granted RFC 8707 resource, and embeds the encrypted `sid`. The examples below show the hand-rolled equivalent, for apps that mint their own token format (e.g. one shared with their web sessions). See [the guide](docs/MCP-AND-AGENT-READINESS.md#auth-minting--verifying-the-token).
3247
+
3215
3248
  ```ts
3216
3249
  // applicationTokenUrl handler (Next.js Pages Router)
3217
3250
  import { handleAppTokenRequest } from '@buildbase/sdk';
@@ -3295,7 +3328,9 @@ return res.json({ id: claims.id, email: claims.email /* … */ });
3295
3328
 
3296
3329
  The one-call handlers cover the common path; the underlying pieces are exported for custom flows: `verifyAppTokenRequest` / `verifyAppRevokeRequest` (verify → claims), `verifyClientJwt`, `extractBearerToken`, and the response builders `appTokenSuccess` / `appTokenFailure`. Verification throws `AppBridgeError` (with a machine-readable `code` like `invalid_signature`, `token_expired`, `invalid_algorithm`). HMAC verification uses the SDK's dependency-free pure-JS HMAC-SHA256, so it behaves identically under ESM, CJS, a bundler, or bare Node.
3297
3330
 
3298
- **Types:** `AppTokenRequestClaims`, `AppRevokeRequestClaims`, `AppTokenResult`, `AppTokenResponseBody`, `HandlerResult`.
3331
+ **Presets** (also on the core entry): `mintAgentToken` (the whole mint — HS256 + `aud` + encrypted `sid`), `buildbaseAuth` (the whole verify), `createSessionRefCrypto` (the `sid` AES-256-GCM crypto). The platform relays the minted token but never sees your secret — it can neither forge nor decode your tokens.
3332
+
3333
+ **Exports:** `handleAppTokenRequest`, `handleAppRevokeRequest`, `verifyAppTokenRequest`, `verifyAppRevokeRequest`, `verifyClientJwt`, `signClientJwt`, `extractBearerToken`, `bearerChallenge`, `appTokenSuccess`, `appTokenFailure`, `mintAgentToken`, `buildbaseAuth`, `createSessionRefCrypto`, `AppBridgeError`. **Types:** `AppTokenRequestClaims`, `AppRevokeRequestClaims`, `AppTokenResult`, `AppTokenResponseBody`, `HandlerResult`, `VerifyClientJwtOptions`, `BuildBaseAuthOptions`, `MintAgentTokenOptions`, `SessionRefCrypto`.
3299
3334
 
3300
3335
  ## 🤝 Contributing
3301
3336