@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.
- package/README.md +1012 -186
- package/assets/biab-sw.js +92 -0
- package/dist/account-core/index.d.ts +126 -0
- package/dist/account-core/index.d.ts.map +1 -0
- package/dist/account-core/index.js +191 -0
- package/dist/account-core/index.js.map +1 -0
- package/dist/account.d.ts +11 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +11 -0
- package/dist/account.js.map +1 -0
- package/dist/auth-handler.d.ts +1 -1
- package/dist/auth-links.d.ts +33 -0
- package/dist/auth-links.d.ts.map +1 -0
- package/dist/auth-links.js +64 -0
- package/dist/auth-links.js.map +1 -0
- package/dist/blog-core/index.d.ts +112 -0
- package/dist/blog-core/index.d.ts.map +1 -0
- package/dist/blog-core/index.js +330 -0
- package/dist/blog-core/index.js.map +1 -0
- package/dist/blog.d.ts +11 -0
- package/dist/blog.d.ts.map +1 -0
- package/dist/blog.js +11 -0
- package/dist/blog.js.map +1 -0
- package/dist/chat-core/index.d.ts +97 -0
- package/dist/chat-core/index.d.ts.map +1 -0
- package/dist/chat-core/index.js +237 -0
- package/dist/chat-core/index.js.map +1 -0
- package/dist/chat.d.ts +11 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +11 -0
- package/dist/chat.js.map +1 -0
- package/dist/checkout-core/index.d.ts +65 -0
- package/dist/checkout-core/index.d.ts.map +1 -0
- package/dist/checkout-core/index.js +159 -0
- package/dist/checkout-core/index.js.map +1 -0
- package/dist/checkout.d.ts +10 -0
- package/dist/checkout.d.ts.map +1 -0
- package/dist/checkout.js +10 -0
- package/dist/checkout.js.map +1 -0
- package/dist/cli.js +133 -25
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +567 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +807 -1
- package/dist/client.js.map +1 -1
- package/dist/contracts.d.ts +3309 -761
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +541 -4
- package/dist/contracts.js.map +1 -1
- package/dist/data-model-schema.d.ts +1 -1
- package/dist/data-model-schema.d.ts.map +1 -1
- package/dist/element.d.ts +30 -0
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +56 -0
- package/dist/element.js.map +1 -1
- package/dist/embed-protocol.d.ts +86 -0
- package/dist/embed-protocol.d.ts.map +1 -0
- package/dist/embed-protocol.js +60 -0
- package/dist/embed-protocol.js.map +1 -0
- package/dist/framework-support.d.ts +122 -0
- package/dist/framework-support.d.ts.map +1 -0
- package/dist/framework-support.js +323 -0
- package/dist/framework-support.js.map +1 -0
- package/dist/index.cjs +1244 -5
- package/dist/legal-core/index.cjs +115 -0
- package/dist/legal-core/index.d.ts +160 -0
- package/dist/legal-core/index.d.ts.map +1 -0
- package/dist/legal-core/index.js +220 -0
- package/dist/legal-core/index.js.map +1 -0
- package/dist/marketing-core/index.d.ts +66 -0
- package/dist/marketing-core/index.d.ts.map +1 -0
- package/dist/marketing-core/index.js +154 -0
- package/dist/marketing-core/index.js.map +1 -0
- package/dist/marketing.d.ts +7 -0
- package/dist/marketing.d.ts.map +1 -0
- package/dist/marketing.js +7 -0
- package/dist/marketing.js.map +1 -0
- package/dist/mcp-adapters/index.d.ts +135 -0
- package/dist/mcp-adapters/index.d.ts.map +1 -0
- package/dist/mcp-adapters/index.js +187 -0
- package/dist/mcp-adapters/index.js.map +1 -0
- package/dist/mcp-adapters.d.ts +7 -0
- package/dist/mcp-adapters.d.ts.map +1 -0
- package/dist/mcp-adapters.js +7 -0
- package/dist/mcp-adapters.js.map +1 -0
- package/dist/model3d-core/index.cjs +95 -0
- package/dist/model3d-core/index.d.ts +167 -0
- package/dist/model3d-core/index.d.ts.map +1 -0
- package/dist/model3d-core/index.js +158 -0
- package/dist/model3d-core/index.js.map +1 -0
- package/dist/notifications-core/index.cjs +315 -0
- package/dist/notifications-core/index.d.ts +229 -0
- package/dist/notifications-core/index.d.ts.map +1 -0
- package/dist/notifications-core/index.js +445 -0
- package/dist/notifications-core/index.js.map +1 -0
- package/dist/portal-core/index.d.ts +451 -0
- package/dist/portal-core/index.d.ts.map +1 -0
- package/dist/portal-core/index.js +819 -0
- package/dist/portal-core/index.js.map +1 -0
- package/dist/portal.d.ts +7 -0
- package/dist/portal.d.ts.map +1 -0
- package/dist/portal.js +7 -0
- package/dist/portal.js.map +1 -0
- package/dist/push-core/index.cjs +171 -0
- package/dist/push-core/index.d.ts +149 -0
- package/dist/push-core/index.d.ts.map +1 -0
- package/dist/push-core/index.js +231 -0
- package/dist/push-core/index.js.map +1 -0
- package/dist/react-marketing.d.ts +1 -17
- package/dist/react-marketing.d.ts.map +1 -1
- package/dist/react-marketing.js +4 -0
- package/dist/react-marketing.js.map +1 -1
- package/dist/react.d.ts +294 -101
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +302 -246
- package/dist/react.js.map +1 -1
- package/dist/seed-core/index.d.ts +354 -0
- package/dist/seed-core/index.d.ts.map +1 -0
- package/dist/seed-core/index.js +420 -0
- package/dist/seed-core/index.js.map +1 -0
- package/dist/seed.d.ts +7 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +7 -0
- package/dist/seed.js.map +1 -0
- package/dist/seo-core/blog.d.ts +94 -0
- package/dist/seo-core/blog.d.ts.map +1 -0
- package/dist/seo-core/blog.js +149 -0
- package/dist/seo-core/blog.js.map +1 -0
- package/dist/seo-core/commerce.d.ts +64 -0
- package/dist/seo-core/commerce.d.ts.map +1 -0
- package/dist/seo-core/commerce.js +87 -0
- package/dist/seo-core/commerce.js.map +1 -0
- package/dist/seo-core/index.cjs +751 -0
- package/dist/seo-core/index.d.ts +226 -0
- package/dist/seo-core/index.d.ts.map +1 -0
- package/dist/seo-core/index.js +455 -0
- package/dist/seo-core/index.js.map +1 -0
- package/dist/seo-core/local.d.ts +181 -0
- package/dist/seo-core/local.d.ts.map +1 -0
- package/dist/seo-core/local.js +193 -0
- package/dist/seo-core/local.js.map +1 -0
- package/dist/sitemap-core/index.cjs +167 -0
- package/dist/sitemap-core/index.d.ts +155 -0
- package/dist/sitemap-core/index.d.ts.map +1 -0
- package/dist/sitemap-core/index.js +195 -0
- package/dist/sitemap-core/index.js.map +1 -0
- package/dist/social-core/index.d.ts +74 -0
- package/dist/social-core/index.d.ts.map +1 -0
- package/dist/social-core/index.js +72 -0
- package/dist/social-core/index.js.map +1 -0
- package/dist/social.d.ts +7 -0
- package/dist/social.d.ts.map +1 -0
- package/dist/social.js +7 -0
- package/dist/social.js.map +1 -0
- package/dist/solid-compat.d.ts +204 -0
- package/dist/solid-compat.d.ts.map +1 -0
- package/dist/solid-compat.js +221 -0
- package/dist/solid-compat.js.map +1 -0
- package/dist/store-core/index.d.ts +141 -0
- package/dist/store-core/index.d.ts.map +1 -0
- package/dist/store-core/index.js +277 -0
- package/dist/store-core/index.js.map +1 -0
- package/dist/store.d.ts +12 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +12 -0
- package/dist/store.js.map +1 -0
- package/dist/vanilla.d.ts +111 -0
- package/dist/vanilla.d.ts.map +1 -1
- package/dist/vanilla.js +206 -0
- package/dist/vanilla.js.map +1 -1
- package/dist/verify-core/index.d.ts +98 -0
- package/dist/verify-core/index.d.ts.map +1 -0
- package/dist/verify-core/index.js +186 -0
- package/dist/verify-core/index.js.map +1 -0
- package/dist/verify.d.ts +8 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +8 -0
- package/dist/verify.js.map +1 -0
- package/openapi.json +4316 -1313
- package/package.json +121 -9
- package/src/account-core/index.ts +309 -0
- package/src/account.ts +11 -0
- package/src/angular/headless.ts +519 -0
- package/src/angular/index.ts +3 -0
- package/src/auth-links.ts +76 -0
- package/src/blog-core/index.ts +423 -0
- package/src/blog.ts +11 -0
- package/src/chat-core/index.ts +335 -0
- package/src/chat.ts +11 -0
- package/src/checkout-core/index.ts +225 -0
- package/src/checkout.ts +10 -0
- package/src/cli.ts +184 -28
- package/src/client.ts +998 -0
- package/src/contracts.ts +730 -11
- package/src/data-model-schema.ts +1 -1
- package/src/element.ts +102 -0
- package/src/embed-protocol.ts +100 -0
- package/src/framework-support.ts +440 -0
- package/src/legal-core/index.ts +276 -0
- package/src/marketing-core/index.ts +209 -0
- package/src/marketing.ts +7 -0
- package/src/mcp-adapters/index.ts +264 -0
- package/src/mcp-adapters.ts +7 -0
- package/src/model3d-core/index.ts +248 -0
- package/src/notifications-core/index.ts +650 -0
- package/src/portal-core/index.ts +1564 -0
- package/src/portal.ts +7 -0
- package/src/push-core/index.ts +335 -0
- package/src/qwik.tsx +602 -2
- package/src/react-marketing.tsx +5 -0
- package/src/react.tsx +434 -319
- package/src/seed-core/index.ts +735 -0
- package/src/seed.ts +7 -0
- package/src/seo-core/blog.ts +216 -0
- package/src/seo-core/commerce.ts +138 -0
- package/src/seo-core/index.ts +605 -0
- package/src/seo-core/local.ts +333 -0
- package/src/sitemap-core/index.ts +296 -0
- package/src/social-core/index.ts +103 -0
- package/src/social.ts +7 -0
- package/src/solid-compat.tsx +347 -0
- package/src/solid-surfaces.tsx +1311 -0
- package/src/solid.tsx +82 -60
- package/src/store-core/index.ts +426 -0
- package/src/store.ts +12 -0
- package/src/svelte/SocialLinks.svelte +84 -0
- package/src/svelte/headless.ts +435 -0
- package/src/svelte/index.ts +9 -0
- package/src/vanilla.ts +367 -0
- package/src/verify-core/index.ts +288 -0
- package/src/verify.ts +8 -0
- package/src/vue/headless.ts +527 -0
- 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 —
|
|
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
|
-
> `@
|
|
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
|
-
> (`@
|
|
562
|
-
> privacy-conscious analytics (`@
|
|
563
|
-
> `@
|
|
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 — `@
|
|
573
|
-
`@
|
|
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
|
-
#### `@
|
|
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
|
|
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 '@
|
|
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
|
|
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
|
-
#### `@
|
|
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 '@
|
|
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 '@
|
|
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
|
-
#### `@
|
|
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 "@
|
|
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
|
-
`@
|
|
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 "@
|
|
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 `@
|
|
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/).
|