@businessdash/sdk 0.9.60 → 0.9.70

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