@businessdash/sdk 0.9.81 → 0.9.90

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