@businessdash/sdk 0.9.60 → 0.9.80

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.
Files changed (239) hide show
  1. package/README.md +991 -295
  2. package/assets/biab-sw.js +92 -0
  3. package/dist/account-core/index.d.ts +126 -0
  4. package/dist/account-core/index.d.ts.map +1 -0
  5. package/dist/account-core/index.js +191 -0
  6. package/dist/account-core/index.js.map +1 -0
  7. package/dist/account.d.ts +11 -0
  8. package/dist/account.d.ts.map +1 -0
  9. package/dist/account.js +11 -0
  10. package/dist/account.js.map +1 -0
  11. package/dist/auth-handler.d.ts +1 -1
  12. package/dist/auth-links.d.ts +33 -0
  13. package/dist/auth-links.d.ts.map +1 -0
  14. package/dist/auth-links.js +64 -0
  15. package/dist/auth-links.js.map +1 -0
  16. package/dist/blog-core/index.d.ts +112 -0
  17. package/dist/blog-core/index.d.ts.map +1 -0
  18. package/dist/blog-core/index.js +330 -0
  19. package/dist/blog-core/index.js.map +1 -0
  20. package/dist/blog.d.ts +11 -0
  21. package/dist/blog.d.ts.map +1 -0
  22. package/dist/blog.js +11 -0
  23. package/dist/blog.js.map +1 -0
  24. package/dist/chat-core/index.d.ts +97 -0
  25. package/dist/chat-core/index.d.ts.map +1 -0
  26. package/dist/chat-core/index.js +237 -0
  27. package/dist/chat-core/index.js.map +1 -0
  28. package/dist/chat.d.ts +11 -0
  29. package/dist/chat.d.ts.map +1 -0
  30. package/dist/chat.js +11 -0
  31. package/dist/chat.js.map +1 -0
  32. package/dist/checkout-core/index.d.ts +65 -0
  33. package/dist/checkout-core/index.d.ts.map +1 -0
  34. package/dist/checkout-core/index.js +159 -0
  35. package/dist/checkout-core/index.js.map +1 -0
  36. package/dist/checkout.d.ts +10 -0
  37. package/dist/checkout.d.ts.map +1 -0
  38. package/dist/checkout.js +10 -0
  39. package/dist/checkout.js.map +1 -0
  40. package/dist/cli.js +133 -25
  41. package/dist/cli.js.map +1 -1
  42. package/dist/client.d.ts +622 -1
  43. package/dist/client.d.ts.map +1 -1
  44. package/dist/client.js +888 -1
  45. package/dist/client.js.map +1 -1
  46. package/dist/consent-core/index.cjs +107 -0
  47. package/dist/consent-core/index.d.ts +103 -0
  48. package/dist/consent-core/index.d.ts.map +1 -0
  49. package/dist/consent-core/index.js +157 -0
  50. package/dist/consent-core/index.js.map +1 -0
  51. package/dist/contracts.d.ts +3526 -767
  52. package/dist/contracts.d.ts.map +1 -1
  53. package/dist/contracts.js +571 -4
  54. package/dist/contracts.js.map +1 -1
  55. package/dist/data-model-schema.d.ts +1 -1
  56. package/dist/data-model-schema.d.ts.map +1 -1
  57. package/dist/element.d.ts +30 -0
  58. package/dist/element.d.ts.map +1 -1
  59. package/dist/element.js +56 -0
  60. package/dist/element.js.map +1 -1
  61. package/dist/embed-protocol.d.ts +86 -0
  62. package/dist/embed-protocol.d.ts.map +1 -0
  63. package/dist/embed-protocol.js +60 -0
  64. package/dist/embed-protocol.js.map +1 -0
  65. package/dist/framework-support.d.ts +122 -0
  66. package/dist/framework-support.d.ts.map +1 -0
  67. package/dist/framework-support.js +323 -0
  68. package/dist/framework-support.js.map +1 -0
  69. package/dist/index.cjs +1355 -24
  70. package/dist/legal-core/index.cjs +115 -0
  71. package/dist/legal-core/index.d.ts +160 -0
  72. package/dist/legal-core/index.d.ts.map +1 -0
  73. package/dist/legal-core/index.js +220 -0
  74. package/dist/legal-core/index.js.map +1 -0
  75. package/dist/marketing-core/index.d.ts +66 -0
  76. package/dist/marketing-core/index.d.ts.map +1 -0
  77. package/dist/marketing-core/index.js +154 -0
  78. package/dist/marketing-core/index.js.map +1 -0
  79. package/dist/marketing.d.ts +7 -0
  80. package/dist/marketing.d.ts.map +1 -0
  81. package/dist/marketing.js +7 -0
  82. package/dist/marketing.js.map +1 -0
  83. package/dist/mcp-adapters/index.d.ts +135 -0
  84. package/dist/mcp-adapters/index.d.ts.map +1 -0
  85. package/dist/mcp-adapters/index.js +187 -0
  86. package/dist/mcp-adapters/index.js.map +1 -0
  87. package/dist/mcp-adapters.d.ts +7 -0
  88. package/dist/mcp-adapters.d.ts.map +1 -0
  89. package/dist/mcp-adapters.js +7 -0
  90. package/dist/mcp-adapters.js.map +1 -0
  91. package/dist/model3d-core/index.cjs +95 -0
  92. package/dist/model3d-core/index.d.ts +167 -0
  93. package/dist/model3d-core/index.d.ts.map +1 -0
  94. package/dist/model3d-core/index.js +158 -0
  95. package/dist/model3d-core/index.js.map +1 -0
  96. package/dist/notifications-core/index.cjs +315 -0
  97. package/dist/notifications-core/index.d.ts +229 -0
  98. package/dist/notifications-core/index.d.ts.map +1 -0
  99. package/dist/notifications-core/index.js +445 -0
  100. package/dist/notifications-core/index.js.map +1 -0
  101. package/dist/portal-core/index.d.ts +451 -0
  102. package/dist/portal-core/index.d.ts.map +1 -0
  103. package/dist/portal-core/index.js +819 -0
  104. package/dist/portal-core/index.js.map +1 -0
  105. package/dist/portal.d.ts +7 -0
  106. package/dist/portal.d.ts.map +1 -0
  107. package/dist/portal.js +7 -0
  108. package/dist/portal.js.map +1 -0
  109. package/dist/push-core/index.cjs +171 -0
  110. package/dist/push-core/index.d.ts +149 -0
  111. package/dist/push-core/index.d.ts.map +1 -0
  112. package/dist/push-core/index.js +231 -0
  113. package/dist/push-core/index.js.map +1 -0
  114. package/dist/react-marketing.d.ts +1 -17
  115. package/dist/react-marketing.d.ts.map +1 -1
  116. package/dist/react-marketing.js +4 -0
  117. package/dist/react-marketing.js.map +1 -1
  118. package/dist/react.d.ts +294 -101
  119. package/dist/react.d.ts.map +1 -1
  120. package/dist/react.js +302 -246
  121. package/dist/react.js.map +1 -1
  122. package/dist/seed-core/index.d.ts +354 -0
  123. package/dist/seed-core/index.d.ts.map +1 -0
  124. package/dist/seed-core/index.js +420 -0
  125. package/dist/seed-core/index.js.map +1 -0
  126. package/dist/seed.d.ts +7 -0
  127. package/dist/seed.d.ts.map +1 -0
  128. package/dist/seed.js +7 -0
  129. package/dist/seed.js.map +1 -0
  130. package/dist/seo-core/blog.d.ts +94 -0
  131. package/dist/seo-core/blog.d.ts.map +1 -0
  132. package/dist/seo-core/blog.js +149 -0
  133. package/dist/seo-core/blog.js.map +1 -0
  134. package/dist/seo-core/commerce.d.ts +64 -0
  135. package/dist/seo-core/commerce.d.ts.map +1 -0
  136. package/dist/seo-core/commerce.js +87 -0
  137. package/dist/seo-core/commerce.js.map +1 -0
  138. package/dist/seo-core/index.cjs +751 -0
  139. package/dist/seo-core/index.d.ts +226 -0
  140. package/dist/seo-core/index.d.ts.map +1 -0
  141. package/dist/seo-core/index.js +455 -0
  142. package/dist/seo-core/index.js.map +1 -0
  143. package/dist/seo-core/local.d.ts +181 -0
  144. package/dist/seo-core/local.d.ts.map +1 -0
  145. package/dist/seo-core/local.js +193 -0
  146. package/dist/seo-core/local.js.map +1 -0
  147. package/dist/sitemap-core/index.cjs +167 -0
  148. package/dist/sitemap-core/index.d.ts +155 -0
  149. package/dist/sitemap-core/index.d.ts.map +1 -0
  150. package/dist/sitemap-core/index.js +195 -0
  151. package/dist/sitemap-core/index.js.map +1 -0
  152. package/dist/social-core/index.d.ts +74 -0
  153. package/dist/social-core/index.d.ts.map +1 -0
  154. package/dist/social-core/index.js +72 -0
  155. package/dist/social-core/index.js.map +1 -0
  156. package/dist/social.d.ts +7 -0
  157. package/dist/social.d.ts.map +1 -0
  158. package/dist/social.js +7 -0
  159. package/dist/social.js.map +1 -0
  160. package/dist/solid-compat.d.ts +204 -0
  161. package/dist/solid-compat.d.ts.map +1 -0
  162. package/dist/solid-compat.js +221 -0
  163. package/dist/solid-compat.js.map +1 -0
  164. package/dist/store-core/index.d.ts +141 -0
  165. package/dist/store-core/index.d.ts.map +1 -0
  166. package/dist/store-core/index.js +277 -0
  167. package/dist/store-core/index.js.map +1 -0
  168. package/dist/store.d.ts +12 -0
  169. package/dist/store.d.ts.map +1 -0
  170. package/dist/store.js +12 -0
  171. package/dist/store.js.map +1 -0
  172. package/dist/vanilla.d.ts +111 -0
  173. package/dist/vanilla.d.ts.map +1 -1
  174. package/dist/vanilla.js +206 -0
  175. package/dist/vanilla.js.map +1 -1
  176. package/dist/verify-core/index.d.ts +98 -0
  177. package/dist/verify-core/index.d.ts.map +1 -0
  178. package/dist/verify-core/index.js +186 -0
  179. package/dist/verify-core/index.js.map +1 -0
  180. package/dist/verify.d.ts +8 -0
  181. package/dist/verify.d.ts.map +1 -0
  182. package/dist/verify.js +8 -0
  183. package/dist/verify.js.map +1 -0
  184. package/openapi.json +4516 -1311
  185. package/package.json +128 -11
  186. package/src/account-core/index.ts +309 -0
  187. package/src/account.ts +11 -0
  188. package/src/angular/headless.ts +519 -0
  189. package/src/angular/index.ts +3 -0
  190. package/src/auth-links.ts +76 -0
  191. package/src/blog-core/index.ts +423 -0
  192. package/src/blog.ts +11 -0
  193. package/src/chat-core/index.ts +335 -0
  194. package/src/chat.ts +11 -0
  195. package/src/checkout-core/index.ts +225 -0
  196. package/src/checkout.ts +10 -0
  197. package/src/cli.ts +184 -28
  198. package/src/client.ts +1097 -0
  199. package/src/consent-core/index.ts +213 -0
  200. package/src/contracts.ts +768 -11
  201. package/src/data-model-schema.ts +1 -1
  202. package/src/element.ts +102 -0
  203. package/src/embed-protocol.ts +100 -0
  204. package/src/framework-support.ts +440 -0
  205. package/src/legal-core/index.ts +276 -0
  206. package/src/marketing-core/index.ts +209 -0
  207. package/src/marketing.ts +7 -0
  208. package/src/mcp-adapters/index.ts +264 -0
  209. package/src/mcp-adapters.ts +7 -0
  210. package/src/model3d-core/index.ts +248 -0
  211. package/src/notifications-core/index.ts +650 -0
  212. package/src/portal-core/index.ts +1564 -0
  213. package/src/portal.ts +7 -0
  214. package/src/push-core/index.ts +335 -0
  215. package/src/qwik.tsx +602 -2
  216. package/src/react-marketing.tsx +5 -0
  217. package/src/react.tsx +434 -319
  218. package/src/seed-core/index.ts +735 -0
  219. package/src/seed.ts +7 -0
  220. package/src/seo-core/blog.ts +216 -0
  221. package/src/seo-core/commerce.ts +138 -0
  222. package/src/seo-core/index.ts +605 -0
  223. package/src/seo-core/local.ts +333 -0
  224. package/src/sitemap-core/index.ts +296 -0
  225. package/src/social-core/index.ts +103 -0
  226. package/src/social.ts +7 -0
  227. package/src/solid-compat.tsx +347 -0
  228. package/src/solid-surfaces.tsx +1311 -0
  229. package/src/solid.tsx +82 -60
  230. package/src/store-core/index.ts +426 -0
  231. package/src/store.ts +12 -0
  232. package/src/svelte/SocialLinks.svelte +84 -0
  233. package/src/svelte/headless.ts +435 -0
  234. package/src/svelte/index.ts +9 -0
  235. package/src/vanilla.ts +367 -0
  236. package/src/verify-core/index.ts +288 -0
  237. package/src/verify.ts +8 -0
  238. package/src/vue/headless.ts +527 -0
  239. package/src/vue/index.ts +3 -0
package/README.md CHANGED
@@ -516,12 +516,12 @@ per-crawler fetch counts, distinct pages read, vendor, intent
516
516
  | `@businessdash/sdk/collections` | Schema primitives — `collection()`, `createSchema()`, `bd`, `Enums()`, `SchemaBoundary` |
517
517
  | `@businessdash/sdk/forms` | Form layout + runtime — `layoutContainers`, `createFormSchemaOrchestrator()`, `createSingleResourceForm()`, controller, formula engine |
518
518
  | `@businessdash/sdk/static` | Static data — `Enums()`, enum types |
519
- | `@businessdash/sdk/react` | React bindings — form, auth, analytics, attribution |
520
- | `@businessdash/sdk/vue` | Vue binding — `<BiabForm>` |
521
- | `@businessdash/sdk/svelte` | Svelte binding — `<BiabForm>` |
522
- | `@businessdash/sdk/solid` | Solid binding — `<BiabForm>` |
519
+ | `@businessdash/sdk/react` | React bindings — **the full UI surface**: forms, chat, auth, storefront, cart, checkout, blog, customer portal, marketing pages, followers, social links, verification |
520
+ | `@businessdash/sdk/vue` | Vue binding — `<BiabForm>` + shared client |
521
+ | `@businessdash/sdk/svelte` | Svelte binding — `<BiabForm>` + shared client |
522
+ | `@businessdash/sdk/solid` | Solid binding — `<BiabForm>` + shared client |
523
523
  | `@businessdash/sdk/qwik` | Qwik binding — `<BiabForm>` |
524
- | `@businessdash/sdk/angular` | Angular binding — `<biab-form>` component |
524
+ | `@businessdash/sdk/angular` | Angular binding — `<biab-form>` component + shared client |
525
525
  | `@businessdash/sdk/element` | `<biab-form>` custom element — drop-in web component |
526
526
  | `@businessdash/sdk/vanilla` | Framework-free imperative DOM binding — `mountBiabForm()` |
527
527
  | `@businessdash/sdk/react-analytics` | `<BIABAnalytics />` — browser page-view tracker for React |
@@ -539,6 +539,71 @@ per-crawler fetch counts, distinct pages read, vendor, intent
539
539
  | `@businessdash/sdk/adapters/revalidate` | Framework-agnostic revalidation handler builder |
540
540
  | `@businessdash/sdk/biab-forms.css` | Default form stylesheet |
541
541
 
542
+ ### Framework parity — read this before picking a stack
543
+
544
+ Forms work identically on all eight bindings, and every binding renders them
545
+ **natively** — real components in your own DOM, styled by your own CSS.
546
+
547
+ Beyond forms, two questions matter: does the binding have the surface at all,
548
+ and does it render into your DOM or into an iframe?
549
+
550
+ | Capability | react | solid | vue | svelte | qwik | angular | vanilla | element |
551
+ |---|---|---|---|---|---|---|---|---|
552
+ | Forms | native | native | native | native | native | native | native | native |
553
+ | Shared client | native | native | native | native | — | native | — | native |
554
+ | AI chat | both | both | native | native | native | native | native | native |
555
+ | Storefront | both | both | native | native | native | native | native | native |
556
+ | Cart | both | both | native | native | native | native | native | native |
557
+ | Blog | both | both | native | native | native | native | native | native |
558
+ | Checkout | both | both | native | native | native | native | native | native |
559
+ | Customer portal | both | both | native | native | native | native | native | native |
560
+ | Customer auth | native | native | native | native | native | native | native | native |
561
+ | Followers | native | native | native | native | native | native | native | native |
562
+ | Marketing pages | native | native | native | native | native | native | native | native |
563
+ | Social links | native | native | native | native | native | native | native | native |
564
+ | Email verification | native | native | native | native | native | native | native | native |
565
+ | Phone verification | native | native | native | native | native | native | native | native |
566
+ | **Surfaces** | **14** | **14** | **14** | **14** | **13** | **14** | **13** | **14** |
567
+
568
+ `iframe` means a BusinessDash-hosted page in a sandbox. It works and it ships
569
+ fast, but it renders our markup, not yours.
570
+
571
+ Two things worth reading against the obvious summary:
572
+
573
+ **Every capability is available on every binding**, via ten shared cores
574
+ (`forms`, `store`, `chat`, `blog`, `checkout`, `account`, `portal`, `marketing`,
575
+ `verify`, `social`) — all as data you render yourself. React and Solid also
576
+ offer the hosted iframe widgets, hence `both`.
577
+
578
+ The only `—` cells are the shared-client primitive on `qwik` and `vanilla`, and
579
+ neither needs one: both take the client per form instead.
580
+
581
+ **The customer portal** is the largest surface — 34 methods. `sessionToken` is
582
+ required (without one it renders empty rather than signed-out); there are no
583
+ list endpoints for invoices, quotes, contracts or shipments, so `work` carries
584
+ the counts and you open each by id; and a submitted review is queued for staff
585
+ approval, so watch `reviewAwaitingModeration` rather than expecting it in
586
+ `reviews`.
587
+
588
+ **React remains ahead on `<ChatbotInline>`** — a rendered chat UI over the same
589
+ shared controller.
590
+
591
+ For anything a binding lacks, `BiabClient` from the root export is
592
+ framework-agnostic: `client.storefront()`, `client.cart()`,
593
+ `client.customerPortal()` and the rest work anywhere JavaScript runs. You write
594
+ the markup; the data layer is already there.
595
+
596
+ **React-compatible targets get the full surface**, because they render React:
597
+ Next.js (App Router included — the adapter ships its own `"use client"`
598
+ boundary), Astro via `@astrojs/react`, Remix / React Router 7, TanStack Start,
599
+ and **Preact** via the standard `preact/compat` alias all import
600
+ `@businessdash/sdk/react`. React Native uses `@businessdash/sdk/native`.
601
+
602
+ The matrix lives in `@businessdash/sdk/framework-support` as data, and release
603
+ gates assert that the code, the manifest and the published docs table all
604
+ agree — including the level, so an iframe can never be published as if it were
605
+ a native component.
606
+
542
607
  ## Releases
543
608
 
544
609
  > **Versioning convention (0.9.50+):** Significant feature groups bump the minor
@@ -550,7 +615,7 @@ per-crawler fetch counts, distinct pages read, vendor, intent
550
615
  >
551
616
  > **0.8.x = schema-driven marketing flow.** Major surface bump (still
552
617
  > alpha). New entrypoints: `defineSiteMarketingSchema()`,
553
- > `@businessdash/sdk/seo` for typed JSON-LD builders, and a `biab-dev` CLI.
618
+ > `@biab-dev/sdk/seo` for typed JSON-LD builders, and a `biab-dev` CLI.
554
619
  >
555
620
  > **0.9.x = full programmatic consumer surface.** The largest expansion
556
621
  > yet: native storefront / cart / checkout / coupons / subscriptions,
@@ -558,341 +623,972 @@ per-crawler fetch counts, distinct pages read, vendor, intent
558
623
  > `<SignIn/>` `<SignUp/>` `<SignOut/>` `useUser`), blog, a paginated
559
624
  > reviews wall, address autocomplete + shipping, programmatic local SEO
560
625
  > (`defineParallelPage()`), the revalidation webhook channel
561
- > (`@businessdash/sdk/next/revalidate` + `@businessdash/sdk/adapters/revalidate`),
562
- > privacy-conscious analytics (`@businessdash/sdk/react-analytics` +
563
- > `@businessdash/sdk/analytics-core`), and a three-state billing-lifecycle
626
+ > (`@biab-dev/sdk/next/revalidate` + `@biab-dev/sdk/adapters/revalidate`),
627
+ > privacy-conscious analytics (`@biab-dev/sdk/react-analytics` +
628
+ > `@biab-dev/sdk/analytics-core`), and a three-state billing-lifecycle
564
629
  > degradation contract. Detailed per-version notes below.
565
630
 
566
631
  The 3 most recent releases are below. **For the complete release
567
632
  history, see the [changelog](https://www.biab.app/docs/developer/changelog).**
568
633
 
569
- ### 0.9.53 — AI Distribution, AEO Visibility & the MCP Proxy
634
+ ### 0.9.80
570
635
 
571
- The release that makes a consumer-hosted site legible to answer engines. Three
572
- new entrypoints — `@businessdash/sdk/distribution`, `@businessdash/sdk/mcp`, and
573
- `@businessdash/sdk/analytics-server` — plus an AI-visibility read-back on the
574
- analytics client, a `product()` JSON-LD builder, and two formula functions.
636
+ #### Tracking consent `@businessdash/sdk/consent`
575
637
 
576
- #### `@businessdash/sdk/distribution` llms.txt + product feed on your domain
638
+ There was no consent mechanism anywhere: `initBiabAnalytics()` started
639
+ unconditionally and analytics, attribution and AEO all collected regardless.
577
640
 
578
- Orgs curate products/services (or custom entries) under **Dashboard
579
- Marketing AI Distribution**, and the platform keeps two public artifacts
580
- current per site: an OpenAI merchant-feed-shaped **product feed** (submittable
581
- to ChatGPT's product-feed program as-is) and an **`llms.txt`**. The llms.txt
582
- convention only works at the site's own root — an AI crawler asks for
583
- `https://yoursite.com/llms.txt` — while the source of truth lives on BusinessDash where
584
- the org curates it. The new entrypoint proxies the two together:
641
+ `loadConsent()` returns what a visitor currently allows, at THIS org, and
642
+ `recordConsent()` stores their answer. Gate analytics on it:
585
643
 
586
644
  ```ts
587
- // app/llms.txt/route.ts
588
- import { llmsTxtHandler } from '@businessdash/sdk/distribution'
589
-
590
- export const GET = llmsTxtHandler({
591
- siteId: process.env.BIAB_SITE_ID!,
592
- baseUrl: process.env.NEXT_PUBLIC_BIAB_APP_URL!, // e.g. https://www.biab.app
593
- })
645
+ const consent = await loadConsent({ client })
646
+ if (consent.allows('analytics')) initBiabAnalytics({ siteId, baseUrl, apiKey })
647
+ if (consent.mustAsk) showYourBanner(consent)
594
648
  ```
595
649
 
596
- - **`llmsTxtHandler(options)`** GET handler serving the curated llms.txt from
597
- your own domain root. Upstream 404 passes through untouched (feed disabled or
598
- entitlement lapsed), network failure returns a plain 503 it never throws
599
- into the framework. Responses carry a 5-minute `s-maxage` +
600
- `stale-while-revalidate`.
601
- - **`productFeedUrl(options)` / `llmsTxtUrl(options)`** — build the public BIAB
602
- feed URLs. The product feed needs no proxy — you submit its BusinessDash URL directly
603
- to merchant/feed programs; `productFeedUrl` builds it for display/submission.
604
- - Gated by the org's **AI Distribution** feature (`marketing.ai_distribution`);
605
- a lapsed entitlement surfaces as the pass-through 404.
650
+ `initBiabAnalytics` is deliberately NOT made to call this itself — a gate that
651
+ fires implicitly is one nobody can see in review, and what happens before
652
+ consent (render nothing, render a placeholder, collect essential-only) is the
653
+ site's decision, not ours.
606
654
 
607
- #### `@businessdash/sdk/mcp` the site's AI connector on ANY domain
655
+ **Consent is per-org and never travels.** A visitor who refuses at org 2 has
656
+ said nothing to org 1, and one who consents at org 1 has NOT consented to org 2
657
+ — that second half is the one with legal teeth. The visitor key lives in a
658
+ first-party cookie on the org's own domain, so two sites cannot see each
659
+ other's, and the isolation is the browser's rather than something we remember
660
+ to scope.
608
661
 
609
- The platform's host-resolved `/api/mcp` only exists on sites the platform
610
- serves. If you host your own app with the SDK, mount these two handlers and
611
- your domain gains the same connector surface — so the URL an org hands to
612
- Claude / ChatGPT / Gemini is their own site, exactly like the docs promise:
662
+ **The gate fails closed.** Any failure returns deny-everything: a gate that
663
+ fails open collects data nobody agreed to, and only one of those outcomes is an
664
+ incident.
613
665
 
614
- ```ts
615
- // app/api/mcp/route.ts
616
- import { mcpHandler } from '@businessdash/sdk/mcp'
617
- export const { POST, GET } = mcpHandler({
618
- siteId: process.env.BIAB_SITE_ID!,
619
- baseUrl: process.env.NEXT_PUBLIC_BIAB_APP_URL!, // https://www.biab.app
620
- })
666
+ Also ships `client.consent.get()` / `.record()` as the transport underneath,
667
+ and `browserOptOut()` for DNT and Global Privacy Control.
668
+
669
+ ### 0.9.70
670
+
671
+ #### 3D product models — `@businessdash/sdk/model3d`
672
+
673
+ Schema columns and a three.js viewer for `model3dUrl` / `model3dUsdzUrl` shipped
674
+ in migration 0162 and reached the BusinessDash-hosted storefront only. Nothing
675
+ was typed in the SDK, nothing was gated, and STEP was unsupported.
676
+
677
+ `planProductModel()` decides which of three surfaces applies and hands back the
678
+ facts. **No renderer ships** — three.js is ~600KB and `<model-viewer>` ~300KB,
679
+ and the choice of engine should be yours.
680
+
681
+ - **AR Quick Look** (`.usdz`) — an `<a rel="ar">` opens the model in the room on
682
+ iOS/iPadOS/visionOS with no JavaScript at all. Feature-detected via
683
+ `relList.supports("ar")` rather than user-agent sniffing.
684
+ - **Web viewer** (`.glb`/`.gltf`/`.obj`/`.stl`) — `neutralMaterial` flags the
685
+ geometry-only formats, which render as a black silhouette in a viewer that
686
+ lights them like a `.glb`.
687
+ - **CAD download** (`.step`/`.iges`) — deliberately not rendered. STEP needs a
688
+ geometry kernel, and the browser WASM builds are 10–30MB. It is also the wrong
689
+ shape for who asks: an engineer wants the file in their own CAD package.
690
+
691
+ Gated behind the new `ecommerce.product_3d` add-on (bundled at Scale). The gate
692
+ is PARTIAL — an unentitled org's products still return, minus four fields.
693
+ Failing the whole request would take a catalog offline over a lapsed $5.
694
+
695
+ The CAD URL never appears in a payload. `client.storefront.getCadDownload(id)`
696
+ issues it from a route that requires a `biab_cad` cookie, rate-limits 10/min per
697
+ IP, and re-checks the entitlement — a catalog of naked URLs is a machine shop's
698
+ design library published to anyone willing to write a loop.
699
+
700
+ #### Legal pages — `@businessdash/sdk/legal`
701
+
702
+ Privacy policy, terms and refund policy written once in the dashboard and served
703
+ on the org's own domain. The SDK never claims a literal path: it exposes a
704
+ resolver for a catch-all, and every file-based framework resolves a static
705
+ segment first — so your own `/privacy` shadows ours with no build conflict.
706
+
707
+ #### Sitemaps — `@businessdash/sdk/sitemap`
708
+
709
+ `buildSitemap()` merges your own routes, what the platform owns the shape of,
710
+ and platform content mapped onto paths you declare. Opt-in per content type:
711
+ declare nothing and nothing is emitted, which is right for a business without
712
+ that surface. Never emits the customer portal or other token-gated paths.
713
+
714
+ #### Browser push — `@businessdash/sdk/push-core` and `/notifications`
715
+
716
+ `enablePush()` handles the whole service-worker dance; `createNotificationFeed()`
717
+ gives one handler for every notification, deduplicated across push and polling,
718
+ with no toast UI of its own.
719
+
720
+ #### Scheduling and conference calls on the main client
721
+
722
+ Booking lived on `sdk.ts`'s separate scheduling resource. The OpenAPI generator
723
+ reads `client.ts`, so it never saw any of it: all seven scheduling routes sat in
724
+ the recorded spec gap, and no non-JS starter had scheduling at all.
725
+
726
+ `client.site(siteId).scheduling` now covers event types, slots, booking, and
727
+ reading/rescheduling/cancelling with the invitee's signed token — plus a new
728
+ **staff** path (`rescheduleBookingAsStaff` / `cancelBookingAsStaff`) so an org
729
+ can move a booking with its API key rather than a token from someone's email.
730
+
731
+ Both sides call the same service, which is what keeps them honest: the invitee
732
+ **and every host** are notified either way, the reminders queued against the old
733
+ time are cancelled either way, and the booking records which side changed it.
734
+ `actorUserId` is what lets a customer's history say "they rescheduled" rather
735
+ than leaving an unexplained change.
736
+
737
+ The staff route needs `scheduling:write`, mapped to the same `scheduling.manage`
738
+ permission the dashboard requires and deliberately **not publishable** — a
739
+ browser token able to cancel any booking by id would let anyone reading the page
740
+ source cancel every meeting the org has.
741
+
742
+ Two generator bugs surfaced while wiring this up, both the same class as the
743
+ nested-template one fixed earlier: it could not see paths built through a
744
+ `this.path()` helper, and it was letting query strings leak into path templates
745
+ (`bookings/{token}?type={type}` was being emitted as a path). Both fixed; the
746
+ corrupt-path gate stays green and the recorded spec gap dropped from 39 routes
747
+ to 33.
748
+
749
+ All six non-JS starters gained the full surface, including both paths — parity
750
+ is 19/19 each.
751
+
752
+
753
+ #### SEO reaches the page, on every framework — `@businessdash/sdk/seo-core`
754
+
755
+ The platform has always produced per-page SEO — every marketing page bundle
756
+ carries title, description, canonical, `noIndex`, Open Graph, Twitter card,
757
+ keywords, JSON-LD and hreflang. The SDK re-exported that as a TYPE and stopped
758
+ there, so every consumer hand-mapped eleven fields into their framework's
759
+ metadata shape, along with the fallbacks, the absolute-URL rules and the robots
760
+ string.
761
+
762
+ Eleven fields is enough that everyone does four. The two people skip are
763
+ `noIndex` and `canonical` — the two where being wrong costs something and says
764
+ nothing.
765
+
766
+ **Layout and page compose.** Precedence runs layout defaults → the platform's
767
+ page SEO → overrides in code, because a developer has context the CMS does not.
768
+ An omitted field inherits; an explicit `null` clears. JSON-LD accumulates
769
+ rather than replaces, since a layout's `Organization` node and a page's
770
+ `Product` node both belong in the document.
771
+
772
+ **Every framework, no framework imports.** `toNextMetadata`, `toRemixMeta`,
773
+ `toNuxtHead`, `toQwikDocumentHead`, `toTanStackHead`, `toHeadTags`,
774
+ `renderHeadTags` and `applyToDocument` each return the plain shape their
775
+ framework expects, so the module works in all of them and depends on none.
776
+
777
+ Two rules are enforced rather than left to the caller. A **relative canonical
778
+ is dropped** instead of emitted — crawlers resolve it against whatever URL they
779
+ fetched, and a wrong canonical consolidates ranking onto the wrong page while a
780
+ missing one is recoverable. And **`robots` is always emitted, both ways**,
781
+ because absence means "index" and relying on absence to express `noindex` is
782
+ how a hidden page gets published.
783
+
784
+ #### SEO for blogs and storefronts
785
+
786
+ `blogPostSeo` emits `BlogPosting` with dates, byline, tags and breadcrumbs, plus
787
+ `og:type: article` and the `article:*` meta that make a shared link render as a
788
+ dated card rather than a bare URL.
789
+
790
+ **Gated posts stay indexed and say so.** A subscriber-only post should be
791
+ findable — discoverability is how anyone subscribes — but serving the full
792
+ article to a crawler and a paywall to a reader is cloaking. So a gated post
793
+ carries schema.org's paywall markup (`isAccessibleForFree: false` plus a
794
+ `hasPart` naming the region), which declares the difference rather than hiding
795
+ it. Anything not `public` counts as gated, including unrecognised access
796
+ levels: failing the other way would declare open access for restricted content.
797
+
798
+ `productListingSeo` emits `CollectionPage` + `ItemList`, and canonicalises
799
+ filtered views to the unfiltered category — faceted URLs multiply into thousands
800
+ of near-identical pages that eat crawl budget and split ranking.
801
+ `transactionalPageSeo` keeps cart, checkout and order confirmations out of the
802
+ index; confirmations have been indexed with customer details in the query
803
+ string by more than one large retailer.
804
+
805
+ `productPageSeo` gained `aggregateRating` and `AggregateOffer`. The rating is
806
+ emitted **only** when `reviewCount > 0`, because an `AggregateRating` with zero
807
+ reviews is invalid and Google rejects the whole Product node for it — a product
808
+ with no reviews yet would lose its price and availability too.
809
+
810
+ Archive and listing pages past the first are `noindex`: the posts and products
811
+ are what should rank, and a thin page 7 competes with them.
812
+
813
+ #### Local SEO: service areas × services, and products
814
+
815
+ `servicesTimesAreas()` expands the Cartesian product into page plans, each with
816
+ its own canonical and a `Service` node scoped with `areaServed` to that place.
817
+ Without those two, near-identical pages are deduplicated away and the node is
818
+ indistinguishable from the generic services page.
819
+
820
+ `describe` is a callback rather than a template on purpose: pages differing only
821
+ by a swapped town name are doorway pages, which are penalised rather than
822
+ ranked. `include` skips combinations the org does not actually cover, so it does
823
+ not rank for work it has to turn down.
824
+
825
+ `productPageSeo()` emits `Product` + `Offer` + `BreadcrumbList`, money as
826
+ integer cents. It pairs with the AI Distribution product feed — the feed gets
827
+ the product into the engines' catalogues, this markup is what their crawlers
828
+ verify on the landing page.
829
+
830
+ All six non-JS starters gained the same surface, with the same rules. Swift's
831
+ `JSONValue` gained `Encodable` on the way: it could decode the org's JSON-LD
832
+ and had no way to render it back out.
833
+
834
+
835
+ #### Subscriptions in the customer portal — and the entitlement check that was a stub
836
+
837
+ `hasActiveSubscription()` returned `true`. For everyone. Every
838
+ `accessLevel: "subscribers"` gate called it, so subscriber-only content was
839
+ readable by anyone signed in.
840
+
841
+ That was survivable while nothing was gated, and it stopped being survivable
842
+ the moment the portal grew a surface that advertises "your subscriber content":
843
+ a page promising an entitlement check that does not perform one is worse than
844
+ no page. It now reads `user_subscriptions` for real. There were 0
845
+ subscriptions, 0 offerings and 0 subscriber-gated posts in production when this
846
+ landed, so switching it on changed nothing anyone could see — which is the
847
+ cheapest possible moment to fix an access-control default.
848
+
849
+ Three cases grant access, and two of them a `status === "active"` check gets
850
+ wrong in the direction that takes something from a customer who paid:
851
+
852
+ - **lifetime** has no period, so an expiry check must not be applied to it;
853
+ - **cancelled but paid through** keeps access until the period ends;
854
+ - `failed` (dunning) and `pending` (incomplete) do not grant — a payment that
855
+ has not succeeded has not bought anything.
856
+
857
+ The rule lives in `subscription-access-rules.ts` with no database import, so it
858
+ is unit-tested directly — the same split, for the same reason, as
859
+ `connect-subscription-mapping.ts`.
860
+
861
+ **The portal surface**: `getSubscription()` returns state plus the org's live
862
+ offerings (a portal reporting "not subscribed" and nothing else is a dead end),
863
+ `cancelSubscription()` / `resumeSubscription()`, and `getSubscriberContent()`.
864
+
865
+ Cancelling ends the RENEWAL, not the access: `cancel_at_period_end` only, with
866
+ `accessUntil` in the response. The customer paid for the period they are in,
867
+ and ending access on the click is the most common way a subscription flow feels
868
+ like a trap. The route deliberately writes no local status — the provider's
869
+ webhook owns it, and racing that would leave the row disagreeing with the
870
+ money — and the controller re-reads rather than guessing what the webhook will
871
+ write.
872
+
873
+ `getSubscriberContent()` answers "what am I actually getting for this?", which
874
+ is the question asked right before someone cancels. When `entitled` is false it
875
+ returns LOCKED previews rather than an empty list: titles and excerpts, never
876
+ bodies. An empty list would hide the offer at exactly the moment it is most
877
+ relevant.
878
+
879
+ **Found while building it**: `org_blog_posts.access_level` is a TEXT column
880
+ with two vocabularies written into it — `public|members|subscribers` by the
881
+ gate and the create route, `public|followers|paid` by the public reads. Nothing
882
+ reconciles or constrains them. Both subscriber spellings now gate and anything
883
+ unrecognised gates too, because an access check must fail closed. Converging
884
+ the column is a migration and a naming decision, not something to settle in a
885
+ read path.
886
+
887
+ All six non-JS starters gained the same four calls.
888
+
889
+
890
+ #### Notification settings, per company
891
+
892
+ A customer who buys from three businesses has three independent preference
893
+ matrices — they are stored per `(org, customer)` and always have been. What did
894
+ not exist was a way to reach more than one of them: `createPortalController`
895
+ built its client once, pinned to the API key's org, for its whole life. A
896
+ dashboard could LIST the customer's other companies and change nothing about
897
+ them, so muting marketing email meant muting it for whichever company the
898
+ portal happened to be pointed at.
899
+
900
+ `loadNotificationPreferencesFor(orgId)` and
901
+ `updateNotificationPreferencesFor(orgId, input)` close that, with
902
+ `notificationPreferencesByOrg` on the snapshot keyed by org id — separate from
903
+ this org's, so the current company renders without waiting on companies the
904
+ customer has not expanded. Pinned clients are memoised per org, because a
905
+ dashboard listing five companies would otherwise build one per render.
906
+
907
+ Writes stay sparse and merged, and the controller stores what the server
908
+ ANSWERED rather than what was sent: the server merges into the stored matrix,
909
+ so echoing the request would show the customer a matrix it never agreed to.
910
+
911
+ Every non-JS starter gained the same surface.
912
+
913
+ #### Non-JS starters reach every surface — and are now measured
914
+
915
+ Swift, Kotlin, Dart, PHP and Elixir consumers do not import this package; each
916
+ starter carries a hand-written client. Nothing that keeps the JS bindings
917
+ honest said anything about them, so they fell behind quietly and the only
918
+ record of how far was an estimate that turned out to be wrong in both
919
+ directions.
920
+
921
+ Measured, then closed:
922
+
923
+ | Starter | Before | After |
924
+ |---|---|---|
925
+ | Swift | 11/15 | **15/15** |
926
+ | Phoenix | 12/15 | **15/15** |
927
+ | Flutter | 11/15 | **15/15** |
928
+ | Laravel | 11/15 | **15/15** |
929
+ | Kotlin | 10/15 | **15/15** |
930
+ | Vapor | 9/15 | **15/15** |
931
+
932
+ The shared gap was social links and the two verification kinds — surfaces added
933
+ to the JS side and never carried across. Laravel also lacked the chatbot;
934
+ Kotlin, Flutter and Vapor lacked the customer portal; Vapor lacked customer
935
+ auth; Kotlin lacked the data model.
936
+
937
+ `non-js-parity.test.ts` measures this every run by matching **request paths**,
938
+ the one thing six languages have in common — a Swift `func reviews(_ id:)` and
939
+ an Elixir `product_reviews/2` share no convention, but both must contain
940
+ `storefront/products`. The baseline may only grow.
941
+
942
+ Two detector bugs were found and fixed while writing it, both the same shape:
943
+ social links have no endpoint at all (they are derived from the branding
944
+ bundle), and the two verification kinds share one endpoint separated by a
945
+ `kind` field. Both would have reported "missing" forever no matter what anyone
946
+ built. **A gate that cannot pass is worse than no gate** — it teaches people to
947
+ ignore it.
948
+
949
+ Full coverage means every surface is REACHABLE from every language. It does not
950
+ mean the ergonomics match, and the gate cannot tell you that.
951
+
952
+ #### The social-platform table is generated, not copied
953
+
954
+ 31 platforms — key, label, icon slug, URL prefix — emitted from
955
+ `src/socials.ts` into all six languages by `pnpm gen:socials`, with
956
+ `gen:socials:check` failing CI when they drift. Six hand-maintained copies
957
+ would separate the first time a platform was added, and the copy that drifted
958
+ would be whichever language nobody was using that week.
959
+
960
+ The resolver around the table stays hand-written per language, because that is
961
+ what a developer reads. Generate the data, hand-write the idiom — the same
962
+ split the whole non-JS plan uses.
963
+
964
+
965
+ #### Exchanges, "it never arrived", and verified-buyer product reviews
966
+
967
+ Three gaps in the post-purchase surface, each of which the customer could
968
+ experience but not report.
969
+
970
+ **Exchanges.** A return request could only ask for money back. Customers asked
971
+ for replacements in the free-text `reason`, where nothing could act on it — the
972
+ org read prose, then refunded and re-ordered by hand. Return requests now carry
973
+ `kind` (`refund` | `exchange`), plus the variant wanted and the shipment that
974
+ eventually carries it. `portal.submitExchangeRequest()` routes through the same
975
+ call as a return, because an exchange is a return request with a different ask
976
+ rather than a separate flow. `kind` defaults to `refund`, so existing callers
977
+ mean exactly what they meant. The staff notification now names the actual ask,
978
+ since staff reaching for a refund when the customer wanted a replacement is the
979
+ miscommunication the field exists to remove.
980
+
981
+ **"It never arrived."** `portal.reportNotReceived(shipmentId)` records the
982
+ claim — and deliberately does **not** touch the shipment's status.
983
+
984
+ The carrier scanned the parcel delivered; the customer says otherwise. Both are
985
+ facts, and the carrier's is the more valuable one right then: it is what the org
986
+ disputes with, and what decides who absorbs the loss. Overwriting it would
987
+ destroy that evidence, quietly redefine "delivered" as "delivered and
988
+ undisputed" in the org's own analytics, and lose the race anyway — `status` is
989
+ webhook-driven, so a customer-written value survives until the next carrier
990
+ event and then looks like the report vanished.
991
+
992
+ So the claim sits in its own columns and the result carries both sides, with
993
+ `contested: true` when they disagree. A static gate now fails the build if that
994
+ route ever assigns a carrier-owned field, because this is a mistake that arrives
995
+ by refactor rather than by decision.
996
+
997
+ **Product reviews.** The portal could review the ORG but not a PRODUCT, despite
998
+ `org_product_reviews` already existing and the storefront already reading it.
999
+ `submitProductReview` / `listProductReviews` close that, and require a matching
1000
+ order: the portal is standing inside the customer's purchase history, which the
1001
+ public storefront is not. Verified-buyer reviews are the ones worth surfacing,
1002
+ and it stops a competitor one-starring a catalogue they never bought. Approval
1003
+ comes from the org's own moderation policy — the same call the storefront makes,
1004
+ so an org that holds reviews does not find portal reviews bypassing it.
1005
+
1006
+
1007
+ #### The customer portal can list what a customer has, and talk to staff
1008
+
1009
+ Invoices, quotes, contracts and shipments were **detail-by-id only**. Work
1010
+ carried the counts, so a customer could be told they had three unpaid invoices
1011
+ and had no call that would name them. "Show me everything I owe" was
1012
+ unanswerable without faking a list from repeated detail fetches, which is a
1013
+ different bug wearing a list's clothes.
1014
+
1015
+ Four list routes close it: `loadInvoices({ unpaid })`,
1016
+ `loadQuotes({ status })`, `loadContracts({ status })` and
1017
+ `loadShipments({ active })`. `unpaid` filters on the computed **balance**, not
1018
+ the status string, because a partially-paid invoice still owes money whatever
1019
+ it is called. Shipments list **stored** carrier status; `trackShipment(id)`
1020
+ still spends a live carrier request, because that is one parcel the customer
1021
+ deliberately opened rather than every parcel in their history on every page
1022
+ load.
1023
+
1024
+ Staff chat lands in the portal too — `loadMessages`, `postMessage`,
1025
+ `markMessagesRead`, with `unreadMessageCount` on the snapshot. Both directions
1026
+ write the same table the CRM timeline reads, so a customer's question appears
1027
+ beside everything else known about them instead of in a parallel inbox nobody
1028
+ opens.
1029
+
1030
+ Return requests read back on the order, `resolutionNote` included: when staff
1031
+ deny a return, their reason *is* the answer, and withholding it turns a decision
1032
+ into silence.
1033
+
1034
+ #### Products and blog posts can be seeded — the last two things you had to hand-enter
1035
+
1036
+ Seeding could push a schema, its records, static collections and coupons. It
1037
+ could not create a **product** or a **blog post**, because no package-API route
1038
+ existed for either — the dashboard created them over tRPC. So an org could build
1039
+ its entire site from code and still had to hand-enter its catalogue, which made
1040
+ "seed your site" a half-answer precisely where the content mattered most.
1041
+
1042
+ Two routes now exist, and `@businessdash/sdk/seed` exposes them as
1043
+ `seedProducts` and `seedBlogPosts`:
1044
+
1045
+ - `POST storefront/products` — scope `storefront:write`. Takes the whole tree,
1046
+ product plus variants plus cross-variants, and writes it in **one
1047
+ transaction**. The dashboard does this in four dependent round trips; making
1048
+ a seed script replay that means threading ids by hand with no way to recover
1049
+ halfway through. Either the product exists complete, or it does not exist.
1050
+ - `POST blog/posts` — scope `blog:write`, a new scope that is deliberately
1051
+ **not publishable**: authoring a post is an operator action, never something
1052
+ a browser token should be able to do.
1053
+
1054
+ Both create **unpublished** by default (`isLive` / `publishNow` are opt-in).
1055
+ A seed that silently pushed a half-configured catalogue to a live storefront —
1056
+ or drafts to every follower's inbox — is worse than one that needs a second,
1057
+ deliberate step.
1058
+
1059
+ Products land in your own tables, **not in Stripe**. Pushing the catalogue to
1060
+ your Stripe account stays a separate explicit call, because it mints objects in
1061
+ an external account you are billed against and that should never be a side
1062
+ effect of a seed script.
1063
+
1064
+ #### `businessdash seed` — one command for the whole seed
621
1065
 
622
- // app/.well-known/mcp.json/route.ts
623
- import { mcpManifestHandler } from '@businessdash/sdk/mcp'
624
- export const GET = mcpManifestHandler({
625
- siteId: process.env.BIAB_SITE_ID!,
626
- baseUrl: process.env.NEXT_PUBLIC_BIAB_APP_URL!,
627
- })
1066
+ ```bash
1067
+ npx tsx node_modules/@businessdash/sdk/dist/cli.js seed [--dry-run] [--yes]
628
1068
  ```
629
1069
 
630
- - **`mcpHandler(options)`** — `POST` proxies JSON-RPC to the platform's public
631
- per-site connector; `GET` mirrors the spec's 405. An unreachable upstream
632
- answers with a proper JSON-RPC `-32603` error and a 502.
633
- - **`mcpManifestHandler(options)`**serves `.well-known/mcp.json` with the
634
- endpoint URL rewritten to YOUR domain (the requesting origin).
635
- - **Thin by design.** The body is forwarded verbatim and the platform still
636
- enforces the org's MCP opt-in and per-tool write gates — a proxy can't widen
637
- anything.
1070
+ Reads `businessdash.seed.ts` and runs schema records → collections →
1071
+ products posts coupons in dependency order. It calls the same `runSeed`
1072
+ the programmatic API exposes rather than reimplementing it, so CI and a Nuxt
1073
+ module behave identically including the refusal on a destructive schema plan,
1074
+ which `--yes` overrides.
1075
+
1076
+ #### Fixed: `seedTargetFromEnv()` read a variable nobody sets
1077
+
1078
+ It looked for `BIAB_PACKAGE_API_KEY`. The CLI reads `BIAB_API_KEY`, and so does
1079
+ every starter's `.env.example` — all fifteen. A project whose CLI worked fine
1080
+ would fail to seed from a build step, and fail naming a variable the consumer
1081
+ had never seen. It now reads `BIAB_API_KEY` first and falls back to
1082
+ `NEXT_PUBLIC_BIAB_PACKAGE_API_BASE_URL` for the base URL, exactly as the CLI
1083
+ does. A test pins the two together.
1084
+
1085
+ #### The MCP connector mounts on every framework
1086
+
1087
+ `mcpHandler` returns Web-standard `(Request) => Promise<Response>`, which is
1088
+ right for Next, Astro, Remix and TanStack Start and wrong for everything else:
1089
+ SvelteKit hands you `{ request }`, Nitro an H3 event, Qwik City a `RequestEvent`
1090
+ you answer by *calling* `send()`, and Express Node's `req`/`res`. Four of eight
1091
+ frameworks had to work that out themselves, for a surface whose whole point is
1092
+ an org handing Claude or ChatGPT a URL on their own domain.
1093
+
1094
+ `@businessdash/sdk/mcp-adapters` ships that bridging: `sveltekitMcpHandler`
1095
+ (aliased `astroMcpHandler`), `nitroMcpHandler`, `qwikMcpHandler` and
1096
+ `expressMcpHandler`, each with a manifest counterpart. Every one is a
1097
+ shape-change over the same handler — none reimplements the proxy, the error
1098
+ mapping or the manifest caching, so a fix lands everywhere at once. On Node the
1099
+ manifest origin is rebuilt from `Host` and `x-forwarded-proto`, because
1100
+ advertising the wrong domain is the one failure this surface cannot tolerate.
1101
+
1102
+ #### `@businessdash/sdk/react` now carries its own `"use client"` boundary
1103
+
1104
+ React Server Components frameworks treat a module without the directive as
1105
+ server code. `react.tsx` holds every stateful component in the SDK and did not
1106
+ have it, so importing `<BiabForm>` into a Next App Router page threw *"you're
1107
+ importing a component that needs useState"*. The only workaround was a
1108
+ hand-written `"use client"` wrapper per component — the Next starter in this
1109
+ repo carries sixteen of them.
1110
+
1111
+ The directive is now on `react.tsx` and `react-marketing.tsx`, so components
1112
+ import directly into a server-rendered page. Every export in those files is
1113
+ browser-only anyway: the components hold state, and the four programmatic
1114
+ helpers (`signIn`, `signUp`, `signOut`, `requestPasswordReset`) each return
1115
+ early on `typeof window === "undefined"` because they navigate.
1116
+
1117
+ Existing wrappers keep working — this removes the need for new ones, it does
1118
+ not invalidate old ones. Bundlers that don't implement the directive treat it
1119
+ as an inert string expression.
1120
+
1121
+ #### Preact is supported, and verified rather than assumed
1122
+
1123
+ `@businessdash/sdk/react` runs unmodified under Preact via the standard
1124
+ `preact/compat` alias, which means Preact gets the **full fourteen-capability
1125
+ surface** — not the forms-only subset every non-React binding has — for the
1126
+ cost of a bundler alias rather than a port.
1127
+
1128
+ Two properties make it work, and both are now gated. The binding imports nine
1129
+ runtime symbols from `react` (`createContext`, `Fragment`, `useCallback`,
1130
+ `useContext`, `useEffect`, `useMemo`, `useRef`, `useState`,
1131
+ `useSyncExternalStore`), all present in `preact/compat`; and it never imports
1132
+ `react-dom`, so nothing pulls in React's reconciler.
1133
+
1134
+ `test/preact-compat.test.ts` bundles the real source twice through esbuild —
1135
+ once against React, once with `react` aliased to `preact/compat` — renders the
1136
+ same trees on both and asserts identical markup, covering the pure-render path,
1137
+ the context path and the `useSyncExternalStore` subscription path. The only
1138
+ tolerated difference is inline-style serialisation (`margin:0` vs
1139
+ `margin:0px`), which computes identically.
1140
+
1141
+ `preact` and `preact-render-to-string` are devDependencies; nothing new ships.
1142
+
1143
+ #### The customer portal, natively, on every framework
1144
+
1145
+ `@businessdash/sdk/portal` — `createPortalController` over the portal's 34
1146
+ methods: work, jobs, quotes, contracts, invoices, orders, downloads, shipping,
1147
+ reviews, referrals, profile and notification preferences. Until now the only
1148
+ way to render any of it was `<Dashboard>`, a hosted page in an iframe, so a
1149
+ business could not put their own customer portal on their own domain.
1150
+
1151
+ Four properties of the surface shape the controller, all from the audit in
1152
+ `Resources/BIAB-Customer-Portal-SDK-Audit.md`:
1153
+
1154
+ - **`sessionToken` is required.** Every route is scoped to the signed-in
1155
+ customer, and without one the calls are unauthenticated — which fails quietly
1156
+ enough to look like an empty portal rather than a signed-out one. The
1157
+ controller takes the token up front and calls `withSession` itself.
1158
+ - **There are no list endpoints** for invoices, quotes, contracts or shipments.
1159
+ `work` carries the counts and each opens by id. The controller does NOT fake
1160
+ a list from repeated detail fetches; the gap stays visible where someone can
1161
+ decide to close it in the API.
1162
+ - **Nothing can be deleted.** There is no delete action anywhere, because there
1163
+ is none in the API — and a test asserts the controller never grows one.
1164
+ - **A submitted review is queued for moderation.** It is deliberately not added
1165
+ to `reviews`; `reviewAwaitingModeration` is set instead, so a portal can say
1166
+ "awaiting review" rather than implying it is live.
1167
+
1168
+ Supporting detail never blanks the thing the customer asked for: a job renders
1169
+ even when its activity, ETA and comments all fail, and an order renders when it
1170
+ has no digital downloads (which most orders don't).
1171
+
1172
+ #### Social links on every framework
1173
+
1174
+ The last gap, and the only one that was never about logic — every binding
1175
+ already had `resolveSocialProfiles`. What was missing was markup, which cannot
1176
+ be shared the way a controller can.
1177
+
1178
+ `@businessdash/sdk/social` shares everything up to the markup: the resolved
1179
+ rows, the icon URL and the handful of layout styles the list needs. Each
1180
+ binding writes the six elements around it — a render function for Vue (rather
1181
+ than an SFC, which would put it back outside the typechecker), a standalone
1182
+ component for Angular, an SFC for Svelte, JSX for React, Solid and Qwik, an
1183
+ imperative `mountSocialLinks` for plain DOM, and `<biab-social-links>` for the
1184
+ web-component entry.
1185
+
1186
+ React and Solid were refactored onto it, so the CDN path and colour convention
1187
+ now live in one place rather than three.
1188
+
1189
+ #### Marketing pages and verification on every framework
1190
+
1191
+ Two final cores. Twelve of the fourteen capabilities are now native on all
1192
+ eight bindings, and Solid joins React at the full fourteen.
1193
+
1194
+ **`@businessdash/sdk/marketing`** — `createMarketingController` holds the page
1195
+ index and loads one page at a time, because a site can have many marketing
1196
+ pages and a visitor reads one. `prefetch()` warms a page you expect next; its
1197
+ failures are swallowed, since a wrong guess must never surface as an error on
1198
+ the page actually being read.
1199
+
1200
+ **`@businessdash/sdk/verify`** — `createVerifyController` is the email/phone
1201
+ state machine: request a token, confirm it, handle it being wrong. One
1202
+ controller serves both kinds; `kind` selects which.
1203
+
1204
+ Two details the React components had buried and that are now explicit:
1205
+
1206
+ - **`flow` is on the snapshot.** The server decides per request whether to send
1207
+ a one-time code or an emailed link. A binding that assumes a code field would
1208
+ leave every link-flow visitor staring at a box waiting for a code that is
1209
+ never coming.
1210
+ - **A rejected code is retryable, not fatal.** The confirm endpoint answers
1211
+ `{ ok: true }` or rejects, so a rejection is the ordinary wrong-code path: the
1212
+ step stays `awaiting-code` and the visitor can try again.
1213
+
1214
+ The resend cooldown lives in the controller rather than the binding. Left to
1215
+ eight bindings, one of them gets it wrong and a visitor can spam themselves
1216
+ with texts.
1217
+
1218
+ #### Checkout, customer auth and followers on every framework
1219
+
1220
+ Two more cores and bindings for all eight adapters, taking the count of
1221
+ capabilities every binding has natively from six to nine.
1222
+
1223
+ **`@businessdash/sdk/checkout`** — `createCheckoutController` mints a Stripe
1224
+ session and resolves the outcome. `start()` deliberately does NOT navigate; it
1225
+ returns the session and leaves `redirect()` as a separate call, because the
1226
+ previous React component set `window.location.href` itself, which is fine in a
1227
+ browser and wrong in a native app, an SSR render or a test.
1228
+
1229
+ `resolve(sessionId)` reads the payment status from the server. A `session_id`
1230
+ in the return URL proves the customer came back, not that they paid — that
1231
+ distinction is the point of the method and has its own test.
1232
+
1233
+ **`@businessdash/sdk/account`** — `createUserController` (the signed-in
1234
+ customer, via the auth handler's `/me`, never an API key) and
1235
+ `createFollowersController` (subscribe/unsubscribe plus the local
1236
+ already-subscribed hint, which is a convenience for hiding a footer form and
1237
+ explicitly not authority — `me()` is).
1238
+
1239
+ Sign-in / sign-up / sign-out are navigations rather than requests, so they ship
1240
+ as `signIn` / `signUp` / `signOut` plus `signInHref` / `signUpHref` /
1241
+ `signOutHref` for bindings that render their own links. All of them no-op
1242
+ outside a browser rather than throwing.
1243
+
1244
+ React and Solid were REFACTORED onto these rather than left alone, so all eight
1245
+ bindings share one implementation. That removed 104 lines of hand-rolled fetch
1246
+ and local-storage handling from the Solid binding alone.
1247
+
1248
+ #### Native blog on every framework — nothing is iframe-only any more
1249
+
1250
+ `@businessdash/sdk/blog` — `createBlogController` owns the post list, paging,
1251
+ category filtering, the active post, comments and likes. Bindings on all eight
1252
+ adapters: React/Vue/Qwik `useBlog`, Solid/Svelte/Angular/vanilla/element
1253
+ `createBlog`.
1254
+
1255
+ Blog was the last surface with no native path anywhere, React included — the
1256
+ worst place to lose control of markup, since the content is what readers and
1257
+ search engines came for and an iframe hides it.
1258
+
1259
+ Two behaviours worth calling out, both tested:
1260
+
1261
+ - **Posting a comment re-reads the thread instead of inserting locally.** A
1262
+ comment may be held for moderation, and splicing a local copy in would show
1263
+ its author an approved-looking comment nobody else can see.
1264
+ - **A likes outage never blanks the article.** Like state is fetched after the
1265
+ post and its failure is swallowed, because a decoration must not take down
1266
+ the thing it decorates.
638
1267
 
639
- #### `@businessdash/sdk/analytics-server` first-party AEO visibility
1268
+ #### Every framework binding is now typechecked
640
1269
 
641
- AI crawlers (GPTBot, ClaudeBot, PerplexityBot, …) fetch your HTML without
642
- executing JavaScript, so `<BIABAnalytics />` never sees them. Your server does:
1270
+ `@builder.io/qwik` is a devDependency and `tsconfig.qwik.json` checks the Qwik
1271
+ binding, which closes the last hole: Solid, Qwik, Vue, Svelte and Angular were
1272
+ all excluded from tsc and therefore never verified. `pnpm typecheck` runs four
1273
+ configs.
643
1274
 
644
- ```ts
645
- // middleware.ts
646
- import { isAiCrawlerUserAgent, reportAiCrawlerHit } from "@businessdash/sdk/analytics-server";
1275
+ Qwik's first run found two real pre-existing errors, both
1276
+ `exactOptionalPropertyTypes` violations where an optional JSX attribute was
1277
+ passed as `undefined` rather than omitted.
647
1278
 
648
- export function middleware(request: Request) {
649
- const ua = request.headers.get("user-agent");
650
- if (isAiCrawlerUserAgent(ua)) {
651
- // Fire-and-forget — never block or fail the crawler's request.
652
- void reportAiCrawlerHit({
653
- siteId: process.env.BIAB_SITE_ID!,
654
- baseUrl: process.env.NEXT_PUBLIC_BIAB_API_URL!,
655
- apiKey: process.env.NEXT_PUBLIC_BIAB_PUBLISHABLE_KEY!,
656
- userAgent: ua,
657
- path: new URL(request.url).pathname,
658
- });
659
- }
660
- }
661
- ```
1279
+ #### Native storefront, cart and chat on EVERY framework
662
1280
 
663
- - **`isAiCrawlerUserAgent(ua)`** cheap prefilter mirroring the platform's
664
- AI-crawler classifier (GPTBot, OAI-SearchBot, ClaudeBot/Claude-User/
665
- Claude-SearchBot, PerplexityBot, GrokBot/xAI-Crawler, Google-CloudVertexBot,
666
- GoogleOther, Applebot-Extended, Bytespider, CCBot, Amazonbot, MistralAI-User,
667
- DuckAssistBot, YouBot, AI2Bot, …). The platform's list is authoritative — a
668
- lagging local list only under-reports, never records bad data.
669
- - **`reportAiCrawlerHit(options)`** — reports one crawler fetch with the
670
- publishable key. Resolves quietly on every outcome — analytics must never
671
- break page serving. Abuse posture: the ingest endpoint keeps a forwarded
672
- user-agent ONLY when it classifies as a known AI crawler, so this path can
673
- record crawler visibility but can never inflate human visitor counts.
674
- - **Read it back — `analytics.aiVisibility()`.** Which crawlers actually
675
- fetched your pages: fetch counts, distinct pages read, vendor, intent
676
- (`training` / `search` / `user` — corpus building vs. answer-engine indexing
677
- vs. a live assistant reading on a person's behalf), and when each last came:
678
-
679
- ```ts
680
- const ai = await biab.site(siteId).analytics.aiVisibility({ days: 30 });
681
- if (ai.available) {
682
- for (const c of ai.crawlers)
683
- console.log(`${c.name}: ${c.fetches} fetches over ${c.distinctPages} pages`);
684
- }
685
- ```
1281
+ Two new framework-agnostic cores, and a binding for each of the eight adapters.
1282
+ Storefront, cart and chat are no longer iframe-only anywhere.
686
1283
 
687
- Ordered most-fetches-first; defaults to a 30-day window. Branch on
688
- `.available` same plan gating as `pageViews()`. Contracts export the
689
- `aiVisibility*` schemas and `AiCrawlerVisibility` types.
1284
+ **`@businessdash/sdk/store`** `createStoreController` owns products,
1285
+ pagination, category filtering, the product detail, the cart, quantities and
1286
+ coupons. Prices cross the boundary as integer minor units and are never
1287
+ divided, rounded or formatted by the controller.
690
1288
 
691
- #### `product()` JSON-LD builder
1289
+ **`@businessdash/sdk/chat`** `createChatController` owns the transcript,
1290
+ pending state, UI actions, availability and the human-escalation call. Lifted
1291
+ out of React's `useChatbot` rather than rewritten, so React's behaviour is
1292
+ preserved and now backs every other binding.
692
1293
 
693
- `@businessdash/sdk/seo` adds a schema.org **Product** (+ nested Offer) builder —
694
- what shopping surfaces and answer engines read off a product page:
1294
+ Both follow the `createFormController` contract exactly: `snapshot()`,
1295
+ `subscribe()` (which does NOT replay), `destroy()`, and actions.
695
1296
 
696
- ```ts
697
- import { product } from "@businessdash/sdk/seo";
1297
+ Per-framework bindings, all thin views over those cores:
698
1298
 
699
- const jsonLd = product({
700
- url: "https://acme.co/store/widget",
701
- name: "Widget",
702
- brand: "Acme",
703
- offer: { priceCents: 12900, availability: "InStock" },
704
- });
705
- ```
1299
+ | Framework | Storefront | Chat |
1300
+ |---|---|---|
1301
+ | React | `useStorefront` | `useChatbot` (now core-backed) |
1302
+ | Solid | `createStorefront` | `createChat` |
1303
+ | Vue | `useStorefront` | `useChat` |
1304
+ | Svelte | `createStorefront` | `createChat` |
1305
+ | Qwik | `useStorefront` | `useChat` |
1306
+ | Angular | `createStorefront` | `createChat` |
1307
+ | Vanilla / element | `createStorefront` | `createChat` |
1308
+
1309
+ The vanilla binding is the controllers themselves — there is no reactivity
1310
+ system to bridge — which makes it the reference the other seven wrap.
1311
+
1312
+ Qwik's controllers are wrapped in `noSerialize` and created inside
1313
+ `useVisibleTask$`. That is not stylistic: a controller holds a client, a
1314
+ subscriber set and timers, and Qwik would otherwise try to serialise it into
1315
+ the HTML on pause and throw.
1316
+
1317
+ #### The parity gate now checks HOW a capability is delivered
1318
+
1319
+ `capabilities` in `framework-support.ts` records `native`, `embed` or `both`
1320
+ per capability, and the gate derives the same levels from the source by reading
1321
+ implementations rather than export names. Recorded as booleans, an iframe
1322
+ wrapper looked identical to real components.
1323
+
1324
+ It also asserts each binding REACHES the core behind every capability it claims
1325
+ — level-aware, so the iframe path requires `embed-protocol` and the native path
1326
+ requires `store-core` / `chat-core` — and that the postMessage origin check
1327
+ exists in exactly one file. A second, subtly weaker copy of that check would be
1328
+ a security bug affecting one framework's users only.
1329
+
1330
+ #### Vue, Svelte and Angular headless bindings are typechecked
1331
+
1332
+ Those bindings are excluded from the main tsconfig because of SFC and decorator
1333
+ syntax. Their new headless modules are plain `.ts`, so `tsconfig.headless.json`
1334
+ checks them properly; `pnpm typecheck` now runs the main, Solid and headless
1335
+ configs. It caught a real generic-inference bug on the first run: the
1336
+ snapshot type was resolving to `unknown`, which would have stripped types from
1337
+ every Vue and Angular consumer.
1338
+
1339
+ #### Solid gains most of the product surface
1340
+
1341
+ `@businessdash/sdk/solid` went from forms-only to eleven of fourteen surfaces:
1342
+ storefront, cart, checkout, blog, the customer portal, the chat widget, tenant
1343
+ auth, followers and social links, alongside the forms binding it already had.
1344
+
1345
+ Most of that was cheap for a reason worth knowing: `Storefront`, `Cart`,
1346
+ `Checkout`, `Blog`, `Chatbot` and `Dashboard` are BusinessDash-hosted pages in a
1347
+ sandboxed iframe, in React exactly as in Solid. Two primitives —
1348
+ `createEmbedSession` and an internal `EmbedIframe` — carry all six, and each
1349
+ surface is a dozen lines on top of them.
1350
+
1351
+ New Solid exports: `Storefront`, `Cart`, `Checkout`, `CheckoutResult`, `Blog`,
1352
+ `Chatbot`, `Dashboard`, `DashboardProvider`, `useDashboardSession`, `SignIn`,
1353
+ `SignUp`, `SignOut`, `signIn`, `signUp`, `signOut`, `createUser`,
1354
+ `createFollowers`, `SocialLinks`, `createEmbedSession`.
1355
+
1356
+ Not ported: marketing pages, and the email/phone verification flows. The
1357
+ headless chat pair (`useChatbot` + `<ChatbotInline>`) also remains React-only —
1358
+ Solid gets the iframe widget, not the build-your-own-UI surface.
1359
+
1360
+ #### The Solid binding is typechecked for the first time
1361
+
1362
+ Source-shipped bindings are excluded from the main `tsconfig`, because tsc here
1363
+ is configured for React's JSX dialect. Excluded also meant never checked.
1364
+ `tsconfig.solid.json` now typechecks it under Solid's own pragma, and `pnpm
1365
+ typecheck` runs both — so it is a release gate.
1366
+
1367
+ It found two real errors on its first run, both in the pre-existing forms
1368
+ binding and both the same Solid hazard: calling an accessor twice inside a
1369
+ ternary, so the guard narrows the first read and the second is still
1370
+ possibly-undefined. One of them could have passed an undefined client into
1371
+ `createFormController`.
1372
+
1373
+ #### Shared cores extracted, so bindings cannot fork
1374
+
1375
+ `embed-protocol.ts` and `auth-links.ts` now own the postMessage protocol and
1376
+ origin check, the session-refresh schedule, the auth-handler URL contract and
1377
+ the follower storage format. React was refactored onto them rather than the
1378
+ Solid binding copying them across.
1379
+
1380
+ The origin check is the reason this matters: it decides whether a message from
1381
+ an arbitrary window may drive a cart or a checkout. A second, subtly weaker copy
1382
+ would be a security bug affecting one framework's users only, so a release gate
1383
+ asserts it exists in exactly one file.
1384
+
1385
+ #### The parity matrix now records HOW, not just whether
1386
+
1387
+ `capabilities` changed from a list to a map of capability to level: `native`
1388
+ (components in your DOM), `embed` (a hosted page in an iframe) or `both`.
1389
+
1390
+ The distinction is not cosmetic. Recorded as booleans, Solid's iframe chat
1391
+ widget looked equal to React's, which additionally ships ~1,200 lines of
1392
+ headless chat UI. It also surfaces something that reads the other way: storefront,
1393
+ cart and blog are `embed` on **every** binding including React, so on those
1394
+ surfaces the non-React bindings were never behind.
1395
+
1396
+ The release gate derives the level from the source — it reads implementations,
1397
+ not export names — and asserts the manifest and the published docs table agree.
1398
+
1399
+ #### Framework capability matrix is now published data
1400
+
1401
+ `@businessdash/sdk/framework-support` gained a `capabilities` field per adapter
1402
+ plus `CAPABILITY_LABELS`, `ALL_CAPABILITIES` and `REACT_TARGETS`. This makes an
1403
+ uncomfortable fact discoverable instead of implicit: **`react` exposes all 14
1404
+ capability areas and every other binding exposes forms**, plus in most cases a
1405
+ shared client. Chat, customer auth, storefront, cart, checkout, blog, the
1406
+ customer portal, marketing pages, followers, social links and the verification
1407
+ flows are React-only.
706
1408
 
707
- `priceCents` is integer cents, formatted to schema.org's decimal string here.
708
- Pair it with the AI Distribution product feed: the feed gets your product INTO
709
- the engines' catalogs; this markup is what their crawlers verify on the landing
710
- page itself.
1409
+ `REACT_TARGETS` documents that Next, Astro, Remix / React Router 7 and TanStack
1410
+ Start are not lesser targets they render React, import
1411
+ `@businessdash/sdk/react`, and get the full surface. React Native uses
1412
+ `@businessdash/sdk/native`.
711
1413
 
712
- #### Formula library: `SUBSTITUTE` + `SLUGIFY`
1414
+ A release gate now asserts the code, the manifest and the published docs table
1415
+ all agree, so the framework page cannot advertise a capability an adapter
1416
+ doesn't export.
713
1417
 
714
- Two additions to `FORMULA_FUNCTIONS` (the engine behind the Math/calculation
715
- form element and Business Functions, via `@businessdash/sdk/forms`):
1418
+ #### Contract fixes found by auditing the SDK against the platform
1419
+
1420
+ Four places where the SDK's schemas disagreed with what the platform actually
1421
+ sends. All are fixes to existing behaviour, not new surface.
716
1422
 
717
- - **`SUBSTITUTE(text, find, replace, [find2, replace2, …])`** multi-pair text
718
- replacement applied in a **single pass**, so a replacement's output is never
719
- itself re-replaced (pairs stay independent of their order; longest match wins
720
- at each position).
721
- - **`SLUGIFY(text)`** the platform's canonical strict slug: lowercase, accents
722
- stripped, non-alphanumerics collapsed to single hyphens.
723
- `SLUGIFY("Solar Panel Install (2026)")` `"solar-panel-install-2026"`.
1423
+ **Form definitions were losing most of their render contract.**
1424
+ `biab.chatbot.getForm()` / `getChatForm()` stripped `settings`, `orgIcon`,
1425
+ `actions` and `businessFunctions` the server sent all four, the schema
1426
+ declared none of them. Anyone building a form UI on the typed client got a
1427
+ bare field list with no animation, no multi-step or progress configuration, no
1428
+ exit confirmation, no branded header, and no way to compute `calculation`
1429
+ fields. All four are now declared, and the schema passes unknown fields
1430
+ through so the next server addition reaches you instead of vanishing.
724
1431
 
725
- #### Platform-side in this release window
1432
+ **Dry-run submissions threw.** A validate-only submit legitimately returns
1433
+ `submissionId: null`, but the schema required a string, so every test submit
1434
+ raised a parse error. It is nullable now, `dryRun` is returned alongside it,
1435
+ and the submit input accepts `dryRun`, `source`, `referrer` and `metadata` —
1436
+ all of which the route already supported and the typed client could not send.
726
1437
 
727
- Customer Loyalty (points + tiers) and a Customer Portal expansion (including a
728
- staff preview-as-customer mode) shipped on the dashboard/portal side — no new
729
- SDK exports yet. The org-grade OAuth MCP connector (the assistant acting as org
730
- STAFF, with a consent screen and per-tool write gates) lives on the platform
731
- host; the SDK's `mcp` entrypoint above fronts the public site-visitor
732
- connector.
1438
+ **Gallery items were losing their description** through the marketing bundle.
733
1439
 
734
- ### 0.9.52 Single-Resource Forms, Typed Join Tables & a Careers Surface
1440
+ Docs: **Developer By feature Framework Support**.
735
1441
 
736
- The schema pipeline learns to generate atomic Create/Edit/Delete forms,
737
- many-to-many joins carry typed payloads, public job postings get a client, two
738
- new form elements land, and a batch of parity fixes makes the dashboard and an
739
- SDK-rendered form finally agree on conditional logic.
1442
+ #### Coupons from the SDK, live in your Stripe account
740
1443
 
741
- #### `createSingleResourceForm()` Create / Edit / Delete instead of a wizard
1444
+ `biab.coupons.create()` makes a discount code and mints it in **your own**
1445
+ Stripe account in the same call — a Coupon plus a Promotion Code, so the same
1446
+ string works on your storefront and on Stripe-hosted Checkout.
742
1447
 
743
1448
  ```ts
744
- import { createSingleResourceForm } from "@businessdash/sdk/forms";
1449
+ const { coupon, stripeSynced } = await biab.coupons.create({
1450
+ code: 'WELCOME10',
1451
+ discountType: 'percent',
1452
+ value: 10,
1453
+ })
1454
+ ```
1455
+
1456
+ Creation and the Stripe sync are one operation deliberately. A coupon that
1457
+ exists locally but not in Stripe is the failure worth designing out: your
1458
+ storefront accepts the code and Stripe rejects it, stranding the customer at
1459
+ payment. If the sync fails the coupon is still created and `stripeSynced` comes
1460
+ back `false` with the reason, rather than pretending it is live.
1461
+
1462
+ `biab.coupons.list()` returns every coupon with its redemption count and Stripe
1463
+ ids. Both need a **secret** key holding the new `storefront:write` scope —
1464
+ `validate()` is unchanged and still works with a publishable token.
1465
+
1466
+ Buy-X-get-Y and bundle-gift coupons stay dashboard-only: Stripe has no native
1467
+ BXGY, so they run through our own cart math and need offer rows the API
1468
+ doesn't collect.
1469
+
1470
+ Docs: **Developer → By feature → Store → Coupons**.
1471
+
1472
+ #### Framework version support is now declared, not implied
1473
+
1474
+ The SDK ships form bindings for eight targets, and until now said nothing
1475
+ about which versions of those frameworks it was built for — `package.json`
1476
+ declared peer ranges for React only. A developer on a brand-new framework
1477
+ major got no signal either way.
745
1478
 
746
- const { create, edit, delete: remove, dependencies } =
747
- createSingleResourceForm(object);
1479
+ Every adapter now declares its range as an optional peer dependency, so your
1480
+ package manager warns on a mismatch instead of failing later with a confusing
1481
+ build error. The same data is importable:
1482
+
1483
+ ```ts
1484
+ import { FRAMEWORK_SUPPORT, supportFor } from '@businessdash/sdk/framework-support'
748
1485
  ```
749
1486
 
750
- - Generates the **three atomic, single-record forms** for one collection
751
- Create, Edit and Delete instead of one big multi-step wizard. Edit and
752
- Delete lead with a record picker ("which row are we acting on?").
753
- - **Relation fields become live collection pickers** automatically, single or
754
- multi according to the relation's own cardinality, so a many-to-many is
755
- filled in inline and the join table never surfaces as a concept.
756
- - **Delete states its impact honestly** — read from the relations that actually
757
- point AT the collection and their declared `onDelete`, rather than asserting
758
- a generic "respects referential integrity".
759
- - **Lineage-safe.** `create` keeps the base `data-model:<uid>` sourceKey the
760
- one-form-per-collection generator has always used, so promoting a model that
761
- previously produced a single form updates that draft rather than orphaning it
762
- and creating a duplicate; `edit`/`delete` take suffixed keys off the same
763
- stem. Types `SingleResourceForms` / `SingleResourceFormOptions` are exported
764
- from `@businessdash/sdk` and `@businessdash/sdk/forms`.
765
-
766
- #### Typed join tables — `belongsToMany({ fields })` end-to-end
767
-
768
- - A join's columns — declared with the same `bd` field builders as a
769
- collection's now compile into typed **link-attribute definitions**
770
- (`LinkAttributeDefinition`, mirroring the platform's link-attributes
771
- contract), the typed payload each many-to-many link row may carry.
772
- - A link is an EDGE, not a record: only a small set of value kinds makes sense
773
- on one. Anything richer maps to TEXT rather than silently pretending to be
774
- supported — richer shapes are asking for a real collection.
775
-
776
- #### `client.hiring` — careers pages on your own domain
777
-
778
- The org's open job postings and their categories, so a consumer can render a
779
- careers page in their own design system:
1487
+ Each entry also names the framework APIs its binding depends on, which is how
1488
+ we assess whether a framework's major actually breaks us a diff against a
1489
+ list rather than a reread of the adapter.
1490
+
1491
+ **When a framework ships a breaking major, we add a second adapter for it
1492
+ rather than replacing the first.** Both ship, the docs show both, and code on
1493
+ either major keeps working.
1494
+
1495
+ **Solid 2.0 (August 2026)** reworked a substantial part of the framework API.
1496
+ `@businessdash/sdk/solid` is written against the **1.x** dialect and is not yet
1497
+ verified on 2.x; a `v2` dialect is planned alongside it. On Solid 2.0 today,
1498
+ `@businessdash/sdk/element` or `@businessdash/sdk/vanilla` give you the same
1499
+ form behaviour with no framework dependency.
1500
+
1501
+ Docs: **Developer By feature → Framework Support**.
1502
+
1503
+
1504
+ #### Push your catalog to your own Stripe account
1505
+
1506
+ `biab.storefront.syncToStripe()` takes the products you manage in
1507
+ BusinessDash and creates them in **your own** Stripe account — each product as
1508
+ a Stripe Product, each variant combination as a Price. It is the same operation
1509
+ as the dashboard's "Sync to Stripe" button, now available to your code, so you
1510
+ can run it on your own schedule, in a deploy step, or immediately after
1511
+ creating a product.
780
1512
 
781
1513
  ```ts
782
- const { items } = await client.hiring.list({ limit: 20 });
783
- const posting = await client.hiring.get("senior-installer");
784
- if (posting.applicationFormId) {
785
- const form = await client.forms.schema(posting.applicationFormId);
786
- }
1514
+ const result = await biab.storefront.syncToStripe()
1515
+ console.log(result.synced) // [{ productId, stripeProductId, combinationsSynced }]
1516
+ console.log(result.remaining) // products still awaiting a sync
787
1517
  ```
788
1518
 
789
- - **`list({ limit, offset, category })`** one page of open postings,
790
- newest-first, offset-paged via `nextOffset`. Only postings that are
791
- published, active, have seats left, and are under their application cap are
792
- returned — a careers page never advertises a role that can no longer be
793
- applied to.
794
- - **`get(slug)`** rejects with a 404 for drafts, closed, archived, filled, or
795
- capped postings; treat that as "this role is gone", not a transient failure.
796
- - **`categories()`** job categories with a count of open postings in each,
797
- enough for filter chips or grouped sections without fetching every posting.
798
- - **Read-only by construction.** Internal hiring state (openings filled,
799
- application caps, default permissions/roles, interview questions) never
800
- crosses this boundary. To take applications, hand the posting's
801
- `applicationFormId` to the forms surface this client grants no submit path
802
- of its own. Contracts export `jobPostingSchema`, `jobCategorySchema`, and
803
- friends.
804
-
805
- #### New form elements: `icon_select` and `image_input`
806
-
807
- - **`icon_select`**pick an icon by name from a searchable grid. The value is
808
- the icon's kebab-case name; `iconConfig.allowedIcons` curates the choices (a
809
- curated list skips the network entirely), otherwise the picker lazily fetches
810
- the platform's icon-name index. Names render as
811
- `<img src="…/api/icons/<name>.svg">` the SDK deliberately bundles no icon
812
- set (~1,600 icons, and three of the bindings can't import a React package),
813
- so the platform stays the single source of truth for what a name means.
814
- - **`image_input`** one image as a single URL string, from an upload OR a
815
- pasted URL (`imageInputConfig.sources: "both" | "upload" | "url"`).
816
- - **Display variants** `display` extends beyond `toggle`/`checkbox` to
817
- `"dropdown"` and `"combobox"`. Display-only: never affects the submitted
818
- value or its validation. Dropdowns self-upgrade to a searchable input past 10
819
- options, or when the builder chose the ComboBox card
820
- (`SEARCHABLE_THRESHOLD` / `shouldSearchOptions`).
821
- - **Shared element helpers** exported from `@businessdash/sdk/forms`:
822
- `BIAB_ICON_BASE`, `iconUrl`, `ICON_INDEX_URL`, `fetchIconNames`,
823
- `filterIconNames`, `SEARCHABLE_THRESHOLD`, `shouldSearchOptions`. The new
824
- elements render in the React, Vue, Svelte and Angular bindings.
825
-
826
- #### Conditional-logic parity the dashboard and your site now agree
827
-
828
- - **`match: "any"` is honored.** The clause combinator was missing from the
829
- SDK's rule type, so a rule the builder saved as "ANY of" silently evaluated
830
- as "ALL of" once it rendered through the SDK — the dashboard and a consumer's
831
- site disagreed about the same form. Fixed across every binding.
832
- - **`matchField` on collection-picker rules.** Collection pickers store record
833
- IDs a rule can now compare a column of the chosen record instead
834
- (`"__label__"` = the displayed label); unresolved falls back to the raw
835
- value.
836
- - **Conditional PAGES.** `input_group` now gates on its `conditionRule` (at the
837
- root of a consecutive form a group IS a page, so a rule there makes the whole
838
- page conditional). A page whose rule is currently false is DROPPED from the
839
- step partition "Next" skips straight past it and the progress indicator
840
- loses its entry in React, Svelte, Vue, Angular, Solid, Qwik and vanilla
841
- alike. Its required fields no longer block Next/Submit, and stale values
842
- behind a hidden group no longer decide an `or_condition` branch. New
843
- `isNodeVisible` export.
844
-
845
- #### CLI: `wipe-database`
846
-
847
- - **`biab-dev wipe-database --collections a,b`** — developer shortcut that
848
- wipes records AND removes the named collections from the schema: the
849
- backup-first data-wipe flow plus a pruned schema-draft push. The schema
850
- change still requires dashboard promotion.
851
- - **`--all`** calls the data-model **reset** endpoint, which atomically
852
- soft-deletes all metadata, clears records, and removes schema slots. The
853
- server saves and verifies the backup before resetting any data.
854
- - Accepts `--dry-run`, and `--yes` to collapse to a single confirmation
855
- (unlike `wipe-collections`, which stays interactive-only by design).
856
-
857
- #### Fixes
858
-
859
- - **Node-ESM import specifiers.** Internal extensionless imports in the
860
- validation and schema modules gained explicit `.js` specifiers
861
- (`./enums.js`, `./email-domains.js`, `./units.js`, `./us-states.js`) — no
862
- more resolution failures for Node ESM / `NodeNext` consumers.
863
- - **`createFormSchemaOrchestrator`** throws a focused error when `boundaries`
864
- isn't an array, instead of failing obscurely mid-walk.
865
-
866
- ### 0.9.51 — Pipeline Completion
867
-
868
- The 0.9.50 notes below were written at 0.9.51 and describe the unified
869
- Schema/Validation/Form pipeline **as completed in this release** — several
870
- documented pieces landed between the two npm artifacts. If you installed the
871
- 0.9.50 artifact, upgrading gets you:
872
-
873
- - **`biab-dev wipe-collections`** — the backup-first record wipe (two-stage
874
- confirmation, verified Media backup, state-bound execution) documented under
875
- 0.9.50.
876
- - **Generated-form schema sync** — `sync-data-model` accepts the full
877
- orchestrator result, offers the interactive candidate multi-select, and
878
- supports `--forms`, `--forms slug-a,slug-b`, and `--no-forms` for CI.
879
- - **The compile-time `Infer*` family** — `InferFieldValue`,
880
- `InferFieldsValues`, `InferCollectionValues`, `InferFormValues`,
881
- `InferSchemaValues` (+ the type-inference compile tests behind them).
882
- - **Multi-location business details** — `companyAddressSchema` /
883
- `companyLocationSchema`, `BusinessAddress` / `BusinessLocation`,
884
- `localBusinessLocations()` + `biabSchemas.localBusinessLocations()`, and the
885
- extended `localBusiness()` builder.
886
- - **Deterministic fluent identities** — collection, field, injected
887
- primary-key, junction, and generated-form IDs converge across CLI
888
- evaluations; camelCase junction fields and distinct `source…Id` /
889
- `target…Id` self-relation fields.
890
- - **Correct published artifacts** — root declarations generated from the
891
- TypeScript build (replacing the handwritten ambient declaration) and the
892
- `dist/index.cjs` runtime file the `require` export map had been promising.
893
- - **Legacy docs archive** — the 0.9.35 README/CHANGELOG moved to
894
- `docs-legacy/0.9.35/`, and the package README was rebuilt around the 0.9.50
895
- pipeline.
1519
+ Pass `productId` to sync one product; omit it to sync everything not yet
1520
+ pushed, in bounded batches — check `remaining` and call again until it reaches
1521
+ zero. Large catalogs are paged deliberately rather than risking a timeout
1522
+ halfway through.
1523
+
1524
+ Re-running is safe. An existing Stripe Product is updated rather than
1525
+ duplicated, and a new Price is minted only when the amount actually changed
1526
+ (Stripe Prices are immutable on amount). A product that fails comes back in
1527
+ `failed` with its reason instead of abandoning the rest of the batch.
1528
+
1529
+ Requires a **secret** key holding the new `storefront:write` scope — never a
1530
+ publishable token, because this spends against Stripe on your behalf — plus
1531
+ the `ecommerce.storefront` entitlement and a connected Stripe account. The
1532
+ endpoint is rate-limited more tightly than the rest of the package API for the
1533
+ same reason.
1534
+
1535
+ Docs: **Developer By feature Store → Sync to Stripe**.
1536
+
1537
+ ### 0.9.60Seed Export, a GraphQL schema for your data model, and the complete artifact
1538
+
1539
+ #### The package is now `@businessdash/sdk`
1540
+
1541
+ This release publishes under the new **`@businessdash/sdk`** name (formerly
1542
+ `@biab-dev/sdk`), matching the platform's move to BusinessDash. Nothing about
1543
+ the API surface changes: same exports, same subpaths, same contracts. The
1544
+ `biab-dev` CLI name keeps working, with `businessdash` added as an alias, and
1545
+ existing `biab.config.ts` / `biab.data-model.config.ts` files are picked up
1546
+ forever (`businessdash.*` variants are now preferred for new projects).
1547
+ `@biab-dev/sdk` on npm stays installable at ≤0.9.53 with a deprecation notice
1548
+ pointing here.
1549
+
1550
+ #### Seed Export — your org's custom database, on your machine
1551
+
1552
+ `npx biab-dev export-data-model` writes a complete, versionable snapshot of the
1553
+ org's custom data model — every object, every field, and (non-sensitive)
1554
+ records to the developer's disk. Two jobs: a real backup you control before a
1555
+ big migration, and ground truth an AI agent can read while building, so it
1556
+ knows what exists without touching production. `--schema-only` for structure
1557
+ without contents; pair with the "Download seed" surface in the dashboard.
1558
+
1559
+ #### `@businessdash/sdk/graphql` a GraphQL SDL for the same model
1560
+
1561
+ `npx biab-dev export-graphql-schema` generates GraphQL SDL from the org's data
1562
+ model (`--include-standard` folds in the platform's standard objects), and the
1563
+ new `./graphql` entrypoint carries the supporting types. Schema-first for now
1564
+ the SDL is the contract your tooling, codegen, and agents can consume while the
1565
+ executable query layer lands.
1566
+
1567
+ **If you installed 0.9.53 from npm, this is the release you actually wanted.**
1568
+ The npm artifact published as 0.9.53 predated the tree it claimed to be: its
1569
+ `exports` map lacked `./distribution`, `./mcp`, `./native`, and
1570
+ `./analytics-server`, and its shipped `src/` was missing files so a fresh
1571
+ install broke every template importing the AEO/MCP entrypoints under strict
1572
+ resolvers (vite 7.3.6+, vite 8, rolldown, turbopack, tsc). This release ships
1573
+ the full surface the 0.9.53 notes below describe.
1574
+
1575
+ Compilation fixes for source-consuming frameworks (the `./angular`, `./qwik`,
1576
+ `./svelte`, `./solid`, and `./vue` subpaths resolve to `src/` by design, so SDK
1577
+ source must compile under consumer tsconfigs):
1578
+
1579
+ - Every index-signature property access rewritten to bracket form (140 sites) —
1580
+ SDK source now compiles clean under `noPropertyAccessFromIndexSignature`
1581
+ (Angular's default strict template config).
1582
+ - Qwik `<textarea>` used the React-style `autoComplete` attribute; qwik 1.20's
1583
+ JSX types require `autocomplete`.
1584
+ - Qwik `ServerCompatQRL` now names the payload-only handler shapes explicitly
1585
+ `server$((payload) => …)` from qwik-city ^1.20 assigns again (QRL's invariant
1586
+ type parameter hides ordinary parameter-narrowing compatibility).
1587
+ - **Packaging:** the tarball now ships the complete `src/` tree. The previous
1588
+ `files` allowlist shipped `src/sdk.ts` without the modules it imports, which
1589
+ broke every source-consuming framework (`Could not resolve './client.js'`).
1590
+ A pack-closure CI guard now fails any release whose shipped sources reference
1591
+ files missing from the tarball.
896
1592
 
897
1593
  ## Legacy Docs
898
1594