@businessdash/sdk 0.9.60
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/LICENSE +21 -0
- package/README.md +899 -0
- package/dist/adapters-revalidate.d.ts +44 -0
- package/dist/adapters-revalidate.d.ts.map +1 -0
- package/dist/adapters-revalidate.js +68 -0
- package/dist/adapters-revalidate.js.map +1 -0
- package/dist/alpha.d.ts +21 -0
- package/dist/alpha.d.ts.map +1 -0
- package/dist/alpha.js +21 -0
- package/dist/alpha.js.map +1 -0
- package/dist/analytics-core.d.ts +34 -0
- package/dist/analytics-core.d.ts.map +1 -0
- package/dist/analytics-core.js +138 -0
- package/dist/analytics-core.js.map +1 -0
- package/dist/analytics-server.d.ts +54 -0
- package/dist/analytics-server.d.ts.map +1 -0
- package/dist/analytics-server.js +78 -0
- package/dist/analytics-server.js.map +1 -0
- package/dist/attribution-logo.d.ts +2 -0
- package/dist/attribution-logo.d.ts.map +1 -0
- package/dist/attribution-logo.js +5 -0
- package/dist/attribution-logo.js.map +1 -0
- package/dist/auth-handler.d.ts +92 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +277 -0
- package/dist/auth-handler.js.map +1 -0
- package/dist/biab-forms.css +484 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1824 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +1681 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2845 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +13 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +13 -0
- package/dist/collections.js.map +1 -0
- package/dist/contracts.d.ts +14609 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2428 -0
- package/dist/contracts.js.map +1 -0
- package/dist/data-model-schema.d.ts +962 -0
- package/dist/data-model-schema.d.ts.map +1 -0
- package/dist/data-model-schema.js +2295 -0
- package/dist/data-model-schema.js.map +1 -0
- package/dist/distribution.d.ts +38 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +61 -0
- package/dist/distribution.js.map +1 -0
- package/dist/element.d.ts +121 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +276 -0
- package/dist/element.js.map +1 -0
- package/dist/enums.d.ts +25 -0
- package/dist/enums.d.ts.map +1 -0
- package/dist/enums.js +38 -0
- package/dist/enums.js.map +1 -0
- package/dist/errors.d.ts +195 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +188 -0
- package/dist/errors.js.map +1 -0
- package/dist/forms-core/autocomplete.d.ts +37 -0
- package/dist/forms-core/autocomplete.d.ts.map +1 -0
- package/dist/forms-core/autocomplete.js +82 -0
- package/dist/forms-core/autocomplete.js.map +1 -0
- package/dist/forms-core/business-function.d.ts +61 -0
- package/dist/forms-core/business-function.d.ts.map +1 -0
- package/dist/forms-core/business-function.js +149 -0
- package/dist/forms-core/business-function.js.map +1 -0
- package/dist/forms-core/calculation.d.ts +72 -0
- package/dist/forms-core/calculation.d.ts.map +1 -0
- package/dist/forms-core/calculation.js +103 -0
- package/dist/forms-core/calculation.js.map +1 -0
- package/dist/forms-core/controller.d.ts +249 -0
- package/dist/forms-core/controller.d.ts.map +1 -0
- package/dist/forms-core/controller.js +590 -0
- package/dist/forms-core/controller.js.map +1 -0
- package/dist/forms-core/elements.d.ts +44 -0
- package/dist/forms-core/elements.d.ts.map +1 -0
- package/dist/forms-core/elements.js +68 -0
- package/dist/forms-core/elements.js.map +1 -0
- package/dist/forms-core/email-domains.d.ts +10 -0
- package/dist/forms-core/email-domains.d.ts.map +1 -0
- package/dist/forms-core/email-domains.js +69 -0
- package/dist/forms-core/email-domains.js.map +1 -0
- package/dist/forms-core/file-rules.d.ts +51 -0
- package/dist/forms-core/file-rules.d.ts.map +1 -0
- package/dist/forms-core/file-rules.js +105 -0
- package/dist/forms-core/file-rules.js.map +1 -0
- package/dist/forms-core/formula.d.ts +114 -0
- package/dist/forms-core/formula.d.ts.map +1 -0
- package/dist/forms-core/formula.js +1170 -0
- package/dist/forms-core/formula.js.map +1 -0
- package/dist/forms-core/index.d.ts +24 -0
- package/dist/forms-core/index.d.ts.map +1 -0
- package/dist/forms-core/index.js +28 -0
- package/dist/forms-core/index.js.map +1 -0
- package/dist/forms-core/units.d.ts +49 -0
- package/dist/forms-core/units.d.ts.map +1 -0
- package/dist/forms-core/units.js +255 -0
- package/dist/forms-core/units.js.map +1 -0
- package/dist/forms-core/us-states.d.ts +14 -0
- package/dist/forms-core/us-states.d.ts.map +1 -0
- package/dist/forms-core/us-states.js +66 -0
- package/dist/forms-core/us-states.js.map +1 -0
- package/dist/forms-core/validation.d.ts +266 -0
- package/dist/forms-core/validation.d.ts.map +1 -0
- package/dist/forms-core/validation.js +850 -0
- package/dist/forms-core/validation.js.map +1 -0
- package/dist/forms.d.ts +19 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +29 -0
- package/dist/forms.js.map +1 -0
- package/dist/gallery-types.d.ts +32 -0
- package/dist/gallery-types.d.ts.map +1 -0
- package/dist/gallery-types.js +2 -0
- package/dist/gallery-types.js.map +1 -0
- package/dist/graphql.d.ts +97 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +212 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.cjs +19448 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/marketing-schema.d.ts +216 -0
- package/dist/marketing-schema.d.ts.map +1 -0
- package/dist/marketing-schema.js +200 -0
- package/dist/marketing-schema.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +87 -0
- package/dist/mcp.js.map +1 -0
- package/dist/native.d.ts +45 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +43 -0
- package/dist/native.js.map +1 -0
- package/dist/next-revalidate.d.ts +43 -0
- package/dist/next-revalidate.d.ts.map +1 -0
- package/dist/next-revalidate.js +84 -0
- package/dist/next-revalidate.js.map +1 -0
- package/dist/parallel-pages.d.ts +102 -0
- package/dist/parallel-pages.d.ts.map +1 -0
- package/dist/parallel-pages.js +108 -0
- package/dist/parallel-pages.js.map +1 -0
- package/dist/payment-icons.d.ts +66 -0
- package/dist/payment-icons.d.ts.map +1 -0
- package/dist/payment-icons.js +160 -0
- package/dist/payment-icons.js.map +1 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +97 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react-analytics.d.ts +39 -0
- package/dist/react-analytics.d.ts.map +1 -0
- package/dist/react-analytics.js +59 -0
- package/dist/react-analytics.js.map +1 -0
- package/dist/react-attribution.d.ts +99 -0
- package/dist/react-attribution.d.ts.map +1 -0
- package/dist/react-attribution.js +52 -0
- package/dist/react-attribution.js.map +1 -0
- package/dist/react-marketing.d.ts +181 -0
- package/dist/react-marketing.d.ts.map +1 -0
- package/dist/react-marketing.js +215 -0
- package/dist/react-marketing.js.map +1 -0
- package/dist/react.d.ts +727 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2418 -0
- package/dist/react.js.map +1 -0
- package/dist/reserved-paths.d.ts +24 -0
- package/dist/reserved-paths.d.ts.map +1 -0
- package/dist/reserved-paths.js +60 -0
- package/dist/reserved-paths.js.map +1 -0
- package/dist/revalidate-verify.d.ts +30 -0
- package/dist/revalidate-verify.d.ts.map +1 -0
- package/dist/revalidate-verify.js +74 -0
- package/dist/revalidate-verify.js.map +1 -0
- package/dist/sdk.d.ts +678 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +801 -0
- package/dist/sdk.js.map +1 -0
- package/dist/seo.d.ts +218 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +309 -0
- package/dist/seo.js.map +1 -0
- package/dist/socials.d.ts +47 -0
- package/dist/socials.d.ts.map +1 -0
- package/dist/socials.js +172 -0
- package/dist/socials.js.map +1 -0
- package/dist/static.d.ts +14 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +14 -0
- package/dist/static.js.map +1 -0
- package/dist/url-pattern.d.ts +47 -0
- package/dist/url-pattern.d.ts.map +1 -0
- package/dist/url-pattern.js +124 -0
- package/dist/url-pattern.js.map +1 -0
- package/dist/vanilla.d.ts +77 -0
- package/dist/vanilla.d.ts.map +1 -0
- package/dist/vanilla.js +903 -0
- package/dist/vanilla.js.map +1 -0
- package/openapi.json +13779 -0
- package/package.json +204 -0
- package/src/adapters-revalidate.ts +88 -0
- package/src/alpha.ts +89 -0
- package/src/analytics-core.ts +175 -0
- package/src/analytics-server.ts +99 -0
- package/src/angular/biab-form.component.ts +1443 -0
- package/src/angular/index.ts +26 -0
- package/src/attribution-logo.ts +5 -0
- package/src/auth-handler.ts +352 -0
- package/src/cli.ts +2517 -0
- package/src/client.ts +3578 -0
- package/src/collections.ts +101 -0
- package/src/contracts.ts +3390 -0
- package/src/data-model-schema.ts +4042 -0
- package/src/distribution.ts +73 -0
- package/src/element.ts +321 -0
- package/src/enums.ts +65 -0
- package/src/errors.ts +338 -0
- package/src/forms-core/autocomplete.ts +97 -0
- package/src/forms-core/business-function.ts +209 -0
- package/src/forms-core/calculation.ts +167 -0
- package/src/forms-core/controller.ts +922 -0
- package/src/forms-core/elements.ts +86 -0
- package/src/forms-core/email-domains.ts +68 -0
- package/src/forms-core/file-rules.ts +123 -0
- package/src/forms-core/formula.ts +1339 -0
- package/src/forms-core/index.ts +122 -0
- package/src/forms-core/units.ts +308 -0
- package/src/forms-core/us-states.ts +68 -0
- package/src/forms-core/validation.ts +1037 -0
- package/src/forms.ts +63 -0
- package/src/gallery-types.ts +31 -0
- package/src/graphql.ts +295 -0
- package/src/index.ts +505 -0
- package/src/marketing-schema.ts +395 -0
- package/src/mcp.ts +108 -0
- package/src/native.ts +73 -0
- package/src/next-revalidate.ts +124 -0
- package/src/parallel-pages.ts +192 -0
- package/src/payment-icons.ts +205 -0
- package/src/proxy.ts +173 -0
- package/src/qwik.tsx +1234 -0
- package/src/react-analytics.tsx +76 -0
- package/src/react-attribution.tsx +192 -0
- package/src/react-marketing.tsx +338 -0
- package/src/react.tsx +5495 -0
- package/src/reserved-paths.ts +65 -0
- package/src/revalidate-verify.ts +98 -0
- package/src/sdk.ts +1671 -0
- package/src/seo.ts +488 -0
- package/src/socials.ts +230 -0
- package/src/solid.tsx +1466 -0
- package/src/static.ts +21 -0
- package/src/styles/biab-forms.css +484 -0
- package/src/svelte/BiabForm.svelte +1069 -0
- package/src/svelte/context.ts +57 -0
- package/src/svelte/index.ts +79 -0
- package/src/url-pattern.ts +153 -0
- package/src/vanilla.ts +1290 -0
- package/src/vue/BiabForm.vue +1538 -0
- package/src/vue/index.ts +119 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,3578 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AddressDetailsResponse,
|
|
5
|
+
type AddressFormValues,
|
|
6
|
+
type AddressSearchInput,
|
|
7
|
+
type AddressSearchResponse,
|
|
8
|
+
type AddressVerifyResponse,
|
|
9
|
+
type AiVisibilityInput,
|
|
10
|
+
type AiVisibilityResponse,
|
|
11
|
+
type AttributionHeartbeatInput,
|
|
12
|
+
type AttributionHeartbeatResponse,
|
|
13
|
+
type AttributionPolicy,
|
|
14
|
+
type AuthIntrospectResponse,
|
|
15
|
+
addressDetailsResponseSchema,
|
|
16
|
+
addressFormValuesSchema,
|
|
17
|
+
addressSearchInputSchema,
|
|
18
|
+
addressSearchResponseSchema,
|
|
19
|
+
addressVerifyResponseSchema,
|
|
20
|
+
aiVisibilityInputSchema,
|
|
21
|
+
aiVisibilityResponseSchema,
|
|
22
|
+
attributionHeartbeatInputSchema,
|
|
23
|
+
attributionHeartbeatResponseSchema,
|
|
24
|
+
attributionPolicySchema,
|
|
25
|
+
authIntrospectResponseSchema,
|
|
26
|
+
type BlogCategoryListResponse,
|
|
27
|
+
type BlogCommentListResponse,
|
|
28
|
+
type BlogCommentPostInput,
|
|
29
|
+
type BlogCommentPostResponse,
|
|
30
|
+
type BlogLikeResponse,
|
|
31
|
+
type BlogListPostsResponse,
|
|
32
|
+
type BlogPostBySlugResponse,
|
|
33
|
+
type BlogSessionInput,
|
|
34
|
+
type BlogSessionResponse,
|
|
35
|
+
type BlogTagListResponse,
|
|
36
|
+
blogCategoryListResponseSchema,
|
|
37
|
+
blogCommentListResponseSchema,
|
|
38
|
+
blogCommentPostInputSchema,
|
|
39
|
+
blogCommentPostResponseSchema,
|
|
40
|
+
blogLikeResponseSchema,
|
|
41
|
+
blogListPostsResponseSchema,
|
|
42
|
+
blogPostBySlugResponseSchema,
|
|
43
|
+
blogSessionInputSchema,
|
|
44
|
+
blogSessionResponseSchema,
|
|
45
|
+
blogTagListResponseSchema,
|
|
46
|
+
type CartAddItemInput,
|
|
47
|
+
type CartCouponInput,
|
|
48
|
+
type CartSessionResponse,
|
|
49
|
+
type CartSnapshot,
|
|
50
|
+
type CartUpdateItemInput,
|
|
51
|
+
type ChatbotAppendMessageInput,
|
|
52
|
+
type ChatbotAvailability,
|
|
53
|
+
type ChatbotChatInput,
|
|
54
|
+
type ChatbotChatResponse,
|
|
55
|
+
type ChatbotConfig,
|
|
56
|
+
type ChatbotConfigUpdateInput,
|
|
57
|
+
type ChatbotFormDefinition,
|
|
58
|
+
type ChatbotFormSubmitInput,
|
|
59
|
+
type ChatbotFormSubmitResponse,
|
|
60
|
+
type ChatbotMessagesResponse,
|
|
61
|
+
type ChatbotPersistedSessionInput,
|
|
62
|
+
type ChatbotPersistedSessionResponse,
|
|
63
|
+
type ChatbotRequestHumanInput,
|
|
64
|
+
type ChatbotRequestHumanResponse,
|
|
65
|
+
type ChatbotSessionResponse,
|
|
66
|
+
type CheckoutSessionInput,
|
|
67
|
+
type CheckoutSessionResponse,
|
|
68
|
+
type CheckoutStartInput,
|
|
69
|
+
type CheckoutStartResponse,
|
|
70
|
+
type CheckoutStatusResponse,
|
|
71
|
+
type ConfirmVerificationInput,
|
|
72
|
+
type ConfirmVerificationResponse,
|
|
73
|
+
type CouponValidateResponse,
|
|
74
|
+
type CreateCollectionInput,
|
|
75
|
+
type CreateCollectionResponse,
|
|
76
|
+
type CreateEmailTemplateInput,
|
|
77
|
+
type CreateEmailTemplateResponse,
|
|
78
|
+
type CreateEmailTemplateVersionInput,
|
|
79
|
+
type CreateEmailTemplateVersionResponse,
|
|
80
|
+
type CustomerContactCompanyInput,
|
|
81
|
+
type CustomerContactCompanyResponse,
|
|
82
|
+
type CustomerContractDetail,
|
|
83
|
+
type CustomerContractSignResponse,
|
|
84
|
+
type CustomerDownloadsResponse,
|
|
85
|
+
type CustomerInvoiceDetail,
|
|
86
|
+
type CustomerInvoicePayInput,
|
|
87
|
+
type CustomerInvoicePayResponse,
|
|
88
|
+
type CustomerInvoicePdf,
|
|
89
|
+
type CustomerJobActivityResponse,
|
|
90
|
+
type CustomerJobComment,
|
|
91
|
+
type CustomerJobCommentPostInput,
|
|
92
|
+
type CustomerJobCommentsListResponse,
|
|
93
|
+
type CustomerJobDetail,
|
|
94
|
+
type CustomerJobEtaResponse,
|
|
95
|
+
type CustomerNotificationPreferencesResponse,
|
|
96
|
+
type CustomerNotificationPreferencesUpdateInput,
|
|
97
|
+
type CustomerNotificationPreferencesUpdateResponse,
|
|
98
|
+
type CustomerOrderDetail,
|
|
99
|
+
type CustomerOrderListResponse,
|
|
100
|
+
type CustomerPortalContextResponse,
|
|
101
|
+
type CustomerPortalOtherOrgsResponse,
|
|
102
|
+
type CustomerPortalProfile,
|
|
103
|
+
type CustomerPortalUpdateProfileInput,
|
|
104
|
+
type CustomerPortalUpdateProfileResponse,
|
|
105
|
+
type CustomerQuoteActionResponse,
|
|
106
|
+
type CustomerQuoteDetail,
|
|
107
|
+
type CustomerReferralHistoryResponse,
|
|
108
|
+
type CustomerReferralPayoutsResponse,
|
|
109
|
+
type CustomerReferralStats,
|
|
110
|
+
type CustomerReturnRequestCreated,
|
|
111
|
+
type CustomerReturnRequestInput,
|
|
112
|
+
type CustomerReviewListResponse,
|
|
113
|
+
type CustomerReviewSubmitInput,
|
|
114
|
+
type CustomerReviewSubmitResponse,
|
|
115
|
+
type CustomerShipmentTracking,
|
|
116
|
+
type CustomerWorkBundle,
|
|
117
|
+
cartAddItemInputSchema,
|
|
118
|
+
cartCouponInputSchema,
|
|
119
|
+
cartSessionResponseSchema,
|
|
120
|
+
cartSnapshotSchema,
|
|
121
|
+
cartUpdateItemInputSchema,
|
|
122
|
+
chatbotAppendMessageInputSchema,
|
|
123
|
+
chatbotAvailabilitySchema,
|
|
124
|
+
chatbotChatInputSchema,
|
|
125
|
+
chatbotChatResponseSchema,
|
|
126
|
+
chatbotConfigSchema,
|
|
127
|
+
chatbotConfigUpdateInputSchema,
|
|
128
|
+
chatbotFormDefinitionSchema,
|
|
129
|
+
chatbotFormSubmitInputSchema,
|
|
130
|
+
chatbotFormSubmitResponseSchema,
|
|
131
|
+
chatbotMessagesResponseSchema,
|
|
132
|
+
chatbotPersistedSessionInputSchema,
|
|
133
|
+
chatbotPersistedSessionResponseSchema,
|
|
134
|
+
chatbotRequestHumanInputSchema,
|
|
135
|
+
chatbotRequestHumanResponseSchema,
|
|
136
|
+
chatbotSessionResponseSchema,
|
|
137
|
+
chatbotUploadUrlResponseSchema,
|
|
138
|
+
checkoutSessionInputSchema,
|
|
139
|
+
checkoutSessionResponseSchema,
|
|
140
|
+
checkoutStartInputSchema,
|
|
141
|
+
checkoutStartResponseSchema,
|
|
142
|
+
checkoutStatusResponseSchema,
|
|
143
|
+
confirmVerificationInputSchema,
|
|
144
|
+
confirmVerificationResponseSchema,
|
|
145
|
+
couponValidateInputSchema,
|
|
146
|
+
couponValidateResponseSchema,
|
|
147
|
+
createCollectionInputSchema,
|
|
148
|
+
createCollectionResponseSchema,
|
|
149
|
+
createEmailTemplateInputSchema,
|
|
150
|
+
createEmailTemplateResponseSchema,
|
|
151
|
+
createEmailTemplateVersionInputSchema,
|
|
152
|
+
createEmailTemplateVersionResponseSchema,
|
|
153
|
+
customerContactCompanyInputSchema,
|
|
154
|
+
customerContactCompanyResponseSchema,
|
|
155
|
+
customerContractDetailSchema,
|
|
156
|
+
customerContractSignResponseSchema,
|
|
157
|
+
customerDownloadsResponseSchema,
|
|
158
|
+
customerInvoiceDetailSchema,
|
|
159
|
+
customerInvoicePayInputSchema,
|
|
160
|
+
customerInvoicePayResponseSchema,
|
|
161
|
+
customerInvoicePdfSchema,
|
|
162
|
+
customerJobActivityResponseSchema,
|
|
163
|
+
customerJobCommentPostInputSchema,
|
|
164
|
+
customerJobCommentSchema,
|
|
165
|
+
customerJobCommentsListResponseSchema,
|
|
166
|
+
customerJobDetailSchema,
|
|
167
|
+
customerJobEtaResponseSchema,
|
|
168
|
+
customerNotificationPreferencesResponseSchema,
|
|
169
|
+
customerNotificationPreferencesUpdateInputSchema,
|
|
170
|
+
customerNotificationPreferencesUpdateResponseSchema,
|
|
171
|
+
customerOrderDetailSchema,
|
|
172
|
+
customerOrderListResponseSchema,
|
|
173
|
+
customerPaymentHistoryResponseSchema,
|
|
174
|
+
customerPortalContextResponseSchema,
|
|
175
|
+
customerPortalOtherOrgsResponseSchema,
|
|
176
|
+
customerPortalProfileSchema,
|
|
177
|
+
customerPortalUpdateProfileInputSchema,
|
|
178
|
+
customerPortalUpdateProfileResponseSchema,
|
|
179
|
+
customerQuoteActionResponseSchema,
|
|
180
|
+
customerQuoteDetailSchema,
|
|
181
|
+
customerReferralHistoryResponseSchema,
|
|
182
|
+
customerReferralPayoutsResponseSchema,
|
|
183
|
+
customerReferralStatsSchema,
|
|
184
|
+
customerReturnRequestCreatedSchema,
|
|
185
|
+
customerReturnRequestInputSchema,
|
|
186
|
+
customerReviewListResponseSchema,
|
|
187
|
+
customerReviewSubmitInputSchema,
|
|
188
|
+
customerReviewSubmitResponseSchema,
|
|
189
|
+
customerShipmentTrackingSchema,
|
|
190
|
+
customerWorkBundleSchema,
|
|
191
|
+
type DashboardSessionResponse,
|
|
192
|
+
type DataModelRecord,
|
|
193
|
+
dashboardSessionResponseSchema,
|
|
194
|
+
type ExecuteActionInput,
|
|
195
|
+
type ExecuteActionResponse,
|
|
196
|
+
executeActionInputSchema,
|
|
197
|
+
executeActionResponseSchema,
|
|
198
|
+
type FollowerEditInput,
|
|
199
|
+
type FollowerEditResponse,
|
|
200
|
+
type FollowerJoinInput,
|
|
201
|
+
type FollowerJoinResponse,
|
|
202
|
+
type FollowerLeaveInput,
|
|
203
|
+
type FollowerLeaveResponse,
|
|
204
|
+
type FollowerMeInput,
|
|
205
|
+
type FollowerMeResponse,
|
|
206
|
+
followerEditInputSchema,
|
|
207
|
+
followerEditResponseSchema,
|
|
208
|
+
followerJoinInputSchema,
|
|
209
|
+
followerJoinResponseSchema,
|
|
210
|
+
followerLeaveInputSchema,
|
|
211
|
+
followerLeaveResponseSchema,
|
|
212
|
+
followerMeInputSchema,
|
|
213
|
+
followerMeResponseSchema,
|
|
214
|
+
type GetCollectionResponse,
|
|
215
|
+
type GetEmailTemplateResponse,
|
|
216
|
+
type GetMarketingLocalesResponse,
|
|
217
|
+
type GetMarketingPageBundleResponse,
|
|
218
|
+
type GetPublishedMarketingSchemaResponse,
|
|
219
|
+
type GetSiteMarketingPageResponse,
|
|
220
|
+
type GoogleAddress,
|
|
221
|
+
getCollectionResponseSchema,
|
|
222
|
+
getEmailTemplateResponseSchema,
|
|
223
|
+
getMarketingLocalesResponseSchema,
|
|
224
|
+
getMarketingPageBundleResponseSchema,
|
|
225
|
+
getPublishedMarketingSchemaResponseSchema,
|
|
226
|
+
getSiteMarketingPageResponseSchema,
|
|
227
|
+
type JobCategoryListResponse,
|
|
228
|
+
type JobPosting,
|
|
229
|
+
type JobPostingListResponse,
|
|
230
|
+
jobCategoryListResponseSchema,
|
|
231
|
+
jobPostingListResponseSchema,
|
|
232
|
+
jobPostingSchema,
|
|
233
|
+
type ListCollectionsResponse,
|
|
234
|
+
type ListDataModelRecordsInput,
|
|
235
|
+
type ListDataModelRecordsResponse,
|
|
236
|
+
type ListEmailTemplatesInput,
|
|
237
|
+
type ListEmailTemplatesResponse,
|
|
238
|
+
type ListRowsInput,
|
|
239
|
+
type ListRowsResponse,
|
|
240
|
+
type ListSiteMarketingPagesResponse,
|
|
241
|
+
listCollectionsResponseSchema,
|
|
242
|
+
listDataModelRecordsInputSchema,
|
|
243
|
+
listDataModelRecordsResponseSchema,
|
|
244
|
+
listEmailTemplatesInputSchema,
|
|
245
|
+
listEmailTemplatesResponseSchema,
|
|
246
|
+
listRowsInputSchema,
|
|
247
|
+
listRowsResponseSchema,
|
|
248
|
+
listSiteMarketingPagesResponseSchema,
|
|
249
|
+
type NotificationPreferencesResponse,
|
|
250
|
+
type NotificationPreferencesUpdateInput,
|
|
251
|
+
notificationPreferencesResponseSchema,
|
|
252
|
+
notificationPreferencesUpdateInputSchema,
|
|
253
|
+
type PageViewsInput,
|
|
254
|
+
type PageViewsResponse,
|
|
255
|
+
type PublishMarketingSectionsInput,
|
|
256
|
+
type PublishMarketingSectionsResponse,
|
|
257
|
+
pageViewsInputSchema,
|
|
258
|
+
pageViewsResponseSchema,
|
|
259
|
+
publishMarketingSectionsInputSchema,
|
|
260
|
+
publishMarketingSectionsResponseSchema,
|
|
261
|
+
type QueryRowsInput,
|
|
262
|
+
type QueryRowsResponse,
|
|
263
|
+
queryRowsInputSchema,
|
|
264
|
+
queryRowsResponseSchema,
|
|
265
|
+
type ReviewWallListResponse,
|
|
266
|
+
reviewWallListResponseSchema,
|
|
267
|
+
type ShippingRatesInput,
|
|
268
|
+
type ShippingRatesResponse,
|
|
269
|
+
type ShippingSettingsResponse,
|
|
270
|
+
type ShippingTrackResponse,
|
|
271
|
+
type SiteAssetInput,
|
|
272
|
+
type SiteBranding,
|
|
273
|
+
type StartVerificationInput,
|
|
274
|
+
type StartVerificationResponse,
|
|
275
|
+
type StorefrontAddonsResponse,
|
|
276
|
+
type StorefrontCategoriesResponse,
|
|
277
|
+
type StorefrontListProductsResponse,
|
|
278
|
+
type StorefrontProductDetail,
|
|
279
|
+
type StorefrontProductReviewsResponse,
|
|
280
|
+
type StorefrontProductsWithMetaResponse,
|
|
281
|
+
type StorefrontRelatedProductsResponse,
|
|
282
|
+
type StorefrontSessionInput,
|
|
283
|
+
type StorefrontSessionResponse,
|
|
284
|
+
type StorefrontSort,
|
|
285
|
+
type SubscriptionCheckoutInput,
|
|
286
|
+
type SubscriptionCheckoutResponse,
|
|
287
|
+
type SubscriptionOffering,
|
|
288
|
+
type SubscriptionOfferingsListResponse,
|
|
289
|
+
shippingRatesInputSchema,
|
|
290
|
+
shippingRatesResponseSchema,
|
|
291
|
+
shippingSettingsResponseSchema,
|
|
292
|
+
shippingTrackResponseSchema,
|
|
293
|
+
siteAssetInputSchema,
|
|
294
|
+
siteBrandingSchema,
|
|
295
|
+
startVerificationInputSchema,
|
|
296
|
+
startVerificationResponseSchema,
|
|
297
|
+
storefrontAddonsResponseSchema,
|
|
298
|
+
storefrontCategoriesResponseSchema,
|
|
299
|
+
storefrontListProductsResponseSchema,
|
|
300
|
+
storefrontProductDetailSchema,
|
|
301
|
+
storefrontProductReviewsResponseSchema,
|
|
302
|
+
storefrontProductsWithMetaResponseSchema,
|
|
303
|
+
storefrontRelatedProductsResponseSchema,
|
|
304
|
+
storefrontSessionInputSchema,
|
|
305
|
+
storefrontSessionResponseSchema,
|
|
306
|
+
subscriptionCheckoutInputSchema,
|
|
307
|
+
subscriptionCheckoutResponseSchema,
|
|
308
|
+
subscriptionOfferingSchema,
|
|
309
|
+
subscriptionOfferingsListResponseSchema,
|
|
310
|
+
type TenantAuthExchangeInput,
|
|
311
|
+
type TenantAuthExchangeResponse,
|
|
312
|
+
type TenantAuthMeResponse,
|
|
313
|
+
type TenantAuthPasswordResetInput,
|
|
314
|
+
type TenantAuthPasswordResetResponse,
|
|
315
|
+
type TenantAuthSignOutResponse,
|
|
316
|
+
type TenantAuthStartInput,
|
|
317
|
+
type TenantAuthStartResponse,
|
|
318
|
+
tenantAuthExchangeInputSchema,
|
|
319
|
+
tenantAuthExchangeResponseSchema,
|
|
320
|
+
tenantAuthMeResponseSchema,
|
|
321
|
+
tenantAuthPasswordResetInputSchema,
|
|
322
|
+
tenantAuthPasswordResetResponseSchema,
|
|
323
|
+
tenantAuthSignOutResponseSchema,
|
|
324
|
+
tenantAuthStartInputSchema,
|
|
325
|
+
tenantAuthStartResponseSchema,
|
|
326
|
+
type UploadSiteAssetResponse,
|
|
327
|
+
type UpsertDraftMarketingSchemaInput,
|
|
328
|
+
type UpsertDraftMarketingSchemaResponse,
|
|
329
|
+
type UpsertRowInput,
|
|
330
|
+
type UpsertRowResponse,
|
|
331
|
+
uploadSiteAssetResponseSchema,
|
|
332
|
+
upsertDraftMarketingSchemaInputSchema,
|
|
333
|
+
upsertDraftMarketingSchemaResponseSchema,
|
|
334
|
+
upsertRowInputSchema,
|
|
335
|
+
upsertRowResponseSchema,
|
|
336
|
+
} from "./contracts.js";
|
|
337
|
+
import {
|
|
338
|
+
accessRejectionFromBody,
|
|
339
|
+
BiabApiError,
|
|
340
|
+
isBiabPlanGateBody,
|
|
341
|
+
} from "./errors.js";
|
|
342
|
+
|
|
343
|
+
export type FetchLike = typeof fetch;
|
|
344
|
+
|
|
345
|
+
/** Options for {@link BiabDevClient} / {@link createBiabDevClient}. */
|
|
346
|
+
export type BiabDevClientOptions = {
|
|
347
|
+
baseUrl: string;
|
|
348
|
+
apiKey: string;
|
|
349
|
+
fetch?: FetchLike;
|
|
350
|
+
defaultHeaders?: HeadersInit;
|
|
351
|
+
/**
|
|
352
|
+
* Origin to send to the platform on every request. Required when the API
|
|
353
|
+
* key has an Allowed Host set on the host (server-to-server `fetch` does
|
|
354
|
+
* not send `Origin` automatically — without it the platform replies 403
|
|
355
|
+
* `origin_required`). Accepts a full URL (`https://my-site.com`) or a
|
|
356
|
+
* bare hostname (`my-site.com`); only the origin is sent on the wire.
|
|
357
|
+
*/
|
|
358
|
+
siteOrigin?: string;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/** An array value becomes a repeated param: `?path=/a&path=/b`. */
|
|
362
|
+
type QueryValue =
|
|
363
|
+
| string
|
|
364
|
+
| number
|
|
365
|
+
| boolean
|
|
366
|
+
| Array<string | number>
|
|
367
|
+
| undefined;
|
|
368
|
+
|
|
369
|
+
type RequestOptions<T> = {
|
|
370
|
+
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
371
|
+
path: string;
|
|
372
|
+
query?: Record<string, QueryValue>;
|
|
373
|
+
body?: unknown;
|
|
374
|
+
responseSchema: z.ZodType<T, z.ZodTypeDef, unknown>;
|
|
375
|
+
headers?: HeadersInit;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
function buildUrl(
|
|
379
|
+
baseUrl: string,
|
|
380
|
+
path: string,
|
|
381
|
+
query?: Record<string, QueryValue>,
|
|
382
|
+
) {
|
|
383
|
+
/**
|
|
384
|
+
* Strip any leading slash from `path` so it is treated as a relative path and
|
|
385
|
+
* appended to `baseUrl`'s pathname (e.g. `/api/package/v1`). Adding a leading
|
|
386
|
+
* slash here would make `path` absolute under the URL spec and silently drop
|
|
387
|
+
* the base URL's path component — see https://url.spec.whatwg.org/#concept-basic-url-parser.
|
|
388
|
+
*/
|
|
389
|
+
const relativePath = path.startsWith("/") ? path.slice(1) : path;
|
|
390
|
+
const url = new URL(relativePath, baseUrl);
|
|
391
|
+
|
|
392
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
393
|
+
if (value == null) continue;
|
|
394
|
+
if (Array.isArray(value)) {
|
|
395
|
+
// Repeat the param once per element so the server reads getAll(key).
|
|
396
|
+
for (const item of value) url.searchParams.append(key, String(item));
|
|
397
|
+
} else {
|
|
398
|
+
url.searchParams.set(key, String(value));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return url;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function parseResponseBody(response: Response) {
|
|
406
|
+
const text = await response.text();
|
|
407
|
+
if (!text) return null;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
return JSON.parse(text) as unknown;
|
|
411
|
+
} catch {
|
|
412
|
+
return text;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Coerce a user-provided origin (URL or bare hostname) into a wire-safe
|
|
418
|
+
* `scheme://host` value. Returns `undefined` if input is empty.
|
|
419
|
+
*/
|
|
420
|
+
function normalizeSiteOrigin(raw: string | undefined): string | undefined {
|
|
421
|
+
if (!raw) return undefined;
|
|
422
|
+
const trimmed = raw.trim();
|
|
423
|
+
if (!trimmed) return undefined;
|
|
424
|
+
const withScheme = /^https?:\/\//i.test(trimmed)
|
|
425
|
+
? trimmed
|
|
426
|
+
: `https://${trimmed}`;
|
|
427
|
+
try {
|
|
428
|
+
const url = new URL(withScheme);
|
|
429
|
+
return `${url.protocol}//${url.host}`;
|
|
430
|
+
} catch {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function extractErrorMessage(body: unknown, fallback: string) {
|
|
436
|
+
if (body && typeof body === "object" && "message" in body) {
|
|
437
|
+
const message = body.message;
|
|
438
|
+
if (typeof message === "string" && message.trim()) {
|
|
439
|
+
return message;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return fallback;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Typed client for the BIAB package HTTP API.
|
|
448
|
+
*
|
|
449
|
+
* @remarks
|
|
450
|
+
* **Alpha:** This SDK is in alpha. Surfaces and behavior may change without semver guarantees.
|
|
451
|
+
* Do not rely on it for production workloads—use development, staging, or experiments only.
|
|
452
|
+
*/
|
|
453
|
+
export class BiabDevClient {
|
|
454
|
+
private readonly baseUrl: string;
|
|
455
|
+
private readonly apiKey: string;
|
|
456
|
+
private readonly fetchImpl: FetchLike;
|
|
457
|
+
private readonly defaultHeaders: HeadersInit | undefined;
|
|
458
|
+
|
|
459
|
+
constructor(options: BiabDevClientOptions) {
|
|
460
|
+
// Trailing slash is required by the WHATWG URL parser so the relative
|
|
461
|
+
// `path` segments are appended to (rather than replacing) the base path.
|
|
462
|
+
this.baseUrl = options.baseUrl.endsWith("/")
|
|
463
|
+
? options.baseUrl
|
|
464
|
+
: `${options.baseUrl}/`;
|
|
465
|
+
this.apiKey = options.apiKey;
|
|
466
|
+
// Bind `fetch` to its global so that calling it as a method on
|
|
467
|
+
// `this` (`this.fetchImpl(url, init)`) doesn't trigger the
|
|
468
|
+
// "Illegal invocation" error in browsers — `Window.fetch` requires
|
|
469
|
+
// `this === window`. `globalThis` works in Node 18+ and the browser.
|
|
470
|
+
this.fetchImpl =
|
|
471
|
+
options.fetch ??
|
|
472
|
+
((input: RequestInfo | URL, init?: RequestInit) =>
|
|
473
|
+
globalThis.fetch(input, init));
|
|
474
|
+
this.defaultHeaders = options.defaultHeaders;
|
|
475
|
+
this.siteOrigin = normalizeSiteOrigin(options.siteOrigin);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private readonly siteOrigin: string | undefined;
|
|
479
|
+
|
|
480
|
+
site(siteId: string) {
|
|
481
|
+
return new BiabDevSiteClient(this, siteId);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
get emailTemplates() {
|
|
485
|
+
return new BiabDevEmailTemplatesClient(this);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
get dashboard() {
|
|
489
|
+
return new BiabDevDashboardClient(this);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
get storefront() {
|
|
493
|
+
return new BiabDevStorefrontClient(this);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
get cart() {
|
|
497
|
+
return new BiabDevCartClient(this);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
get checkout() {
|
|
501
|
+
return new BiabDevCheckoutClient(this);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
get coupons() {
|
|
505
|
+
return new BiabDevCouponsClient(this);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
get subscriptions() {
|
|
509
|
+
return new BiabDevSubscriptionsClient(this);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
get shipping() {
|
|
513
|
+
return new BiabDevShippingClient(this);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
get notifications() {
|
|
517
|
+
return new BiabDevNotificationsClient(this);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
get address() {
|
|
521
|
+
return new BiabDevAddressClient(this);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
get blog() {
|
|
525
|
+
return new BiabDevBlogClient(this);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
get reviews() {
|
|
529
|
+
return new BiabDevReviewsClient(this);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/** Public job postings — build a careers page on your own domain. */
|
|
533
|
+
get hiring() {
|
|
534
|
+
return new BiabDevHiringClient(this);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
get chatbot() {
|
|
538
|
+
return new BiabDevChatbotClient(this);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
get auth() {
|
|
542
|
+
return new BiabDevAuthClient(this);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Customer-portal client. Pin to a specific BIAB organization id so the
|
|
547
|
+
* caller never sees data from any other org (recommended). Omit
|
|
548
|
+
* `organizationId` to fall back to the user's currently-active session
|
|
549
|
+
* org — only use that on first-party hosts where the host already pins
|
|
550
|
+
* the org.
|
|
551
|
+
*/
|
|
552
|
+
customerPortal(organizationId?: string) {
|
|
553
|
+
return new BiabDevCustomerPortalClient(this, organizationId ?? null);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async introspect(): Promise<AuthIntrospectResponse> {
|
|
557
|
+
return this.request({
|
|
558
|
+
method: "POST",
|
|
559
|
+
path: "auth/introspect",
|
|
560
|
+
responseSchema: authIntrospectResponseSchema,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async request<T>(options: RequestOptions<T>): Promise<T> {
|
|
565
|
+
const url = buildUrl(this.baseUrl, options.path, options.query);
|
|
566
|
+
const init: RequestInit = {
|
|
567
|
+
method: options.method ?? "GET",
|
|
568
|
+
headers: {
|
|
569
|
+
Accept: "application/json",
|
|
570
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
571
|
+
...(options.body === undefined
|
|
572
|
+
? {}
|
|
573
|
+
: { "Content-Type": "application/json" }),
|
|
574
|
+
...(this.siteOrigin ? { Origin: this.siteOrigin } : {}),
|
|
575
|
+
...(this.defaultHeaders ?? {}),
|
|
576
|
+
...(options.headers ?? {}),
|
|
577
|
+
},
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
if (options.body !== undefined) {
|
|
581
|
+
init.body = JSON.stringify(options.body);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const response = await this.fetchImpl(url, init);
|
|
585
|
+
|
|
586
|
+
const body = await parseResponseBody(response);
|
|
587
|
+
|
|
588
|
+
// Access gate — 200 (reads, body-only signal) and 402 (writes)
|
|
589
|
+
// both surface as a typed error so consumer code is uniform.
|
|
590
|
+
// Three flavours: plan-required (BiabFeatureNotEntitledError),
|
|
591
|
+
// payment-required (BiabPaymentLapsedError), service-suspended
|
|
592
|
+
// (BiabServiceSuspendedError) — all extend BiabAccessRejectedError
|
|
593
|
+
// for "catch any" patterns. `unwrapAccess()` is the opt-in
|
|
594
|
+
// Result-shape escape hatch.
|
|
595
|
+
if (isBiabPlanGateBody(body)) {
|
|
596
|
+
throw accessRejectionFromBody({
|
|
597
|
+
body,
|
|
598
|
+
path: url.pathname,
|
|
599
|
+
status: response.status,
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!response.ok) {
|
|
604
|
+
throw new BiabApiError({
|
|
605
|
+
status: response.status,
|
|
606
|
+
path: url.pathname,
|
|
607
|
+
message: extractErrorMessage(
|
|
608
|
+
body,
|
|
609
|
+
`BIAB API request failed with status ${response.status}.`,
|
|
610
|
+
),
|
|
611
|
+
body,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return options.responseSchema.parse(body);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
export class BiabDevSiteClient {
|
|
620
|
+
readonly collections: BiabDevCollectionsClient;
|
|
621
|
+
readonly rows: BiabDevRowsClient;
|
|
622
|
+
/**
|
|
623
|
+
* The org's custom database — the tables from `defineDataModel()`.
|
|
624
|
+
* Distinct from `collections`/`rows`, which are Site Data. See
|
|
625
|
+
* {@link BiabDevDataModelClient}.
|
|
626
|
+
*/
|
|
627
|
+
readonly dataModel: BiabDevDataModelClient;
|
|
628
|
+
/**
|
|
629
|
+
* Read your site's visitor counts back (the counterpart to the
|
|
630
|
+
* `<BIABAnalytics/>` tracker, which only records). See
|
|
631
|
+
* {@link BiabDevAnalyticsClient}.
|
|
632
|
+
*/
|
|
633
|
+
readonly analytics: BiabDevAnalyticsClient;
|
|
634
|
+
/**
|
|
635
|
+
* The "Powered by BusinessDash" badge's policy + heartbeat. See
|
|
636
|
+
* {@link BiabDevAttributionClient}.
|
|
637
|
+
*/
|
|
638
|
+
readonly attribution: BiabDevAttributionClient;
|
|
639
|
+
readonly assets: BiabDevAssetsClient;
|
|
640
|
+
readonly actions: BiabDevActionsClient;
|
|
641
|
+
readonly followers: BiabDevFollowersClient;
|
|
642
|
+
readonly marketingPages: BiabDevMarketingPagesClient;
|
|
643
|
+
/**
|
|
644
|
+
* Schema-driven marketing flow (Phase 2). Pair with the SDK's
|
|
645
|
+
* `defineSiteMarketingSchema()` builder + the `biab-dev sync-schema`
|
|
646
|
+
* CLI for the authoring side; this client is the live-site read
|
|
647
|
+
* path (`getPageBundle`) and the upload entry point for the CLI.
|
|
648
|
+
*/
|
|
649
|
+
readonly marketing: BiabDevMarketingClient;
|
|
650
|
+
readonly parallelPages: BiabDevParallelPagesClient;
|
|
651
|
+
|
|
652
|
+
constructor(
|
|
653
|
+
readonly client: BiabDevClient,
|
|
654
|
+
readonly siteId: string,
|
|
655
|
+
) {
|
|
656
|
+
this.collections = new BiabDevCollectionsClient(client, siteId);
|
|
657
|
+
this.rows = new BiabDevRowsClient(client, siteId);
|
|
658
|
+
this.dataModel = new BiabDevDataModelClient(client, siteId);
|
|
659
|
+
this.analytics = new BiabDevAnalyticsClient(client, siteId);
|
|
660
|
+
this.attribution = new BiabDevAttributionClient(client, siteId);
|
|
661
|
+
this.assets = new BiabDevAssetsClient(client, siteId);
|
|
662
|
+
this.actions = new BiabDevActionsClient(client, siteId);
|
|
663
|
+
this.followers = new BiabDevFollowersClient(client, siteId);
|
|
664
|
+
this.marketingPages = new BiabDevMarketingPagesClient(client, siteId);
|
|
665
|
+
this.marketing = new BiabDevMarketingClient(client, siteId);
|
|
666
|
+
this.parallelPages = new BiabDevParallelPagesClient(client, siteId);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* The org's storefront branding — logo + favicon (site static media) plus
|
|
671
|
+
* the org name + icon. Render the masthead/favicon/og without the org
|
|
672
|
+
* re-supplying assets. Available even when billing has lapsed.
|
|
673
|
+
*
|
|
674
|
+
* @example
|
|
675
|
+
* ```ts
|
|
676
|
+
* const b = await client.site(SITE_ID).branding();
|
|
677
|
+
* <img src={b.logoUrl ?? b.orgIcon ?? ""} alt={b.orgName ?? ""} />
|
|
678
|
+
* ```
|
|
679
|
+
*/
|
|
680
|
+
async branding(): Promise<SiteBranding> {
|
|
681
|
+
return this.client.request({
|
|
682
|
+
path: `sites/${encodeURIComponent(this.siteId)}/branding`,
|
|
683
|
+
responseSchema: siteBrandingSchema,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const parallelPageMetaSchema = z.object({
|
|
689
|
+
title: z.string(),
|
|
690
|
+
description: z.string(),
|
|
691
|
+
canonical: z.string().nullable(),
|
|
692
|
+
ogImage: z.string().nullable(),
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
export const parallelPagesListResponseSchema = z.object({
|
|
696
|
+
items: z.array(
|
|
697
|
+
z.object({
|
|
698
|
+
id: z.string(),
|
|
699
|
+
key: z.string(),
|
|
700
|
+
routePattern: z.string(),
|
|
701
|
+
variables: z.array(
|
|
702
|
+
z.object({
|
|
703
|
+
name: z.string(),
|
|
704
|
+
source: z.object({
|
|
705
|
+
section: z.string(),
|
|
706
|
+
path: z.string().optional(),
|
|
707
|
+
}),
|
|
708
|
+
slugField: z.string(),
|
|
709
|
+
tokenFields: z.array(z.string()),
|
|
710
|
+
}),
|
|
711
|
+
),
|
|
712
|
+
meta: z.object({
|
|
713
|
+
title: z.string(),
|
|
714
|
+
description: z.string(),
|
|
715
|
+
canonical: z.string().optional(),
|
|
716
|
+
ogImage: z.string().optional(),
|
|
717
|
+
}),
|
|
718
|
+
templateRef: z.string(),
|
|
719
|
+
source: z.enum(["sdk", "site_builder"]),
|
|
720
|
+
crawlRule: z.enum(["inherit", "index", "noindex"]),
|
|
721
|
+
isActive: z.boolean(),
|
|
722
|
+
}),
|
|
723
|
+
),
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
export const parallelPagesVariantsResponseSchema = z.object({
|
|
727
|
+
variants: z.array(z.record(z.string(), z.string())),
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
export const parallelPagesRenderResponseSchema = z.object({
|
|
731
|
+
key: z.string(),
|
|
732
|
+
routePattern: z.string(),
|
|
733
|
+
slugs: z.record(z.string(), z.string()),
|
|
734
|
+
meta: parallelPageMetaSchema,
|
|
735
|
+
body: z.unknown().nullable(),
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
export type ParallelPagesListResponse = z.infer<
|
|
739
|
+
typeof parallelPagesListResponseSchema
|
|
740
|
+
>;
|
|
741
|
+
export type ParallelPagesVariantsResponse = z.infer<
|
|
742
|
+
typeof parallelPagesVariantsResponseSchema
|
|
743
|
+
>;
|
|
744
|
+
export type ParallelPagesRenderResponse = z.infer<
|
|
745
|
+
typeof parallelPagesRenderResponseSchema
|
|
746
|
+
>;
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Programmatic SEO ("parallel pages") client. Same shape across
|
|
750
|
+
* SDK-defined and site-builder-defined pages — both share the
|
|
751
|
+
* `org_parallel_pages` table on the server.
|
|
752
|
+
*
|
|
753
|
+
* Typical wiring in Next.js:
|
|
754
|
+
*
|
|
755
|
+
* export async function generateStaticParams() {
|
|
756
|
+
* const { variants } = await client
|
|
757
|
+
* .site(SITE_ID)
|
|
758
|
+
* .parallelPages
|
|
759
|
+
* .listVariants("repair-area");
|
|
760
|
+
* return variants;
|
|
761
|
+
* }
|
|
762
|
+
*
|
|
763
|
+
* export default async function Page({ params }) {
|
|
764
|
+
* const bundle = await client
|
|
765
|
+
* .site(SITE_ID)
|
|
766
|
+
* .parallelPages
|
|
767
|
+
* .render("repair-area", await params);
|
|
768
|
+
* return <RenderTemplate bundle={bundle} />;
|
|
769
|
+
* }
|
|
770
|
+
*/
|
|
771
|
+
export class BiabDevParallelPagesClient {
|
|
772
|
+
constructor(
|
|
773
|
+
private readonly client: BiabDevClient,
|
|
774
|
+
private readonly siteId: string,
|
|
775
|
+
) {}
|
|
776
|
+
|
|
777
|
+
async list(): Promise<ParallelPagesListResponse> {
|
|
778
|
+
return this.client.request({
|
|
779
|
+
path: `sites/${encodeURIComponent(this.siteId)}/parallel-pages`,
|
|
780
|
+
responseSchema: parallelPagesListResponseSchema,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
async listVariants(
|
|
785
|
+
key: string,
|
|
786
|
+
opts: { locale?: string } = {},
|
|
787
|
+
): Promise<ParallelPagesVariantsResponse> {
|
|
788
|
+
return this.client.request({
|
|
789
|
+
path: `sites/${encodeURIComponent(this.siteId)}/parallel-pages/${encodeURIComponent(key)}/variants`,
|
|
790
|
+
...(opts.locale ? { query: { locale: opts.locale } } : {}),
|
|
791
|
+
responseSchema: parallelPagesVariantsResponseSchema,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async render(
|
|
796
|
+
key: string,
|
|
797
|
+
slugs: Record<string, string>,
|
|
798
|
+
opts: { locale?: string } = {},
|
|
799
|
+
): Promise<ParallelPagesRenderResponse> {
|
|
800
|
+
return this.client.request({
|
|
801
|
+
path: `sites/${encodeURIComponent(this.siteId)}/parallel-pages/${encodeURIComponent(key)}/render`,
|
|
802
|
+
query: {
|
|
803
|
+
...slugs,
|
|
804
|
+
...(opts.locale ? { locale: opts.locale } : {}),
|
|
805
|
+
},
|
|
806
|
+
responseSchema: parallelPagesRenderResponseSchema,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* URL of the auto-generated sitemap. Consumer's `/sitemap.xml` route
|
|
812
|
+
* proxies straight through. Returns null when the client isn't
|
|
813
|
+
* configured with a base URL.
|
|
814
|
+
*/
|
|
815
|
+
sitemapUrl(): string {
|
|
816
|
+
return `sites/${encodeURIComponent(this.siteId)}/sitemap.xml`;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* URL of the auto-generated robots.txt. Consumer's `/robots.txt` route
|
|
821
|
+
* proxies straight through.
|
|
822
|
+
*/
|
|
823
|
+
robotsUrl(): string {
|
|
824
|
+
return `sites/${encodeURIComponent(this.siteId)}/robots.txt`;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Process-wide cache for the schema-driven marketing reads, keyed by
|
|
830
|
+
* the response's content-version fingerprint so cache hits are safe
|
|
831
|
+
* across SDK consumer re-renders. Bundle entries are short-lived (the
|
|
832
|
+
* server already sets `Cache-Control: s-maxage=60` so an upstream CDN
|
|
833
|
+
* does most of the work); the cache here is the SDK-process tier
|
|
834
|
+
* that prerendered Next builds reuse across all generated pages.
|
|
835
|
+
*/
|
|
836
|
+
const marketingBundleCache: Map<string, GetMarketingPageBundleResponse> =
|
|
837
|
+
new Map();
|
|
838
|
+
const marketingSchemaCache: Map<string, GetPublishedMarketingSchemaResponse> =
|
|
839
|
+
new Map();
|
|
840
|
+
const marketingLocalesCache: Map<string, GetMarketingLocalesResponse> =
|
|
841
|
+
new Map();
|
|
842
|
+
|
|
843
|
+
function bundleCacheKey(
|
|
844
|
+
siteId: string,
|
|
845
|
+
pageKey: string,
|
|
846
|
+
locale: string,
|
|
847
|
+
): string {
|
|
848
|
+
return `${siteId}/${pageKey}/${locale}`;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Manually invalidate marketing caches scoped to a site. Useful after a
|
|
853
|
+
* server-side mutation the SDK didn't initiate (e.g. an operator saved a
|
|
854
|
+
* section in the dashboard while the consumer's Next process is still
|
|
855
|
+
* warm).
|
|
856
|
+
*/
|
|
857
|
+
export function clearMarketingCache(siteId: string): void {
|
|
858
|
+
const prefix = `${siteId}/`;
|
|
859
|
+
for (const k of Array.from(marketingBundleCache.keys())) {
|
|
860
|
+
if (k.startsWith(prefix)) marketingBundleCache.delete(k);
|
|
861
|
+
}
|
|
862
|
+
marketingSchemaCache.delete(siteId);
|
|
863
|
+
marketingLocalesCache.delete(siteId);
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Live-site read client for the schema-driven marketing flow.
|
|
868
|
+
*
|
|
869
|
+
* Surfaces:
|
|
870
|
+
* - `getPageBundle({ pageKey, locale })` — every section for a page,
|
|
871
|
+
* tagged ok|error per section, plus SEO + availableLocales.
|
|
872
|
+
* - `getPublishedSchema()` — the live schema (returns `{ schema: null }`
|
|
873
|
+
* until the consumer's CLI has published one).
|
|
874
|
+
* - `getDraftSchema()` — what's currently staged (CLI-only path).
|
|
875
|
+
* - `publishDraftSchema(input)` — CLI upload. Idempotent (same
|
|
876
|
+
* checksum = `noop: true`).
|
|
877
|
+
* - `getLocales()` — enabled locales + brand-token map.
|
|
878
|
+
*/
|
|
879
|
+
export class BiabDevMarketingClient {
|
|
880
|
+
constructor(
|
|
881
|
+
private readonly client: BiabDevClient,
|
|
882
|
+
private readonly siteId: string,
|
|
883
|
+
) {}
|
|
884
|
+
|
|
885
|
+
async getPageBundle(
|
|
886
|
+
input: { pageKey?: string; locale?: string; reviewsLimit?: number } = {},
|
|
887
|
+
options: { skipCache?: boolean } = {},
|
|
888
|
+
): Promise<GetMarketingPageBundleResponse> {
|
|
889
|
+
const pageKey = input.pageKey ?? "home";
|
|
890
|
+
const locale = input.locale ?? "en";
|
|
891
|
+
// reviewsLimit sizes bundle.reviews.items; different sizes are different
|
|
892
|
+
// payloads, so it participates in the cache key.
|
|
893
|
+
const cacheKey =
|
|
894
|
+
bundleCacheKey(this.siteId, pageKey, locale) +
|
|
895
|
+
(input.reviewsLimit != null ? `/r${input.reviewsLimit}` : "");
|
|
896
|
+
if (!options.skipCache) {
|
|
897
|
+
const hit = marketingBundleCache.get(cacheKey);
|
|
898
|
+
if (hit) return hit;
|
|
899
|
+
}
|
|
900
|
+
const response = await this.client.request({
|
|
901
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing/bundle`,
|
|
902
|
+
query: { pageKey, locale, reviewsLimit: input.reviewsLimit },
|
|
903
|
+
responseSchema: getMarketingPageBundleResponseSchema,
|
|
904
|
+
});
|
|
905
|
+
marketingBundleCache.set(cacheKey, response);
|
|
906
|
+
return response;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
getPageBundleCached(
|
|
910
|
+
input: { pageKey?: string; locale?: string } = {},
|
|
911
|
+
): GetMarketingPageBundleResponse | null {
|
|
912
|
+
const pageKey = input.pageKey ?? "home";
|
|
913
|
+
const locale = input.locale ?? "en";
|
|
914
|
+
return (
|
|
915
|
+
marketingBundleCache.get(bundleCacheKey(this.siteId, pageKey, locale)) ??
|
|
916
|
+
null
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
async prefetchPageBundle(
|
|
921
|
+
input: { pageKey?: string; locale?: string } = {},
|
|
922
|
+
): Promise<GetMarketingPageBundleResponse> {
|
|
923
|
+
return this.getPageBundle(input);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async getPublishedSchema(
|
|
927
|
+
options: { skipCache?: boolean } = {},
|
|
928
|
+
): Promise<GetPublishedMarketingSchemaResponse> {
|
|
929
|
+
if (!options.skipCache) {
|
|
930
|
+
const hit = marketingSchemaCache.get(this.siteId);
|
|
931
|
+
if (hit) return hit;
|
|
932
|
+
}
|
|
933
|
+
const response = await this.client.request({
|
|
934
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing/published-schema`,
|
|
935
|
+
responseSchema: getPublishedMarketingSchemaResponseSchema,
|
|
936
|
+
});
|
|
937
|
+
marketingSchemaCache.set(this.siteId, response);
|
|
938
|
+
return response;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async getDraftSchema(): Promise<GetPublishedMarketingSchemaResponse> {
|
|
942
|
+
// Same response shape as published-schema, just from the other slot.
|
|
943
|
+
// Not cached — the CLI calls this once per upload cycle.
|
|
944
|
+
return this.client.request({
|
|
945
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing/draft-schema`,
|
|
946
|
+
responseSchema: getPublishedMarketingSchemaResponseSchema,
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async publishDraftSchema(
|
|
951
|
+
input: UpsertDraftMarketingSchemaInput,
|
|
952
|
+
): Promise<UpsertDraftMarketingSchemaResponse> {
|
|
953
|
+
const body = upsertDraftMarketingSchemaInputSchema.parse(input);
|
|
954
|
+
return this.client.request({
|
|
955
|
+
method: "POST",
|
|
956
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing/draft-schema`,
|
|
957
|
+
body,
|
|
958
|
+
responseSchema: upsertDraftMarketingSchemaResponseSchema,
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Bulk-upsert section values. Used by `biab-dev sync-content` to push
|
|
964
|
+
* local JSON content to BIAB. Server caps each request at 256 items;
|
|
965
|
+
* larger imports are batched by the CLI. Per-item failures are
|
|
966
|
+
* reported in the response (`results[].ok === false`) without aborting
|
|
967
|
+
* the rest of the batch.
|
|
968
|
+
*
|
|
969
|
+
* The CLI's `--lax` flag sets `validate: false` for first-time imports
|
|
970
|
+
* of legacy content that doesn't yet match the published schema —
|
|
971
|
+
* useful so writers can pull existing content into BIAB and fix
|
|
972
|
+
* validation errors in the dashboard incrementally.
|
|
973
|
+
*/
|
|
974
|
+
async publishSections(
|
|
975
|
+
input: PublishMarketingSectionsInput,
|
|
976
|
+
): Promise<PublishMarketingSectionsResponse> {
|
|
977
|
+
const body = publishMarketingSectionsInputSchema.parse(input);
|
|
978
|
+
return this.client.request({
|
|
979
|
+
method: "POST",
|
|
980
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing/sections`,
|
|
981
|
+
body,
|
|
982
|
+
responseSchema: publishMarketingSectionsResponseSchema,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async getLocales(
|
|
987
|
+
options: { skipCache?: boolean } = {},
|
|
988
|
+
): Promise<GetMarketingLocalesResponse> {
|
|
989
|
+
if (!options.skipCache) {
|
|
990
|
+
const hit = marketingLocalesCache.get(this.siteId);
|
|
991
|
+
if (hit) return hit;
|
|
992
|
+
}
|
|
993
|
+
const response = await this.client.request({
|
|
994
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing/locales`,
|
|
995
|
+
responseSchema: getMarketingLocalesResponseSchema,
|
|
996
|
+
});
|
|
997
|
+
marketingLocalesCache.set(this.siteId, response);
|
|
998
|
+
return response;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/**
|
|
1003
|
+
* Process-wide cache for marketing-page payloads, keyed by `(siteId, pageKey)`.
|
|
1004
|
+
* Shared across `BiabDevMarketingPagesClient` instances so that
|
|
1005
|
+
* `prefetch(pageKey)` warms the same store `get(pageKey)` reads on demand.
|
|
1006
|
+
*
|
|
1007
|
+
* Lives on a module-level `Map` rather than instance state because consumers
|
|
1008
|
+
* commonly create transient `client.site(siteId)` proxies inside React
|
|
1009
|
+
* components — instance-level caching would evict on every re-render.
|
|
1010
|
+
*/
|
|
1011
|
+
const marketingPageCache: Map<string, GetSiteMarketingPageResponse> = new Map();
|
|
1012
|
+
|
|
1013
|
+
function marketingPageCacheKey(siteId: string, pageKey: string): string {
|
|
1014
|
+
return `${siteId} ${pageKey}`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Manually clear the SDK's cache for `(siteId, pageKey)` (or the whole
|
|
1019
|
+
* site when `pageKey` is omitted). Useful after a server-side mutation
|
|
1020
|
+
* that the SDK didn't initiate, or in tests.
|
|
1021
|
+
*/
|
|
1022
|
+
export function clearMarketingPageCache(
|
|
1023
|
+
siteId: string,
|
|
1024
|
+
pageKey?: string,
|
|
1025
|
+
): void {
|
|
1026
|
+
if (pageKey) {
|
|
1027
|
+
marketingPageCache.delete(marketingPageCacheKey(siteId, pageKey));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const prefix = `${siteId} `;
|
|
1031
|
+
for (const key of Array.from(marketingPageCache.keys())) {
|
|
1032
|
+
if (key.startsWith(prefix)) marketingPageCache.delete(key);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Read-only access to the host's `org_site_marketing_pages` table — the JSON
|
|
1038
|
+
* documents the Site Builder writes for marketing landing pages (e.g. the
|
|
1039
|
+
* `home` page rendered by `custom-demo`).
|
|
1040
|
+
*
|
|
1041
|
+
* Requires the `collections:read` scope on the API key (the same scope used
|
|
1042
|
+
* for collection reads, so existing keys created from the host's Site Builder
|
|
1043
|
+
* → Developer panel work without re-issuing).
|
|
1044
|
+
*
|
|
1045
|
+
* The client transparently caches per-page responses across instances; pair
|
|
1046
|
+
* `list()` (no payloads) with `get(pageKey)` / `prefetch(pageKey)` to load
|
|
1047
|
+
* pages on demand instead of paying the full-content cost up front.
|
|
1048
|
+
*/
|
|
1049
|
+
export class BiabDevMarketingPagesClient {
|
|
1050
|
+
constructor(
|
|
1051
|
+
private readonly client: BiabDevClient,
|
|
1052
|
+
private readonly siteId: string,
|
|
1053
|
+
) {}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Payload-free directory of every marketing page for the site. Use this
|
|
1057
|
+
* to render a navigation index without loading any content; call
|
|
1058
|
+
* {@link get} or {@link prefetch} when the visitor selects a page.
|
|
1059
|
+
*/
|
|
1060
|
+
async list(): Promise<ListSiteMarketingPagesResponse> {
|
|
1061
|
+
return this.client.request({
|
|
1062
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing-pages`,
|
|
1063
|
+
responseSchema: listSiteMarketingPagesResponseSchema,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/**
|
|
1068
|
+
* Fetches a single marketing page payload by `pageKey` (defaults to `"home"`).
|
|
1069
|
+
* Returns `{ page: null }` when the host has not published one yet — callers
|
|
1070
|
+
* typically render a static fallback in that case.
|
|
1071
|
+
*
|
|
1072
|
+
* Reads from the shared cache when present (warmed by {@link prefetch} or
|
|
1073
|
+
* a previous `get`). Pass `{ skipCache: true }` to force a network round-trip
|
|
1074
|
+
* (e.g. after a save the SDK didn't make itself).
|
|
1075
|
+
*/
|
|
1076
|
+
async get(
|
|
1077
|
+
pageKey = "home",
|
|
1078
|
+
options: { skipCache?: boolean } = {},
|
|
1079
|
+
): Promise<GetSiteMarketingPageResponse> {
|
|
1080
|
+
const cacheKey = marketingPageCacheKey(this.siteId, pageKey);
|
|
1081
|
+
if (!options.skipCache) {
|
|
1082
|
+
const hit = marketingPageCache.get(cacheKey);
|
|
1083
|
+
if (hit) return hit;
|
|
1084
|
+
}
|
|
1085
|
+
const response = await this.client.request({
|
|
1086
|
+
path: `sites/${encodeURIComponent(this.siteId)}/marketing-pages/${encodeURIComponent(pageKey)}`,
|
|
1087
|
+
responseSchema: getSiteMarketingPageResponseSchema,
|
|
1088
|
+
});
|
|
1089
|
+
marketingPageCache.set(cacheKey, response);
|
|
1090
|
+
return response;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Read the cached value for `pageKey` without making a network call.
|
|
1095
|
+
* Returns `null` when the entry hasn't been fetched yet — callers can
|
|
1096
|
+
* fall back to a synchronous static default while {@link prefetch}
|
|
1097
|
+
* warms the cache in the background.
|
|
1098
|
+
*/
|
|
1099
|
+
getCached(pageKey: string): GetSiteMarketingPageResponse | null {
|
|
1100
|
+
return (
|
|
1101
|
+
marketingPageCache.get(marketingPageCacheKey(this.siteId, pageKey)) ??
|
|
1102
|
+
null
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Warm the SDK cache for `pageKey` without rendering anything yet.
|
|
1108
|
+
* Common pattern: call from a `<Link onPointerEnter>` / `onFocus` so
|
|
1109
|
+
* the next page is already loaded by the time the visitor clicks.
|
|
1110
|
+
* Subsequent `get(pageKey)` calls resolve from cache.
|
|
1111
|
+
*/
|
|
1112
|
+
async prefetch(pageKey = "home"): Promise<GetSiteMarketingPageResponse> {
|
|
1113
|
+
return this.get(pageKey);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export class BiabDevCollectionsClient {
|
|
1118
|
+
constructor(
|
|
1119
|
+
private readonly client: BiabDevClient,
|
|
1120
|
+
private readonly siteId: string,
|
|
1121
|
+
) {}
|
|
1122
|
+
|
|
1123
|
+
async list(): Promise<ListCollectionsResponse> {
|
|
1124
|
+
return this.client.request({
|
|
1125
|
+
path: `sites/${encodeURIComponent(this.siteId)}/collections`,
|
|
1126
|
+
responseSchema: listCollectionsResponseSchema,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
async create(
|
|
1131
|
+
input: CreateCollectionInput,
|
|
1132
|
+
): Promise<CreateCollectionResponse> {
|
|
1133
|
+
const body = createCollectionInputSchema.parse(input);
|
|
1134
|
+
|
|
1135
|
+
return this.client.request({
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
path: `sites/${encodeURIComponent(this.siteId)}/collections`,
|
|
1138
|
+
body,
|
|
1139
|
+
responseSchema: createCollectionResponseSchema,
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
async get(slug: string): Promise<GetCollectionResponse> {
|
|
1144
|
+
return this.client.request({
|
|
1145
|
+
path: `sites/${encodeURIComponent(this.siteId)}/collections/${encodeURIComponent(slug)}`,
|
|
1146
|
+
responseSchema: getCollectionResponseSchema,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
export class BiabDevRowsClient {
|
|
1152
|
+
constructor(
|
|
1153
|
+
private readonly client: BiabDevClient,
|
|
1154
|
+
private readonly siteId: string,
|
|
1155
|
+
) {}
|
|
1156
|
+
|
|
1157
|
+
async list(
|
|
1158
|
+
collectionSlug: string,
|
|
1159
|
+
input: ListRowsInput = {},
|
|
1160
|
+
): Promise<ListRowsResponse> {
|
|
1161
|
+
const query = listRowsInputSchema.parse(input);
|
|
1162
|
+
|
|
1163
|
+
return this.client.request({
|
|
1164
|
+
path: `sites/${encodeURIComponent(this.siteId)}/collections/${encodeURIComponent(collectionSlug)}/rows`,
|
|
1165
|
+
query,
|
|
1166
|
+
responseSchema: listRowsResponseSchema,
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async upsert(
|
|
1171
|
+
collectionSlug: string,
|
|
1172
|
+
input: UpsertRowInput,
|
|
1173
|
+
): Promise<UpsertRowResponse> {
|
|
1174
|
+
const body = upsertRowInputSchema.parse(input);
|
|
1175
|
+
|
|
1176
|
+
return this.client.request({
|
|
1177
|
+
method: "POST",
|
|
1178
|
+
path: `sites/${encodeURIComponent(this.siteId)}/collections/${encodeURIComponent(collectionSlug)}/rows`,
|
|
1179
|
+
body,
|
|
1180
|
+
responseSchema: upsertRowResponseSchema,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async query(
|
|
1185
|
+
collectionSlug: string,
|
|
1186
|
+
input: QueryRowsInput,
|
|
1187
|
+
): Promise<QueryRowsResponse> {
|
|
1188
|
+
const body = queryRowsInputSchema.parse(input);
|
|
1189
|
+
|
|
1190
|
+
return this.client.request({
|
|
1191
|
+
method: "POST",
|
|
1192
|
+
path: `sites/${encodeURIComponent(this.siteId)}/collections/${encodeURIComponent(collectionSlug)}/query`,
|
|
1193
|
+
body,
|
|
1194
|
+
responseSchema: queryRowsResponseSchema,
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Read rows out of the org's CUSTOM DATABASE — the tables you declare with
|
|
1201
|
+
* `defineDataModel()` and push with `biab-dev sync-data-model`.
|
|
1202
|
+
*
|
|
1203
|
+
* ⚠️ Not the same thing as `.collections` / `.rows`. Those read Site Data (the
|
|
1204
|
+
* site builder's tables, `site_data_rows`). Rows seeded with
|
|
1205
|
+
* `biab-dev sync-records` live in `org_object_records` and are reachable only
|
|
1206
|
+
* through here — no `collections:*` call will ever return one.
|
|
1207
|
+
*
|
|
1208
|
+
* Requires the `metadata:read_records` scope. What a key can actually read is
|
|
1209
|
+
* decided per object by its `visibility`:
|
|
1210
|
+
*
|
|
1211
|
+
* - `private` (default) — a SECRET key only. Call from a server component, a
|
|
1212
|
+
* route handler, or at build time; a `pk_…` key gets "not found".
|
|
1213
|
+
* - `authenticated` — a secret key, or a publishable key carrying a signed-in
|
|
1214
|
+
* tenant session.
|
|
1215
|
+
* - `public` — any key, including a publishable (`pk_…`) one in page JS.
|
|
1216
|
+
*
|
|
1217
|
+
* So a table you declared `public` is readable straight from the browser; a
|
|
1218
|
+
* private one never is. Open a table by declaring `visibility` in your model.
|
|
1219
|
+
*
|
|
1220
|
+
* ```ts
|
|
1221
|
+
* const result = await biab.site(siteId).dataModel.listRecords({
|
|
1222
|
+
* object: TECH_OBJECT_ID, // the universalIdentifier from your config
|
|
1223
|
+
* });
|
|
1224
|
+
* if (!result.available) return <UpgradeCard feature={result.requiredFeature} />;
|
|
1225
|
+
* for (const tech of result.records) {
|
|
1226
|
+
* tech.fields.name; // scalar values, keyed by field name
|
|
1227
|
+
* tech.relations.icon; // [{ recordId, object }] — links, not embedded rows
|
|
1228
|
+
* }
|
|
1229
|
+
* ```
|
|
1230
|
+
*/
|
|
1231
|
+
export class BiabDevDataModelClient {
|
|
1232
|
+
constructor(
|
|
1233
|
+
private readonly client: BiabDevClient,
|
|
1234
|
+
private readonly siteId: string,
|
|
1235
|
+
) {}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* One page of an object's rows, newest first, with relations expanded.
|
|
1239
|
+
*
|
|
1240
|
+
* Relations come back as LINKS (`{ recordId, object }`), not embedded rows —
|
|
1241
|
+
* fetch the targets with a follow-up call for the object they point at.
|
|
1242
|
+
* Every declared relation field is present; `[]` means "no links yet", which
|
|
1243
|
+
* is deliberately distinguishable from a field that wasn't returned.
|
|
1244
|
+
*
|
|
1245
|
+
* Paginate by passing the previous response's `nextCursor` back as `cursor`
|
|
1246
|
+
* until it comes back `null`. The cursor is opaque — don't build one.
|
|
1247
|
+
*/
|
|
1248
|
+
async listRecords(
|
|
1249
|
+
input: ListDataModelRecordsInput,
|
|
1250
|
+
): Promise<ListDataModelRecordsResponse> {
|
|
1251
|
+
const query = listDataModelRecordsInputSchema.parse(input);
|
|
1252
|
+
|
|
1253
|
+
return this.client.request({
|
|
1254
|
+
path: `sites/${encodeURIComponent(this.siteId)}/data-model/records`,
|
|
1255
|
+
query,
|
|
1256
|
+
responseSchema: listDataModelRecordsResponseSchema,
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* Every row of an object, walking `nextCursor` to exhaustion.
|
|
1262
|
+
*
|
|
1263
|
+
* For build-time rendering of a table you own and whose size you know. It is
|
|
1264
|
+
* an unbounded loop over your data by definition — for anything visitor-facing
|
|
1265
|
+
* or open-ended, page with {@link listRecords} instead.
|
|
1266
|
+
*
|
|
1267
|
+
* Returns the plan-gate shape unchanged if the org lacks the entitlement, so
|
|
1268
|
+
* the same `available` branch works for both.
|
|
1269
|
+
*/
|
|
1270
|
+
async listAllRecords(
|
|
1271
|
+
input: Omit<ListDataModelRecordsInput, "cursor">,
|
|
1272
|
+
): Promise<ListDataModelRecordsResponse> {
|
|
1273
|
+
const records: DataModelRecord[] = [];
|
|
1274
|
+
let cursor: string | undefined;
|
|
1275
|
+
|
|
1276
|
+
do {
|
|
1277
|
+
const page = await this.listRecords({ ...input, cursor });
|
|
1278
|
+
if (!page.available) return page;
|
|
1279
|
+
records.push(...page.records);
|
|
1280
|
+
cursor = page.nextCursor ?? undefined;
|
|
1281
|
+
} while (cursor);
|
|
1282
|
+
|
|
1283
|
+
return { available: true, records, nextCursor: null };
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Read your site's visitor counts back.
|
|
1289
|
+
*
|
|
1290
|
+
* The `<BIABAnalytics/>` tracker (`@businessdash/sdk/react-analytics`) RECORDS
|
|
1291
|
+
* pageviews; this READS the totals, so you can put a number on the page instead
|
|
1292
|
+
* of only feeding the dashboard.
|
|
1293
|
+
*
|
|
1294
|
+
* ```ts
|
|
1295
|
+
* // Site-wide, all time
|
|
1296
|
+
* const all = await biab.site(siteId).analytics.pageViews();
|
|
1297
|
+
* if (all.available) console.log(`${all.total.views} views`);
|
|
1298
|
+
*
|
|
1299
|
+
* // One page, last 30 days
|
|
1300
|
+
* const home = await biab.site(siteId).analytics.pageViews({ paths: ['/'], days: 30 });
|
|
1301
|
+
*
|
|
1302
|
+
* // A batch of pages in one round-trip
|
|
1303
|
+
* const list = await biab.site(siteId).analytics.pageViews({
|
|
1304
|
+
* paths: ['/tech/typescript', '/tech/rust'],
|
|
1305
|
+
* });
|
|
1306
|
+
* // list.paths → [{ path, views, visitors }, …], every requested path present
|
|
1307
|
+
* ```
|
|
1308
|
+
*
|
|
1309
|
+
* `visitors` approximates unique visitors (distinct daily-rotating anon id).
|
|
1310
|
+
* Requires the `analytics.web` entitlement — an org without it gets
|
|
1311
|
+
* `{ available: false }`, so branch on `.available`.
|
|
1312
|
+
*/
|
|
1313
|
+
export class BiabDevAnalyticsClient {
|
|
1314
|
+
constructor(
|
|
1315
|
+
private readonly client: BiabDevClient,
|
|
1316
|
+
private readonly siteId: string,
|
|
1317
|
+
) {}
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* Pageview + visitor counts. Pass `paths` for a per-path breakdown (plus a
|
|
1321
|
+
* combined `total`), or omit them for a site-wide `total`. `days` windows the
|
|
1322
|
+
* count; omit for all-time.
|
|
1323
|
+
*/
|
|
1324
|
+
async pageViews(input: PageViewsInput = {}): Promise<PageViewsResponse> {
|
|
1325
|
+
const { paths, days } = pageViewsInputSchema.parse(input);
|
|
1326
|
+
return this.client.request({
|
|
1327
|
+
path: `sites/${encodeURIComponent(this.siteId)}/analytics/views`,
|
|
1328
|
+
query: {
|
|
1329
|
+
// `path` repeats once per requested path; the route reads getAll.
|
|
1330
|
+
...(paths && paths.length > 0 ? { path: paths } : {}),
|
|
1331
|
+
...(days !== undefined ? { days } : {}),
|
|
1332
|
+
},
|
|
1333
|
+
responseSchema: pageViewsResponseSchema,
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Which AI crawlers (GPTBot, ClaudeBot, PerplexityBot, …) actually fetched
|
|
1339
|
+
* this site's pages: fetch counts, distinct pages read, vendor, intent
|
|
1340
|
+
* (training / search / live-assistant), and when each last came.
|
|
1341
|
+
*
|
|
1342
|
+
* First-party AEO visibility — recorded when crawlers hit the tracker (the
|
|
1343
|
+
* assistant browsing agents run JS) and when your server reports the rest
|
|
1344
|
+
* via `reportAiCrawlerHit` from `@businessdash/sdk/analytics-server`. Defaults
|
|
1345
|
+
* to a 30-day window.
|
|
1346
|
+
*
|
|
1347
|
+
* ```ts
|
|
1348
|
+
* const ai = await biab.site(siteId).analytics.aiVisibility({ days: 30 });
|
|
1349
|
+
* if (ai.available) {
|
|
1350
|
+
* for (const c of ai.crawlers)
|
|
1351
|
+
* console.log(`${c.name}: ${c.fetches} fetches over ${c.distinctPages} pages`);
|
|
1352
|
+
* }
|
|
1353
|
+
* ```
|
|
1354
|
+
*/
|
|
1355
|
+
async aiVisibility(
|
|
1356
|
+
input: AiVisibilityInput = {},
|
|
1357
|
+
): Promise<AiVisibilityResponse> {
|
|
1358
|
+
const { days } = aiVisibilityInputSchema.parse(input);
|
|
1359
|
+
return this.client.request({
|
|
1360
|
+
path: `sites/${encodeURIComponent(this.siteId)}/analytics/ai-crawlers`,
|
|
1361
|
+
query: { ...(days !== undefined ? { days } : {}) },
|
|
1362
|
+
responseSchema: aiVisibilityResponseSchema,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* The "Powered by BusinessDash" attribution badge's one narrow function.
|
|
1369
|
+
*
|
|
1370
|
+
* `<BusinessDashFooter/>` uses this to (a) read whether the badge is required on
|
|
1371
|
+
* this plan and (b) optionally report that it rendered. Both are scoped to your
|
|
1372
|
+
* own site with a publishable key.
|
|
1373
|
+
*
|
|
1374
|
+
* ⚠️ The heartbeat is a courtesy signal, not proof of anything — BusinessDash
|
|
1375
|
+
* verifies the badge by fetching your live domain, not by trusting this call. So
|
|
1376
|
+
* don't build logic on the heartbeat; do honor `policy().required`.
|
|
1377
|
+
*/
|
|
1378
|
+
export class BiabDevAttributionClient {
|
|
1379
|
+
constructor(
|
|
1380
|
+
private readonly client: BiabDevClient,
|
|
1381
|
+
private readonly siteId: string,
|
|
1382
|
+
) {}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* This site's badge policy: `{ siteId, required }`. `required` is `true` on
|
|
1386
|
+
* plans that can't remove the badge (launch), `false` otherwise. Build the
|
|
1387
|
+
* badge's `?ref=<siteId>` backlink from `siteId`.
|
|
1388
|
+
*/
|
|
1389
|
+
async policy(): Promise<AttributionPolicy> {
|
|
1390
|
+
return this.client.request({
|
|
1391
|
+
path: `sites/${encodeURIComponent(this.siteId)}/attribution`,
|
|
1392
|
+
responseSchema: attributionPolicySchema,
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Report that the badge rendered on `url`. Best-effort and untrusted — it
|
|
1398
|
+
* never throws for a bad URL (you get `{ recorded: false }`), and it's safe to
|
|
1399
|
+
* fire-and-forget. Don't await it in a render path.
|
|
1400
|
+
*/
|
|
1401
|
+
async heartbeat(
|
|
1402
|
+
input: AttributionHeartbeatInput,
|
|
1403
|
+
): Promise<AttributionHeartbeatResponse> {
|
|
1404
|
+
const body = attributionHeartbeatInputSchema.parse(input);
|
|
1405
|
+
return this.client.request({
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
path: `sites/${encodeURIComponent(this.siteId)}/attribution/heartbeat`,
|
|
1408
|
+
body,
|
|
1409
|
+
responseSchema: attributionHeartbeatResponseSchema,
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export class BiabDevAssetsClient {
|
|
1415
|
+
constructor(
|
|
1416
|
+
private readonly client: BiabDevClient,
|
|
1417
|
+
private readonly siteId: string,
|
|
1418
|
+
) {}
|
|
1419
|
+
|
|
1420
|
+
async create(input: SiteAssetInput): Promise<UploadSiteAssetResponse> {
|
|
1421
|
+
const body = siteAssetInputSchema.parse(input);
|
|
1422
|
+
|
|
1423
|
+
return this.client.request({
|
|
1424
|
+
method: "POST",
|
|
1425
|
+
path: `sites/${encodeURIComponent(this.siteId)}/assets`,
|
|
1426
|
+
body,
|
|
1427
|
+
responseSchema: uploadSiteAssetResponseSchema,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
export class BiabDevActionsClient {
|
|
1433
|
+
constructor(
|
|
1434
|
+
private readonly client: BiabDevClient,
|
|
1435
|
+
private readonly siteId: string,
|
|
1436
|
+
) {}
|
|
1437
|
+
|
|
1438
|
+
async run(
|
|
1439
|
+
actionName: string,
|
|
1440
|
+
input: ExecuteActionInput = { payload: {} },
|
|
1441
|
+
): Promise<ExecuteActionResponse> {
|
|
1442
|
+
const body = executeActionInputSchema.parse(input);
|
|
1443
|
+
|
|
1444
|
+
return this.client.request({
|
|
1445
|
+
method: "POST",
|
|
1446
|
+
path: `sites/${encodeURIComponent(this.siteId)}/actions/${encodeURIComponent(actionName)}`,
|
|
1447
|
+
body,
|
|
1448
|
+
responseSchema: executeActionResponseSchema,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
export class BiabDevEmailTemplatesClient {
|
|
1454
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
1455
|
+
|
|
1456
|
+
async list(
|
|
1457
|
+
input: ListEmailTemplatesInput = {},
|
|
1458
|
+
): Promise<ListEmailTemplatesResponse> {
|
|
1459
|
+
const query = listEmailTemplatesInputSchema.parse(input);
|
|
1460
|
+
|
|
1461
|
+
return this.client.request({
|
|
1462
|
+
path: "email/templates",
|
|
1463
|
+
query,
|
|
1464
|
+
responseSchema: listEmailTemplatesResponseSchema,
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
async get(templateId: number): Promise<GetEmailTemplateResponse> {
|
|
1469
|
+
return this.client.request({
|
|
1470
|
+
path: `email/templates/${templateId}`,
|
|
1471
|
+
responseSchema: getEmailTemplateResponseSchema,
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
async create(
|
|
1476
|
+
input: CreateEmailTemplateInput,
|
|
1477
|
+
): Promise<CreateEmailTemplateResponse> {
|
|
1478
|
+
const body = createEmailTemplateInputSchema.parse(input);
|
|
1479
|
+
|
|
1480
|
+
return this.client.request({
|
|
1481
|
+
method: "POST",
|
|
1482
|
+
path: "email/templates",
|
|
1483
|
+
body,
|
|
1484
|
+
responseSchema: createEmailTemplateResponseSchema,
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
async createVersion(
|
|
1489
|
+
templateId: number,
|
|
1490
|
+
input: CreateEmailTemplateVersionInput,
|
|
1491
|
+
): Promise<CreateEmailTemplateVersionResponse> {
|
|
1492
|
+
const body = createEmailTemplateVersionInputSchema.parse(input);
|
|
1493
|
+
|
|
1494
|
+
return this.client.request({
|
|
1495
|
+
method: "POST",
|
|
1496
|
+
path: `email/templates/${templateId}/versions`,
|
|
1497
|
+
body,
|
|
1498
|
+
responseSchema: createEmailTemplateVersionResponseSchema,
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
export class BiabDevFollowersClient {
|
|
1504
|
+
constructor(
|
|
1505
|
+
private readonly client: BiabDevClient,
|
|
1506
|
+
private readonly siteId: string,
|
|
1507
|
+
) {}
|
|
1508
|
+
|
|
1509
|
+
private actionPath(action: string) {
|
|
1510
|
+
return `sites/${encodeURIComponent(this.siteId)}/actions/${encodeURIComponent(action)}`;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async join(input: FollowerJoinInput): Promise<FollowerJoinResponse> {
|
|
1514
|
+
const payload = followerJoinInputSchema.parse(input);
|
|
1515
|
+
const body = executeActionInputSchema.parse({ payload });
|
|
1516
|
+
const response = await this.client.request({
|
|
1517
|
+
method: "POST",
|
|
1518
|
+
path: this.actionPath("followers.join"),
|
|
1519
|
+
body,
|
|
1520
|
+
responseSchema: executeActionResponseSchema,
|
|
1521
|
+
});
|
|
1522
|
+
return followerJoinResponseSchema.parse(response.result);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
async me(input: FollowerMeInput): Promise<FollowerMeResponse> {
|
|
1526
|
+
const payload = followerMeInputSchema.parse(input);
|
|
1527
|
+
const body = executeActionInputSchema.parse({ payload });
|
|
1528
|
+
const response = await this.client.request({
|
|
1529
|
+
method: "POST",
|
|
1530
|
+
path: this.actionPath("followers.me"),
|
|
1531
|
+
body,
|
|
1532
|
+
responseSchema: executeActionResponseSchema,
|
|
1533
|
+
});
|
|
1534
|
+
return followerMeResponseSchema.parse(response.result);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
async edit(input: FollowerEditInput): Promise<FollowerEditResponse> {
|
|
1538
|
+
const payload = followerEditInputSchema.parse(input);
|
|
1539
|
+
const body = executeActionInputSchema.parse({ payload });
|
|
1540
|
+
const response = await this.client.request({
|
|
1541
|
+
method: "POST",
|
|
1542
|
+
path: this.actionPath("followers.edit"),
|
|
1543
|
+
body,
|
|
1544
|
+
responseSchema: executeActionResponseSchema,
|
|
1545
|
+
});
|
|
1546
|
+
return followerEditResponseSchema.parse(response.result);
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
async leave(input: FollowerLeaveInput): Promise<FollowerLeaveResponse> {
|
|
1550
|
+
const payload = followerLeaveInputSchema.parse(input);
|
|
1551
|
+
const body = executeActionInputSchema.parse({ payload });
|
|
1552
|
+
const response = await this.client.request({
|
|
1553
|
+
method: "POST",
|
|
1554
|
+
path: this.actionPath("followers.leave"),
|
|
1555
|
+
body,
|
|
1556
|
+
responseSchema: executeActionResponseSchema,
|
|
1557
|
+
});
|
|
1558
|
+
return followerLeaveResponseSchema.parse(response.result);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
export class BiabDevDashboardClient {
|
|
1563
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
1564
|
+
|
|
1565
|
+
async createSession(): Promise<DashboardSessionResponse> {
|
|
1566
|
+
return this.client.request({
|
|
1567
|
+
method: "POST",
|
|
1568
|
+
path: "dashboard/session",
|
|
1569
|
+
responseSchema: dashboardSessionResponseSchema,
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
export class BiabDevStorefrontClient {
|
|
1575
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Legacy embed-session mint (kept for iframe-widget consumers).
|
|
1579
|
+
* The programmatic methods below let consumer sites render their
|
|
1580
|
+
* own native product catalog using BIAB as the data layer.
|
|
1581
|
+
*/
|
|
1582
|
+
async createSession(
|
|
1583
|
+
input?: StorefrontSessionInput,
|
|
1584
|
+
): Promise<StorefrontSessionResponse> {
|
|
1585
|
+
const body = storefrontSessionInputSchema.parse(input ?? undefined);
|
|
1586
|
+
return this.client.request({
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
path: "storefront/session",
|
|
1589
|
+
body: body ?? {},
|
|
1590
|
+
responseSchema: storefrontSessionResponseSchema,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
/**
|
|
1595
|
+
* Paginated list of live products for the calling org's storefront.
|
|
1596
|
+
* Cursor is an opaque integer offset — pass back the response's
|
|
1597
|
+
* `nextCursor` to fetch the next page. Filter by category via the
|
|
1598
|
+
* `categoryId` option.
|
|
1599
|
+
*/
|
|
1600
|
+
async listProducts(input?: {
|
|
1601
|
+
limit?: number;
|
|
1602
|
+
cursor?: number | null;
|
|
1603
|
+
categoryId?: string;
|
|
1604
|
+
}): Promise<StorefrontListProductsResponse> {
|
|
1605
|
+
const query: Record<string, string | number | undefined> = {};
|
|
1606
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
1607
|
+
if (input?.cursor != null) query["cursor"] = input.cursor;
|
|
1608
|
+
if (input?.categoryId) query["categoryId"] = input.categoryId;
|
|
1609
|
+
return this.client.request({
|
|
1610
|
+
path: "storefront/products",
|
|
1611
|
+
query,
|
|
1612
|
+
responseSchema: storefrontListProductsResponseSchema,
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
/**
|
|
1617
|
+
* Filterable listing grid with facets — the data behind a full shop page.
|
|
1618
|
+
* Returns enriched per-card price/ratings/badges plus `categoryCounts` and
|
|
1619
|
+
* the catalog-wide `priceRange` for a sidebar. Supports `search`,
|
|
1620
|
+
* `categoryId`, `minPriceCents`/`maxPriceCents`, `minRating` (1–5), and
|
|
1621
|
+
* `sort` (featured | newest | price-asc | price-desc | rating-desc).
|
|
1622
|
+
*/
|
|
1623
|
+
async listProductsWithMeta(input?: {
|
|
1624
|
+
search?: string;
|
|
1625
|
+
categoryId?: string;
|
|
1626
|
+
minPriceCents?: number;
|
|
1627
|
+
maxPriceCents?: number;
|
|
1628
|
+
minRating?: number;
|
|
1629
|
+
sort?: StorefrontSort;
|
|
1630
|
+
limit?: number;
|
|
1631
|
+
/** Dev-only: also surface pending (non-live) products. Caller-gated. */
|
|
1632
|
+
preview?: boolean;
|
|
1633
|
+
}): Promise<StorefrontProductsWithMetaResponse> {
|
|
1634
|
+
const query: Record<string, string | number | undefined> = { meta: "1" };
|
|
1635
|
+
if (input?.search) query["search"] = input.search;
|
|
1636
|
+
if (input?.categoryId) query["categoryId"] = input.categoryId;
|
|
1637
|
+
if (input?.minPriceCents != null)
|
|
1638
|
+
query["minPriceCents"] = input.minPriceCents;
|
|
1639
|
+
if (input?.maxPriceCents != null)
|
|
1640
|
+
query["maxPriceCents"] = input.maxPriceCents;
|
|
1641
|
+
if (input?.minRating != null) query["minRating"] = input.minRating;
|
|
1642
|
+
if (input?.sort) query["sort"] = input.sort;
|
|
1643
|
+
if (input?.limit != null) query["limit"] = input.limit;
|
|
1644
|
+
if (input?.preview) query["preview"] = "1";
|
|
1645
|
+
return this.client.request({
|
|
1646
|
+
path: "storefront/products",
|
|
1647
|
+
query,
|
|
1648
|
+
responseSchema: storefrontProductsWithMetaResponseSchema,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/** Org-scoped product categories for a storefront sidebar. */
|
|
1653
|
+
async listCategories(): Promise<StorefrontCategoriesResponse> {
|
|
1654
|
+
return this.client.request({
|
|
1655
|
+
path: "storefront/categories",
|
|
1656
|
+
responseSchema: storefrontCategoriesResponseSchema,
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Full product detail with variants + per-image rows. Returns 404
|
|
1662
|
+
* via `BiabApiError` if the product isn't live or doesn't belong
|
|
1663
|
+
* to the org bound to the API key.
|
|
1664
|
+
*/
|
|
1665
|
+
async getProduct(productId: string): Promise<StorefrontProductDetail> {
|
|
1666
|
+
return this.client.request({
|
|
1667
|
+
path: `storefront/products/${encodeURIComponent(productId)}`,
|
|
1668
|
+
responseSchema: storefrontProductDetailSchema,
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
/** Approved reviews for a product + aggregate (`avgRating`, `totalCount`).
|
|
1673
|
+
* Cursor-paginated — pass back `nextCursor` for the next page. */
|
|
1674
|
+
async getProductReviews(
|
|
1675
|
+
productId: string,
|
|
1676
|
+
input?: { limit?: number; cursor?: number | null },
|
|
1677
|
+
): Promise<StorefrontProductReviewsResponse> {
|
|
1678
|
+
const query: Record<string, string | number | undefined> = {};
|
|
1679
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
1680
|
+
if (input?.cursor != null) query["cursor"] = input.cursor;
|
|
1681
|
+
return this.client.request({
|
|
1682
|
+
path: `storefront/products/${encodeURIComponent(productId)}/reviews`,
|
|
1683
|
+
query,
|
|
1684
|
+
responseSchema: storefrontProductReviewsResponseSchema,
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
/** "You may also like" — same-category / paired recommendations. */
|
|
1689
|
+
async getRelatedProducts(
|
|
1690
|
+
productId: string,
|
|
1691
|
+
input?: { limit?: number },
|
|
1692
|
+
): Promise<StorefrontRelatedProductsResponse> {
|
|
1693
|
+
const query: Record<string, string | number | undefined> = {};
|
|
1694
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
1695
|
+
return this.client.request({
|
|
1696
|
+
path: `storefront/products/${encodeURIComponent(productId)}/related`,
|
|
1697
|
+
query,
|
|
1698
|
+
responseSchema: storefrontRelatedProductsResponseSchema,
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
/** Companion / exclusive cross-sell addons for the "complete your X" rail. */
|
|
1703
|
+
async getProductAddons(productId: string): Promise<StorefrontAddonsResponse> {
|
|
1704
|
+
return this.client.request({
|
|
1705
|
+
path: `storefront/products/${encodeURIComponent(productId)}/addons`,
|
|
1706
|
+
responseSchema: storefrontAddonsResponseSchema,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
export class BiabDevCartClient {
|
|
1712
|
+
constructor(
|
|
1713
|
+
private readonly client: BiabDevClient,
|
|
1714
|
+
private readonly visitorToken: string | null = null,
|
|
1715
|
+
private readonly sessionToken: string | null = null,
|
|
1716
|
+
) {}
|
|
1717
|
+
|
|
1718
|
+
/**
|
|
1719
|
+
* Use an anonymous visitor token (UUID stored in the consumer's
|
|
1720
|
+
* own httpOnly cookie). Same shopper across page loads, no
|
|
1721
|
+
* sign-in required.
|
|
1722
|
+
*/
|
|
1723
|
+
forVisitor(visitorToken: string): BiabDevCartClient {
|
|
1724
|
+
return new BiabDevCartClient(this.client, visitorToken, null);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/**
|
|
1728
|
+
* Use a tenantAuth session token instead of a visitor token.
|
|
1729
|
+
* Carts are now keyed to the signed-in customer; persists
|
|
1730
|
+
* across devices.
|
|
1731
|
+
*/
|
|
1732
|
+
withSession(sessionToken: string): BiabDevCartClient {
|
|
1733
|
+
return new BiabDevCartClient(this.client, null, sessionToken);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
private headers(): HeadersInit {
|
|
1737
|
+
const out: Record<string, string> = {};
|
|
1738
|
+
if (this.visitorToken) out["X-BIAB-Cart-Visitor"] = this.visitorToken;
|
|
1739
|
+
if (this.sessionToken) out["X-BIAB-Session-Token"] = this.sessionToken;
|
|
1740
|
+
return out;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* Legacy embed-session mint (kept for iframe-widget consumers).
|
|
1745
|
+
* The programmatic methods below let the consumer site render
|
|
1746
|
+
* its own native cart UI.
|
|
1747
|
+
*/
|
|
1748
|
+
async createSession(): Promise<CartSessionResponse> {
|
|
1749
|
+
return this.client.request({
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
path: "cart/session",
|
|
1752
|
+
responseSchema: cartSessionResponseSchema,
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/**
|
|
1757
|
+
* Read the current cart snapshot — item count, items with
|
|
1758
|
+
* resolved product detail (name, image, variant title, unit
|
|
1759
|
+
* price), subtotal, currency. Auto-creates an empty cart row
|
|
1760
|
+
* the first time it's called for a new visitor/session, so
|
|
1761
|
+
* the consumer always gets a valid snapshot.
|
|
1762
|
+
*/
|
|
1763
|
+
async get(): Promise<CartSnapshot> {
|
|
1764
|
+
return this.client.request({
|
|
1765
|
+
path: "cart",
|
|
1766
|
+
responseSchema: cartSnapshotSchema,
|
|
1767
|
+
headers: this.headers(),
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Add a product (optionally with a variant) to the cart. If the
|
|
1773
|
+
* (productId, variantId) already exists, increments its quantity
|
|
1774
|
+
* rather than creating a duplicate line.
|
|
1775
|
+
*/
|
|
1776
|
+
async addItem(input: CartAddItemInput): Promise<CartSnapshot> {
|
|
1777
|
+
const body = cartAddItemInputSchema.parse(input);
|
|
1778
|
+
return this.client.request({
|
|
1779
|
+
method: "POST",
|
|
1780
|
+
path: "cart/items",
|
|
1781
|
+
body,
|
|
1782
|
+
responseSchema: cartSnapshotSchema,
|
|
1783
|
+
headers: this.headers(),
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
/**
|
|
1788
|
+
* Set a line's quantity. Pass quantity=0 to delete the line.
|
|
1789
|
+
*/
|
|
1790
|
+
async updateItem(
|
|
1791
|
+
itemId: string,
|
|
1792
|
+
input: CartUpdateItemInput,
|
|
1793
|
+
): Promise<CartSnapshot> {
|
|
1794
|
+
const body = cartUpdateItemInputSchema.parse(input);
|
|
1795
|
+
return this.client.request({
|
|
1796
|
+
method: "PATCH",
|
|
1797
|
+
path: `cart/items/${encodeURIComponent(itemId)}`,
|
|
1798
|
+
body,
|
|
1799
|
+
responseSchema: cartSnapshotSchema,
|
|
1800
|
+
headers: this.headers(),
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
async removeItem(itemId: string): Promise<CartSnapshot> {
|
|
1805
|
+
return this.client.request({
|
|
1806
|
+
method: "DELETE",
|
|
1807
|
+
path: `cart/items/${encodeURIComponent(itemId)}`,
|
|
1808
|
+
responseSchema: cartSnapshotSchema,
|
|
1809
|
+
headers: this.headers(),
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
/**
|
|
1814
|
+
* Stamp a coupon code on the cart. Validation runs at checkout —
|
|
1815
|
+
* stamping here is a no-op when invalid; the consumer learns
|
|
1816
|
+
* via the Stripe-checkout-session response whether the code
|
|
1817
|
+
* actually applies.
|
|
1818
|
+
*/
|
|
1819
|
+
async applyCoupon(input: CartCouponInput): Promise<CartSnapshot> {
|
|
1820
|
+
const body = cartCouponInputSchema.parse(input);
|
|
1821
|
+
return this.client.request({
|
|
1822
|
+
method: "POST",
|
|
1823
|
+
path: "cart/coupon",
|
|
1824
|
+
body,
|
|
1825
|
+
responseSchema: cartSnapshotSchema,
|
|
1826
|
+
headers: this.headers(),
|
|
1827
|
+
});
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
async removeCoupon(): Promise<CartSnapshot> {
|
|
1831
|
+
return this.client.request({
|
|
1832
|
+
method: "DELETE",
|
|
1833
|
+
path: "cart/coupon",
|
|
1834
|
+
responseSchema: cartSnapshotSchema,
|
|
1835
|
+
headers: this.headers(),
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
/** Empty all items + drop the stamped coupon. Keeps the cart row itself. */
|
|
1840
|
+
async clear(): Promise<CartSnapshot> {
|
|
1841
|
+
return this.client.request({
|
|
1842
|
+
method: "POST",
|
|
1843
|
+
path: "cart/clear",
|
|
1844
|
+
responseSchema: cartSnapshotSchema,
|
|
1845
|
+
headers: this.headers(),
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
export class BiabDevCheckoutClient {
|
|
1851
|
+
constructor(
|
|
1852
|
+
private readonly client: BiabDevClient,
|
|
1853
|
+
private readonly visitorToken: string | null = null,
|
|
1854
|
+
private readonly sessionToken: string | null = null,
|
|
1855
|
+
) {}
|
|
1856
|
+
|
|
1857
|
+
/**
|
|
1858
|
+
* Authenticate the start-from-cart call as an anonymous shopper
|
|
1859
|
+
* (visitor-token cart). Same UUID you used with `client.cart.forVisitor(...)`.
|
|
1860
|
+
*/
|
|
1861
|
+
forVisitor(visitorToken: string): BiabDevCheckoutClient {
|
|
1862
|
+
return new BiabDevCheckoutClient(this.client, visitorToken, null);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/**
|
|
1866
|
+
* Authenticate as a signed-in customer (tenantAuth session token).
|
|
1867
|
+
* The Stripe Checkout Session will be created against this
|
|
1868
|
+
* customer's saved cart.
|
|
1869
|
+
*/
|
|
1870
|
+
withSession(sessionToken: string): BiabDevCheckoutClient {
|
|
1871
|
+
return new BiabDevCheckoutClient(this.client, null, sessionToken);
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
private headers(): HeadersInit {
|
|
1875
|
+
const out: Record<string, string> = {};
|
|
1876
|
+
if (this.visitorToken) out["X-BIAB-Cart-Visitor"] = this.visitorToken;
|
|
1877
|
+
if (this.sessionToken) out["X-BIAB-Session-Token"] = this.sessionToken;
|
|
1878
|
+
return out;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
/**
|
|
1882
|
+
* Legacy iframe-embed mint — gives back a tokenized URL to render
|
|
1883
|
+
* inside an iframe that redirects to Stripe. Kept for `<Checkout/>`
|
|
1884
|
+
* consumers; new code should call `start()` for native handoff.
|
|
1885
|
+
*/
|
|
1886
|
+
async createSession(
|
|
1887
|
+
input: CheckoutSessionInput,
|
|
1888
|
+
): Promise<CheckoutSessionResponse> {
|
|
1889
|
+
const body = checkoutSessionInputSchema.parse(input);
|
|
1890
|
+
return this.client.request({
|
|
1891
|
+
method: "POST",
|
|
1892
|
+
path: "checkout/session",
|
|
1893
|
+
body,
|
|
1894
|
+
responseSchema: checkoutSessionResponseSchema,
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
/**
|
|
1899
|
+
* Convert the caller's saved BIAB cart (resolved via visitor token
|
|
1900
|
+
* or session token) into a Stripe Checkout Session. Returns the
|
|
1901
|
+
* Stripe-hosted URL; redirect the browser there to collect payment.
|
|
1902
|
+
*
|
|
1903
|
+
* `successUrl` + `cancelUrl` may be full URLs on the consumer's own
|
|
1904
|
+
* domain. We append `session_id={CHECKOUT_SESSION_ID}` to the
|
|
1905
|
+
* success URL automatically so the success page can call
|
|
1906
|
+
* `getStatus(sessionId)` to confirm payment.
|
|
1907
|
+
*/
|
|
1908
|
+
async start(input?: CheckoutStartInput): Promise<CheckoutStartResponse> {
|
|
1909
|
+
const body = checkoutStartInputSchema.parse(input);
|
|
1910
|
+
return this.client.request({
|
|
1911
|
+
method: "POST",
|
|
1912
|
+
path: "checkout/start",
|
|
1913
|
+
body: body ?? {},
|
|
1914
|
+
responseSchema: checkoutStartResponseSchema,
|
|
1915
|
+
headers: this.headers(),
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
/**
|
|
1920
|
+
* Look up a Stripe Checkout Session by id. Returns paid / unpaid
|
|
1921
|
+
* status plus the customer details Stripe collected — what the
|
|
1922
|
+
* consumer's "thank you" page calls before rendering.
|
|
1923
|
+
*/
|
|
1924
|
+
async getStatus(sessionId: string): Promise<CheckoutStatusResponse> {
|
|
1925
|
+
return this.client.request({
|
|
1926
|
+
path: `checkout/${encodeURIComponent(sessionId)}`,
|
|
1927
|
+
responseSchema: checkoutStatusResponseSchema,
|
|
1928
|
+
});
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
/**
|
|
1933
|
+
* White-glove Google Places proxy. BIAB owns the API key — consumer
|
|
1934
|
+
* code never sees it. Use at the checkout delivery-address input
|
|
1935
|
+
* to give shoppers text-search predictions + verify deliverability
|
|
1936
|
+
* before submitting the order.
|
|
1937
|
+
*
|
|
1938
|
+
* Mirrors the working reference in `Resources/addressParser/`:
|
|
1939
|
+
* - `search(query)` — text-search predictions (uses Google
|
|
1940
|
+
* `places:searchText`)
|
|
1941
|
+
* - `getDetails(placeId)` — placeId → `GoogleAddress` shape
|
|
1942
|
+
* - `verify(form)` — text-searches the entered address and
|
|
1943
|
+
* compares component-by-component
|
|
1944
|
+
*
|
|
1945
|
+
* Three pure converters are exported alongside as static methods —
|
|
1946
|
+
* the same ones the reference React hooks wrap, just plain functions
|
|
1947
|
+
* so they work outside React too.
|
|
1948
|
+
*
|
|
1949
|
+
* @example
|
|
1950
|
+
* ```tsx
|
|
1951
|
+
* // On each keystroke (debounced):
|
|
1952
|
+
* const result = await client.address.search({ query: input });
|
|
1953
|
+
* if (!result.enabled) return; // platform not configured; show plain input
|
|
1954
|
+
*
|
|
1955
|
+
* // When user picks a prediction:
|
|
1956
|
+
* const detail = await client.address.getDetails(prediction.place_id);
|
|
1957
|
+
* if (detail.enabled && detail.address) {
|
|
1958
|
+
* const form = BiabDevAddressClient.googleToFormValues(detail.address);
|
|
1959
|
+
* reactHookForm.reset(form);
|
|
1960
|
+
* }
|
|
1961
|
+
*
|
|
1962
|
+
* // Before submitting the order:
|
|
1963
|
+
* const v = await client.address.verify(form);
|
|
1964
|
+
* if (!v.isVerified) showDidYouMean(v.googleAddress);
|
|
1965
|
+
* ```
|
|
1966
|
+
*/
|
|
1967
|
+
export class BiabDevAddressClient {
|
|
1968
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
1969
|
+
|
|
1970
|
+
async search(input: AddressSearchInput): Promise<AddressSearchResponse> {
|
|
1971
|
+
const body = addressSearchInputSchema.parse(input);
|
|
1972
|
+
return this.client.request({
|
|
1973
|
+
method: "POST",
|
|
1974
|
+
path: "address/search",
|
|
1975
|
+
body,
|
|
1976
|
+
responseSchema: addressSearchResponseSchema,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
async getDetails(placeId: string): Promise<AddressDetailsResponse> {
|
|
1981
|
+
return this.client.request({
|
|
1982
|
+
path: `address/details/${encodeURIComponent(placeId)}`,
|
|
1983
|
+
responseSchema: addressDetailsResponseSchema,
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
async verify(input: AddressFormValues): Promise<AddressVerifyResponse> {
|
|
1988
|
+
const body = addressFormValuesSchema.parse(input);
|
|
1989
|
+
return this.client.request({
|
|
1990
|
+
method: "POST",
|
|
1991
|
+
path: "address/validate",
|
|
1992
|
+
body,
|
|
1993
|
+
responseSchema: addressVerifyResponseSchema,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/** Convert Google's `GoogleAddress` shape into a form-values object. */
|
|
1998
|
+
static googleToFormValues(g: GoogleAddress): AddressFormValues {
|
|
1999
|
+
return {
|
|
2000
|
+
address_ln1: `${g.street_number} ${g.route}`.trim(),
|
|
2001
|
+
address_ln2: g.subpremise ?? "",
|
|
2002
|
+
city: g.locality,
|
|
2003
|
+
state: g.administrative_area_level_1,
|
|
2004
|
+
zip: g.postal_code,
|
|
2005
|
+
country: g.country,
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/** Convert a form-values object into Stripe's billing-address shape. */
|
|
2010
|
+
static formToStripeAddress(a: AddressFormValues): {
|
|
2011
|
+
line1: string;
|
|
2012
|
+
line2: string | null;
|
|
2013
|
+
city: string;
|
|
2014
|
+
state: string;
|
|
2015
|
+
postal_code: string;
|
|
2016
|
+
country: string;
|
|
2017
|
+
} {
|
|
2018
|
+
return {
|
|
2019
|
+
line1: a.address_ln1,
|
|
2020
|
+
line2: a.address_ln2 || null,
|
|
2021
|
+
city: a.city,
|
|
2022
|
+
state: a.state,
|
|
2023
|
+
postal_code: a.zip,
|
|
2024
|
+
country: a.country,
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
/**
|
|
2030
|
+
* Org-side recurring offerings (gym memberships, monthly service
|
|
2031
|
+
* plans, software subscriptions). Lists are public read; the
|
|
2032
|
+
* checkout-start endpoint hands the customer off to Stripe-hosted
|
|
2033
|
+
* Checkout in `mode: 'subscription'`.
|
|
2034
|
+
*
|
|
2035
|
+
* Requires the org to have synced each offering to Stripe via the
|
|
2036
|
+
* dashboard's "Sync to Stripe" action — otherwise `startCheckout`
|
|
2037
|
+
* returns 409 `subscription_not_synced`.
|
|
2038
|
+
*/
|
|
2039
|
+
export class BiabDevSubscriptionsClient {
|
|
2040
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
2041
|
+
|
|
2042
|
+
async list(): Promise<SubscriptionOfferingsListResponse> {
|
|
2043
|
+
return this.client.request({
|
|
2044
|
+
path: "subscriptions",
|
|
2045
|
+
responseSchema: subscriptionOfferingsListResponseSchema,
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
async get(id: string): Promise<SubscriptionOffering> {
|
|
2050
|
+
return this.client.request({
|
|
2051
|
+
path: `subscriptions/${encodeURIComponent(id)}`,
|
|
2052
|
+
responseSchema: subscriptionOfferingSchema,
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
async startCheckout(
|
|
2057
|
+
id: string,
|
|
2058
|
+
input?: SubscriptionCheckoutInput,
|
|
2059
|
+
): Promise<SubscriptionCheckoutResponse> {
|
|
2060
|
+
const body = subscriptionCheckoutInputSchema.parse(input);
|
|
2061
|
+
return this.client.request({
|
|
2062
|
+
method: "POST",
|
|
2063
|
+
path: `subscriptions/${encodeURIComponent(id)}/checkout`,
|
|
2064
|
+
body: body ?? {},
|
|
2065
|
+
responseSchema: subscriptionCheckoutResponseSchema,
|
|
2066
|
+
});
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
export class BiabDevCouponsClient {
|
|
2071
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Validate a coupon code against the org's Stripe-connected
|
|
2075
|
+
* account. Returns the discount shape (percent / amount /
|
|
2076
|
+
* duration) when valid; `{ valid: false, reason, message }`
|
|
2077
|
+
* otherwise so the consumer's UI can show "$10 off" before the
|
|
2078
|
+
* shopper proceeds to checkout.
|
|
2079
|
+
*
|
|
2080
|
+
* Source of truth is Stripe — what this endpoint validates is
|
|
2081
|
+
* exactly what Stripe-hosted Checkout will accept.
|
|
2082
|
+
*
|
|
2083
|
+
* @example
|
|
2084
|
+
* ```ts
|
|
2085
|
+
* const result = await client.coupons.validate("SAVE10");
|
|
2086
|
+
* if (result.valid) showDiscount(result.percentOff ?? result.amountOffCents);
|
|
2087
|
+
* else toast.error(result.message);
|
|
2088
|
+
* ```
|
|
2089
|
+
*/
|
|
2090
|
+
async validate(code: string): Promise<CouponValidateResponse> {
|
|
2091
|
+
const body = couponValidateInputSchema.parse({ code });
|
|
2092
|
+
return this.client.request({
|
|
2093
|
+
method: "POST",
|
|
2094
|
+
path: "coupons/validate",
|
|
2095
|
+
body,
|
|
2096
|
+
responseSchema: couponValidateResponseSchema,
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
/**
|
|
2102
|
+
* Shipping: rate lookups + on-demand tracking. White-glove model —
|
|
2103
|
+
* BIAB owns the carrier integration; consumer code never sees the
|
|
2104
|
+
* underlying provider name. Gated on `ecommerce.shipping` so calls
|
|
2105
|
+
* from orgs on the Launch plan return the standard plan-gate body
|
|
2106
|
+
* via {@link BiabApiError} (status 403).
|
|
2107
|
+
*
|
|
2108
|
+
* @example
|
|
2109
|
+
* ```ts
|
|
2110
|
+
* const { options, requiresShipping } = await client.shipping
|
|
2111
|
+
* .forVisitor(visitorToken)
|
|
2112
|
+
* .getRates({ deliveryAddress });
|
|
2113
|
+
* if (!requiresShipping) skipShippingStep();
|
|
2114
|
+
* else {
|
|
2115
|
+
* const picked = options[0]; // cheapest
|
|
2116
|
+
* await client.checkout.forVisitor(visitorToken).start({
|
|
2117
|
+
* shipping: picked,
|
|
2118
|
+
* successUrl, cancelUrl,
|
|
2119
|
+
* });
|
|
2120
|
+
* }
|
|
2121
|
+
* ```
|
|
2122
|
+
*/
|
|
2123
|
+
export class BiabDevShippingClient {
|
|
2124
|
+
constructor(
|
|
2125
|
+
private readonly client: BiabDevClient,
|
|
2126
|
+
private readonly visitorToken: string | null = null,
|
|
2127
|
+
private readonly sessionToken: string | null = null,
|
|
2128
|
+
) {}
|
|
2129
|
+
|
|
2130
|
+
forVisitor(visitorToken: string): BiabDevShippingClient {
|
|
2131
|
+
return new BiabDevShippingClient(this.client, visitorToken, null);
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
withSession(sessionToken: string): BiabDevShippingClient {
|
|
2135
|
+
return new BiabDevShippingClient(this.client, null, sessionToken);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
private headers(): HeadersInit {
|
|
2139
|
+
const out: Record<string, string> = {};
|
|
2140
|
+
if (this.visitorToken) out["X-BIAB-Cart-Visitor"] = this.visitorToken;
|
|
2141
|
+
if (this.sessionToken) out["X-BIAB-Session-Token"] = this.sessionToken;
|
|
2142
|
+
return out;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* Fetch shipping rate options for the caller's cart + a delivery
|
|
2147
|
+
* address. Multi-origin + multi-parcel orders surface as a single
|
|
2148
|
+
* combined cost per service tier (carrier+speed). Pass the chosen
|
|
2149
|
+
* option directly to `client.checkout.start({ shipping })`.
|
|
2150
|
+
*/
|
|
2151
|
+
async getRates(input: ShippingRatesInput): Promise<ShippingRatesResponse> {
|
|
2152
|
+
const body = shippingRatesInputSchema.parse(input);
|
|
2153
|
+
return this.client.request({
|
|
2154
|
+
method: "POST",
|
|
2155
|
+
path: "shipping/rates",
|
|
2156
|
+
body,
|
|
2157
|
+
responseSchema: shippingRatesResponseSchema,
|
|
2158
|
+
headers: this.headers(),
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* On-demand tracking lookup for a known tracking number. Returns
|
|
2164
|
+
* the cached `status_history` we already received via webhook
|
|
2165
|
+
* PLUS a fresh poll against the carrier (`liveStatus`). Consumer
|
|
2166
|
+
* apps that don't subscribe to push notifications use this to
|
|
2167
|
+
* refresh "Where's my order?" pages.
|
|
2168
|
+
*/
|
|
2169
|
+
async track(input: {
|
|
2170
|
+
trackingNumber: string;
|
|
2171
|
+
}): Promise<ShippingTrackResponse> {
|
|
2172
|
+
return this.client.request({
|
|
2173
|
+
path: `shipping/track/${encodeURIComponent(input.trackingNumber)}`,
|
|
2174
|
+
responseSchema: shippingTrackResponseSchema,
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/**
|
|
2179
|
+
* The org's storefront shipping config — whether shipping is on, a coarse
|
|
2180
|
+
* "ships from" (city/state/country), the destinations it ships to, the duty
|
|
2181
|
+
* model, and preferred carriers. Sanitized (no keys/secrets, no precise
|
|
2182
|
+
* origin address). Render shipping copy without the org re-declaring it.
|
|
2183
|
+
*
|
|
2184
|
+
* @example
|
|
2185
|
+
* ```ts
|
|
2186
|
+
* const s = await client.shipping.settings();
|
|
2187
|
+
* if (s.enabled) show(`Ships to ${s.allowedDestinationCountries?.join(", ") ?? "anywhere"}`);
|
|
2188
|
+
* ```
|
|
2189
|
+
*/
|
|
2190
|
+
async settings(): Promise<ShippingSettingsResponse> {
|
|
2191
|
+
return this.client.request({
|
|
2192
|
+
path: "shipping/settings",
|
|
2193
|
+
responseSchema: shippingSettingsResponseSchema,
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* Customer notification preferences. Per-customer-per-org channel
|
|
2200
|
+
* settings (in-app / email / SMS / outbound webhook). Identified by
|
|
2201
|
+
* the tenantAuth session token — bind with `.withSession(token)`.
|
|
2202
|
+
*
|
|
2203
|
+
* Outbound-webhook events the consumer site can subscribe to are
|
|
2204
|
+
* the same as the shipping fan-out keys:
|
|
2205
|
+
* - `order.shipped`
|
|
2206
|
+
* - `order.out_for_delivery`
|
|
2207
|
+
* - `order.delivered`
|
|
2208
|
+
* - `order.exception`
|
|
2209
|
+
* - `order.return_label_created`
|
|
2210
|
+
*
|
|
2211
|
+
* The webhook payload is JSON + signed with the customer's saved
|
|
2212
|
+
* `webhookSigningSecret` via `X-BIAB-Signature: <hex hmac-sha256>`.
|
|
2213
|
+
* Consumer's receiver verifies via:
|
|
2214
|
+
* crypto.createHmac('sha256', secret).update(rawBody).digest('hex')
|
|
2215
|
+
*/
|
|
2216
|
+
export class BiabDevNotificationsClient {
|
|
2217
|
+
constructor(
|
|
2218
|
+
private readonly client: BiabDevClient,
|
|
2219
|
+
private readonly sessionToken: string | null = null,
|
|
2220
|
+
) {}
|
|
2221
|
+
|
|
2222
|
+
withSession(sessionToken: string): BiabDevNotificationsClient {
|
|
2223
|
+
return new BiabDevNotificationsClient(this.client, sessionToken);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
private headers(): HeadersInit {
|
|
2227
|
+
return this.sessionToken
|
|
2228
|
+
? { "X-BIAB-Session-Token": this.sessionToken }
|
|
2229
|
+
: {};
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
async getPreferences(): Promise<CustomerNotificationPreferencesResponse> {
|
|
2233
|
+
return this.client.request({
|
|
2234
|
+
path: "notifications/preferences",
|
|
2235
|
+
responseSchema: customerNotificationPreferencesResponseSchema,
|
|
2236
|
+
headers: this.headers(),
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
async updatePreferences(
|
|
2241
|
+
input: CustomerNotificationPreferencesUpdateInput,
|
|
2242
|
+
): Promise<CustomerNotificationPreferencesUpdateResponse> {
|
|
2243
|
+
const body = customerNotificationPreferencesUpdateInputSchema.parse(input);
|
|
2244
|
+
return this.client.request({
|
|
2245
|
+
method: "PATCH",
|
|
2246
|
+
path: "notifications/preferences",
|
|
2247
|
+
body,
|
|
2248
|
+
responseSchema: customerNotificationPreferencesUpdateResponseSchema,
|
|
2249
|
+
headers: this.headers(),
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* Start a verification flow for a destination the customer wants to
|
|
2255
|
+
* use for shipping notifications. Email: a verification link is sent
|
|
2256
|
+
* (15-min TTL). Phone: a 6-digit OTP is sent (5-min TTL).
|
|
2257
|
+
*
|
|
2258
|
+
* Until verified, the channel stays inert — preventing the customer
|
|
2259
|
+
* from spamming arbitrary destinations.
|
|
2260
|
+
*/
|
|
2261
|
+
async startVerification(
|
|
2262
|
+
input: StartVerificationInput,
|
|
2263
|
+
): Promise<StartVerificationResponse> {
|
|
2264
|
+
const body = startVerificationInputSchema.parse(input);
|
|
2265
|
+
return this.client.request({
|
|
2266
|
+
method: "POST",
|
|
2267
|
+
path: "notifications/preferences/verify",
|
|
2268
|
+
body,
|
|
2269
|
+
responseSchema: startVerificationResponseSchema,
|
|
2270
|
+
headers: this.headers(),
|
|
2271
|
+
});
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
/**
|
|
2275
|
+
* Consume the verification token from the email link / SMS OTP.
|
|
2276
|
+
* On success: stamps `*VerifiedAt` on the prefs row + activates the
|
|
2277
|
+
* channel.
|
|
2278
|
+
*/
|
|
2279
|
+
async confirmVerification(
|
|
2280
|
+
input: ConfirmVerificationInput,
|
|
2281
|
+
): Promise<ConfirmVerificationResponse> {
|
|
2282
|
+
const body = confirmVerificationInputSchema.parse(input);
|
|
2283
|
+
return this.client.request({
|
|
2284
|
+
method: "POST",
|
|
2285
|
+
path: "notifications/preferences/verify/confirm",
|
|
2286
|
+
body,
|
|
2287
|
+
responseSchema: confirmVerificationResponseSchema,
|
|
2288
|
+
headers: this.headers(),
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
export class BiabDevBlogClient {
|
|
2294
|
+
constructor(
|
|
2295
|
+
private readonly client: BiabDevClient,
|
|
2296
|
+
private readonly sessionToken: string | null = null,
|
|
2297
|
+
) {}
|
|
2298
|
+
|
|
2299
|
+
/**
|
|
2300
|
+
* Chainable: return a blog client whose `postComment` + future
|
|
2301
|
+
* write methods authenticate via the tenantAuth session token.
|
|
2302
|
+
* Reads (listPosts, getPost, listCategories, listTags, listComments)
|
|
2303
|
+
* don't require a session and work the same either way.
|
|
2304
|
+
*
|
|
2305
|
+
* @example
|
|
2306
|
+
* ```ts
|
|
2307
|
+
* await client.blog().withSession(token).postComment(slug, { content });
|
|
2308
|
+
* ```
|
|
2309
|
+
*/
|
|
2310
|
+
withSession(sessionToken: string): BiabDevBlogClient {
|
|
2311
|
+
return new BiabDevBlogClient(this.client, sessionToken);
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
private headers(): HeadersInit {
|
|
2315
|
+
return this.sessionToken
|
|
2316
|
+
? { "X-BIAB-Session-Token": this.sessionToken }
|
|
2317
|
+
: {};
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* Legacy embed-session mint (kept for backwards compat with
|
|
2322
|
+
* embedded-iframe consumers).
|
|
2323
|
+
*/
|
|
2324
|
+
async createSession(input?: BlogSessionInput): Promise<BlogSessionResponse> {
|
|
2325
|
+
const body = blogSessionInputSchema.parse(input ?? undefined);
|
|
2326
|
+
return this.client.request({
|
|
2327
|
+
method: "POST",
|
|
2328
|
+
path: "blog/session",
|
|
2329
|
+
body: body ?? {},
|
|
2330
|
+
responseSchema: blogSessionResponseSchema,
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
/**
|
|
2335
|
+
* Paginated list of published blog posts for the calling org's
|
|
2336
|
+
* site. Cursor is an opaque number — pass back the `nextCursor`
|
|
2337
|
+
* from the previous response to fetch the next page.
|
|
2338
|
+
*/
|
|
2339
|
+
async listPosts(input?: {
|
|
2340
|
+
limit?: number;
|
|
2341
|
+
cursor?: number | null;
|
|
2342
|
+
categoryId?: string;
|
|
2343
|
+
}): Promise<BlogListPostsResponse> {
|
|
2344
|
+
const query: Record<string, string | number | undefined> = {};
|
|
2345
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
2346
|
+
if (input?.cursor != null) query["cursor"] = input.cursor;
|
|
2347
|
+
if (input?.categoryId) query["categoryId"] = input.categoryId;
|
|
2348
|
+
return this.client.request({
|
|
2349
|
+
path: "blog/posts",
|
|
2350
|
+
query,
|
|
2351
|
+
responseSchema: blogListPostsResponseSchema,
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
/**
|
|
2356
|
+
* Get a single post by its slug. Returns `{ post, access }`
|
|
2357
|
+
* where `access === 'paywall'` and `post.content === ''` for
|
|
2358
|
+
* posts marked `accessLevel: 'paid' | 'followers'`. The
|
|
2359
|
+
* consumer renders a paywall over the empty body when access
|
|
2360
|
+
* is restricted.
|
|
2361
|
+
*/
|
|
2362
|
+
async getPost(slug: string): Promise<BlogPostBySlugResponse> {
|
|
2363
|
+
return this.client.request({
|
|
2364
|
+
path: `blog/posts/${encodeURIComponent(slug)}`,
|
|
2365
|
+
responseSchema: blogPostBySlugResponseSchema,
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
async listCategories(): Promise<BlogCategoryListResponse> {
|
|
2370
|
+
return this.client.request({
|
|
2371
|
+
path: "blog/categories",
|
|
2372
|
+
responseSchema: blogCategoryListResponseSchema,
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
async listTags(): Promise<BlogTagListResponse> {
|
|
2377
|
+
return this.client.request({
|
|
2378
|
+
path: "blog/tags",
|
|
2379
|
+
responseSchema: blogTagListResponseSchema,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
async listComments(
|
|
2384
|
+
slug: string,
|
|
2385
|
+
input?: { limit?: number; cursor?: number | null },
|
|
2386
|
+
): Promise<BlogCommentListResponse> {
|
|
2387
|
+
const query: Record<string, string | number | undefined> = {};
|
|
2388
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
2389
|
+
if (input?.cursor != null) query["cursor"] = input.cursor;
|
|
2390
|
+
return this.client.request({
|
|
2391
|
+
path: `blog/posts/${encodeURIComponent(slug)}/comments`,
|
|
2392
|
+
query,
|
|
2393
|
+
responseSchema: blogCommentListResponseSchema,
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
/**
|
|
2398
|
+
* Submit a comment on a published post. Requires `withSession()` —
|
|
2399
|
+
* anonymous comments aren't supported in 0.9.0 (the blog_comments
|
|
2400
|
+
* row references a `user_id` text column that's NOT NULL). The
|
|
2401
|
+
* server runs the same moderation screener used by the dashboard
|
|
2402
|
+
* tRPC path; the response surfaces both the comment row + the
|
|
2403
|
+
* moderation verdict so the consumer can show "your comment is
|
|
2404
|
+
* pending review" when applicable.
|
|
2405
|
+
*/
|
|
2406
|
+
async postComment(
|
|
2407
|
+
slug: string,
|
|
2408
|
+
input: BlogCommentPostInput,
|
|
2409
|
+
): Promise<BlogCommentPostResponse> {
|
|
2410
|
+
const body = blogCommentPostInputSchema.parse(input);
|
|
2411
|
+
return this.client.request({
|
|
2412
|
+
method: "POST",
|
|
2413
|
+
path: `blog/posts/${encodeURIComponent(slug)}/comments`,
|
|
2414
|
+
body,
|
|
2415
|
+
responseSchema: blogCommentPostResponseSchema,
|
|
2416
|
+
headers: this.headers(),
|
|
2417
|
+
});
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/**
|
|
2421
|
+
* Current like state + count for a post. With `withSession()`, `liked`
|
|
2422
|
+
* reflects whether this visitor already liked it (filled heart on load);
|
|
2423
|
+
* without a session the public count is returned with `liked: false`.
|
|
2424
|
+
*/
|
|
2425
|
+
async likeStatus(slug: string): Promise<BlogLikeResponse> {
|
|
2426
|
+
return this.client.request({
|
|
2427
|
+
path: `blog/posts/${encodeURIComponent(slug)}/like`,
|
|
2428
|
+
responseSchema: blogLikeResponseSchema,
|
|
2429
|
+
headers: this.headers(),
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Toggle the session visitor's like on a post. Requires `withSession()`
|
|
2435
|
+
* (same identity as `postComment`). Returns the new like state + count.
|
|
2436
|
+
*/
|
|
2437
|
+
async toggleLike(slug: string): Promise<BlogLikeResponse> {
|
|
2438
|
+
return this.client.request({
|
|
2439
|
+
method: "POST",
|
|
2440
|
+
path: `blog/posts/${encodeURIComponent(slug)}/like`,
|
|
2441
|
+
responseSchema: blogLikeResponseSchema,
|
|
2442
|
+
headers: this.headers(),
|
|
2443
|
+
});
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/**
|
|
2448
|
+
* Public reviews-wall client. Reads the org's external reviews
|
|
2449
|
+
* (Google + Yelp + manual + CSV) — the same store the marketing bundle's
|
|
2450
|
+
* `reviews` aggregate is built from. The bundle ships the first page + the
|
|
2451
|
+
* aggregate (count / average / histogram); use this to lazily fetch deeper
|
|
2452
|
+
* pages on "View all" / scroll.
|
|
2453
|
+
*
|
|
2454
|
+
* @example
|
|
2455
|
+
* ```ts
|
|
2456
|
+
* const page1 = await client.reviews.list({ limit: 10 });
|
|
2457
|
+
* if (page1.nextOffset != null) {
|
|
2458
|
+
* const page2 = await client.reviews.list({ limit: 10, offset: page1.nextOffset });
|
|
2459
|
+
* }
|
|
2460
|
+
* ```
|
|
2461
|
+
*/
|
|
2462
|
+
export class BiabDevReviewsClient {
|
|
2463
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
2464
|
+
|
|
2465
|
+
/**
|
|
2466
|
+
* One page of the org's public reviews, newest-first. `limit` defaults to
|
|
2467
|
+
* the server default (10, max 50). Pass the previous response's
|
|
2468
|
+
* `nextOffset` to page forward — it's `null` once the list is exhausted.
|
|
2469
|
+
* `source` narrows to a single platform (e.g. `"yelp"`).
|
|
2470
|
+
*/
|
|
2471
|
+
async list(input?: {
|
|
2472
|
+
limit?: number;
|
|
2473
|
+
offset?: number;
|
|
2474
|
+
source?: "google" | "yelp" | "housecall_pro" | "other";
|
|
2475
|
+
}): Promise<ReviewWallListResponse> {
|
|
2476
|
+
const query: Record<string, string | number | undefined> = {};
|
|
2477
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
2478
|
+
if (input?.offset !== undefined) query["offset"] = input.offset;
|
|
2479
|
+
if (input?.source) query["source"] = input.source;
|
|
2480
|
+
return this.client.request({
|
|
2481
|
+
path: "reviews",
|
|
2482
|
+
query,
|
|
2483
|
+
responseSchema: reviewWallListResponseSchema,
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
/**
|
|
2489
|
+
* Public hiring client — the org's open job postings and their categories, so
|
|
2490
|
+
* a consumer can render a careers page in their own design system.
|
|
2491
|
+
*
|
|
2492
|
+
* Read-only by construction: everything returned is content the org published
|
|
2493
|
+
* for candidates. To take applications, hand the posting's `applicationFormId`
|
|
2494
|
+
* to the forms surface — this client grants no submit path of its own.
|
|
2495
|
+
*
|
|
2496
|
+
* @example
|
|
2497
|
+
* ```ts
|
|
2498
|
+
* const { items } = await client.hiring.list({ limit: 20 });
|
|
2499
|
+
* const posting = await client.hiring.get("senior-installer");
|
|
2500
|
+
* if (posting.applicationFormId) {
|
|
2501
|
+
* const form = await client.forms.schema(posting.applicationFormId);
|
|
2502
|
+
* }
|
|
2503
|
+
* ```
|
|
2504
|
+
*/
|
|
2505
|
+
export class BiabDevHiringClient {
|
|
2506
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* One page of open postings, newest-first. `limit` defaults to the server
|
|
2510
|
+
* default (25, max 100). Pass the previous response's `nextOffset` to page
|
|
2511
|
+
* forward — it's `null` once the list is exhausted. `category` narrows to a
|
|
2512
|
+
* single group by its slug.
|
|
2513
|
+
*
|
|
2514
|
+
* Only postings that are published, active, have seats left, and are under
|
|
2515
|
+
* their application cap are returned, so a careers page never advertises a
|
|
2516
|
+
* role that can no longer be applied to.
|
|
2517
|
+
*/
|
|
2518
|
+
async list(input?: {
|
|
2519
|
+
limit?: number;
|
|
2520
|
+
offset?: number;
|
|
2521
|
+
category?: string;
|
|
2522
|
+
}): Promise<JobPostingListResponse> {
|
|
2523
|
+
const query: Record<string, string | number | undefined> = {};
|
|
2524
|
+
if (input?.limit !== undefined) query["limit"] = input.limit;
|
|
2525
|
+
if (input?.offset !== undefined) query["offset"] = input.offset;
|
|
2526
|
+
if (input?.category) query["category"] = input.category;
|
|
2527
|
+
return this.client.request({
|
|
2528
|
+
path: "hiring/jobs",
|
|
2529
|
+
query,
|
|
2530
|
+
responseSchema: jobPostingListResponseSchema,
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* One open posting by slug. Rejects with a 404 error when the posting is a
|
|
2536
|
+
* draft, closed, archived, fully filled, or has hit its application cap —
|
|
2537
|
+
* treat that as "this role is gone" rather than as a transient failure.
|
|
2538
|
+
*/
|
|
2539
|
+
async get(slug: string): Promise<JobPosting> {
|
|
2540
|
+
return this.client.request({
|
|
2541
|
+
path: `hiring/jobs/${encodeURIComponent(slug)}`,
|
|
2542
|
+
responseSchema: jobPostingSchema,
|
|
2543
|
+
});
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
/**
|
|
2547
|
+
* The org's job categories with a count of open postings in each — enough to
|
|
2548
|
+
* render filter chips or grouped sections without fetching every posting.
|
|
2549
|
+
*/
|
|
2550
|
+
async categories(): Promise<JobCategoryListResponse> {
|
|
2551
|
+
return this.client.request({
|
|
2552
|
+
path: "hiring/categories",
|
|
2553
|
+
responseSchema: jobCategoryListResponseSchema,
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
export class BiabDevChatbotClient {
|
|
2559
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
2560
|
+
|
|
2561
|
+
/** Returns the iframe URL for `<Chatbot/>`. */
|
|
2562
|
+
async createSession(): Promise<ChatbotSessionResponse> {
|
|
2563
|
+
return this.client.request({
|
|
2564
|
+
method: "POST",
|
|
2565
|
+
path: "chatbot/session",
|
|
2566
|
+
responseSchema: chatbotSessionResponseSchema,
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
/** Reads the org's persisted chatbot config (system prompt, KB, model). */
|
|
2571
|
+
async getConfig(): Promise<ChatbotConfig> {
|
|
2572
|
+
return this.client.request({
|
|
2573
|
+
method: "GET",
|
|
2574
|
+
path: "chatbot/config",
|
|
2575
|
+
responseSchema: chatbotConfigSchema,
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
/**
|
|
2580
|
+
* Persists the org's chatbot config. Requires `chatbot:write` scope on the
|
|
2581
|
+
* API key — typically only used by admin tools, not embedded widgets.
|
|
2582
|
+
*/
|
|
2583
|
+
async updateConfig(input: ChatbotConfigUpdateInput): Promise<ChatbotConfig> {
|
|
2584
|
+
const body = chatbotConfigUpdateInputSchema.parse(input);
|
|
2585
|
+
return this.client.request({
|
|
2586
|
+
method: "PATCH",
|
|
2587
|
+
path: "chatbot/config",
|
|
2588
|
+
body,
|
|
2589
|
+
responseSchema: chatbotConfigSchema,
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
/**
|
|
2594
|
+
* Headless chat — host owns the UI. Each request is fully org-scoped: the
|
|
2595
|
+
* server runs the LLM with the org's saved system prompt + KB + model.
|
|
2596
|
+
*
|
|
2597
|
+
* The response includes `uiActions` (e.g. `show_form`) and `availability`
|
|
2598
|
+
* (front-desk presence + wait-time hint). Headless consumers should resolve
|
|
2599
|
+
* any `show_form` action via {@link getChatForm}, render the fields, and POST
|
|
2600
|
+
* back through {@link submitChatForm} (which carries the chat transcript).
|
|
2601
|
+
*/
|
|
2602
|
+
async chat(input: ChatbotChatInput): Promise<ChatbotChatResponse> {
|
|
2603
|
+
const body = chatbotChatInputSchema.parse(input);
|
|
2604
|
+
return this.client.request({
|
|
2605
|
+
method: "POST",
|
|
2606
|
+
path: "chatbot/chat",
|
|
2607
|
+
body,
|
|
2608
|
+
responseSchema: chatbotChatResponseSchema,
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
/**
|
|
2613
|
+
* Upload a file a visitor attached in the chat. Presigns a direct-to-R2 PUT
|
|
2614
|
+
* (gated by the org's `chat.file_uploads` entitlement — check
|
|
2615
|
+
* `getConfig().fileUploadsEnabled` before offering it), uploads the bytes, and
|
|
2616
|
+
* returns a ref you can attach to the conversation. `sessionId` is any stable
|
|
2617
|
+
* per-chat id (drives the 5-files/hour cap + provenance). Throws on a refused
|
|
2618
|
+
* upload (entitlement, 7 MB cap, or rate limit).
|
|
2619
|
+
*/
|
|
2620
|
+
async uploadFile(args: {
|
|
2621
|
+
sessionId: string;
|
|
2622
|
+
file: File;
|
|
2623
|
+
/** Captured visitor email — stored for provenance + checked against the
|
|
2624
|
+
* platform block-list. Optional. */
|
|
2625
|
+
uploaderEmail?: string | null;
|
|
2626
|
+
}): Promise<{ url: string; name: string; type: string; size: number }> {
|
|
2627
|
+
const contentType = args.file.type || "application/octet-stream";
|
|
2628
|
+
const presigned = await this.client.request({
|
|
2629
|
+
method: "POST",
|
|
2630
|
+
path: "chatbot/upload-url",
|
|
2631
|
+
body: {
|
|
2632
|
+
sessionId: args.sessionId,
|
|
2633
|
+
filename: args.file.name,
|
|
2634
|
+
contentType,
|
|
2635
|
+
sizeBytes: args.file.size,
|
|
2636
|
+
uploaderEmail: args.uploaderEmail ?? null,
|
|
2637
|
+
},
|
|
2638
|
+
responseSchema: chatbotUploadUrlResponseSchema,
|
|
2639
|
+
});
|
|
2640
|
+
const put = await fetch(presigned.uploadUrl, {
|
|
2641
|
+
method: "PUT",
|
|
2642
|
+
headers: { "Content-Type": contentType },
|
|
2643
|
+
body: args.file,
|
|
2644
|
+
});
|
|
2645
|
+
if (!put.ok) throw new Error(`Upload failed (${put.status}).`);
|
|
2646
|
+
return {
|
|
2647
|
+
url: presigned.publicUrl,
|
|
2648
|
+
name: args.file.name,
|
|
2649
|
+
type: contentType,
|
|
2650
|
+
size: args.file.size,
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* Live front-desk availability — tier flag, staff-online presence, and
|
|
2656
|
+
* a wait-time hint string. Safe to poll (every 20–30s) so the SDK
|
|
2657
|
+
* consumer can render or hide a "Talk to a person" CTA without sending
|
|
2658
|
+
* a chat turn.
|
|
2659
|
+
*/
|
|
2660
|
+
async getAvailability(): Promise<ChatbotAvailability> {
|
|
2661
|
+
return this.client.request({
|
|
2662
|
+
method: "GET",
|
|
2663
|
+
path: "chatbot/availability",
|
|
2664
|
+
responseSchema: chatbotAvailabilitySchema,
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
/**
|
|
2669
|
+
* Fetch a **Live** form definition to render inline on your own page.
|
|
2670
|
+
* A form is reachable here when it is Live (`isActive`) — independent of
|
|
2671
|
+
* `chatbotAccess`. Returns a 404-equivalent throw when the form is missing
|
|
2672
|
+
* or not Live. For a form the AI chatbot recommended via a `show_form`
|
|
2673
|
+
* action, use {@link getChatForm} instead (it carries the chat transcript on
|
|
2674
|
+
* submit).
|
|
2675
|
+
*/
|
|
2676
|
+
async getForm(slug: string): Promise<ChatbotFormDefinition> {
|
|
2677
|
+
return this.client.request({
|
|
2678
|
+
method: "GET",
|
|
2679
|
+
path: `forms/${encodeURIComponent(slug)}`,
|
|
2680
|
+
responseSchema: chatbotFormDefinitionSchema,
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/**
|
|
2685
|
+
* Submit a **Live** form. Fans out to the same hiring/leads/inquiries CRM
|
|
2686
|
+
* mirrors, actions, scheduling, and staff notification as every other ingest
|
|
2687
|
+
* point. Gated on Live only — no `chatbotAccess` required.
|
|
2688
|
+
*/
|
|
2689
|
+
async submitForm(
|
|
2690
|
+
slug: string,
|
|
2691
|
+
input: ChatbotFormSubmitInput,
|
|
2692
|
+
): Promise<ChatbotFormSubmitResponse> {
|
|
2693
|
+
const body = chatbotFormSubmitInputSchema.parse(input);
|
|
2694
|
+
return this.client.request({
|
|
2695
|
+
method: "POST",
|
|
2696
|
+
path: `forms/${encodeURIComponent(slug)}`,
|
|
2697
|
+
body,
|
|
2698
|
+
responseSchema: chatbotFormSubmitResponseSchema,
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* Fetch a form the AI chatbot recommended via a `show_form` UI action. This
|
|
2704
|
+
* additionally requires the form to be chatbot-accessible (`chatbotAccess`),
|
|
2705
|
+
* which a recommended form always is. Most embeds want {@link getForm}; reach
|
|
2706
|
+
* for this only inside the chat flow.
|
|
2707
|
+
*/
|
|
2708
|
+
async getChatForm(slug: string): Promise<ChatbotFormDefinition> {
|
|
2709
|
+
return this.client.request({
|
|
2710
|
+
method: "GET",
|
|
2711
|
+
path: `chatbot/forms/${encodeURIComponent(slug)}`,
|
|
2712
|
+
responseSchema: chatbotFormDefinitionSchema,
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
/**
|
|
2717
|
+
* Submit a chatbot-recommended form. Same CRM fan-out as {@link submitForm},
|
|
2718
|
+
* but on the chatbot path — pass `priorChat` so the resulting CRM inquiry can
|
|
2719
|
+
* include the chat transcript that led to the submission.
|
|
2720
|
+
*/
|
|
2721
|
+
async submitChatForm(
|
|
2722
|
+
slug: string,
|
|
2723
|
+
input: ChatbotFormSubmitInput,
|
|
2724
|
+
): Promise<ChatbotFormSubmitResponse> {
|
|
2725
|
+
const body = chatbotFormSubmitInputSchema.parse(input);
|
|
2726
|
+
return this.client.request({
|
|
2727
|
+
method: "POST",
|
|
2728
|
+
path: `chatbot/forms/${encodeURIComponent(slug)}`,
|
|
2729
|
+
body,
|
|
2730
|
+
responseSchema: chatbotFormSubmitResponseSchema,
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// ── Front Desk (Batch JJ) ────────────────────────────────────────
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* Create or resume the persisted `chatSessions` row for this
|
|
2738
|
+
* visitor. Idempotent on `(orgId, visitorToken)`. The returned
|
|
2739
|
+
* `sessionId` is what you pass to `postMessage`, `pollMessages`,
|
|
2740
|
+
* and `requestHuman`.
|
|
2741
|
+
*
|
|
2742
|
+
* Distinct from {@link createSession} — that mints an iframe-embed
|
|
2743
|
+
* token. This one exposes the underlying server session so the
|
|
2744
|
+
* consumer can render their own UI and still reach Front Desk.
|
|
2745
|
+
*/
|
|
2746
|
+
async createPersistedSession(
|
|
2747
|
+
input: ChatbotPersistedSessionInput,
|
|
2748
|
+
): Promise<ChatbotPersistedSessionResponse> {
|
|
2749
|
+
const body = chatbotPersistedSessionInputSchema.parse(input);
|
|
2750
|
+
return this.client.request({
|
|
2751
|
+
method: "POST",
|
|
2752
|
+
path: "chatbot/persisted-session",
|
|
2753
|
+
body,
|
|
2754
|
+
responseSchema: chatbotPersistedSessionResponseSchema,
|
|
2755
|
+
});
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
/**
|
|
2759
|
+
* Poll messages + session status. Pass `since` (ISO timestamp) on
|
|
2760
|
+
* subsequent polls to only fetch new rows. Recommended cadence:
|
|
2761
|
+
* 3–5s while the widget is open and focused.
|
|
2762
|
+
*/
|
|
2763
|
+
async pollMessages(input: {
|
|
2764
|
+
sessionId: string;
|
|
2765
|
+
visitorToken: string;
|
|
2766
|
+
since?: string;
|
|
2767
|
+
}): Promise<ChatbotMessagesResponse> {
|
|
2768
|
+
return this.client.request({
|
|
2769
|
+
path: "chatbot/messages",
|
|
2770
|
+
query: {
|
|
2771
|
+
sessionId: input.sessionId,
|
|
2772
|
+
visitorToken: input.visitorToken,
|
|
2773
|
+
since: input.since,
|
|
2774
|
+
},
|
|
2775
|
+
responseSchema: chatbotMessagesResponseSchema,
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
/**
|
|
2780
|
+
* Append a visitor message. Returns the updated message log so
|
|
2781
|
+
* the consumer can replace its local list with an authoritative
|
|
2782
|
+
* snapshot rather than tracking optimistic updates manually.
|
|
2783
|
+
*/
|
|
2784
|
+
async postMessage(
|
|
2785
|
+
input: ChatbotAppendMessageInput,
|
|
2786
|
+
): Promise<ChatbotMessagesResponse> {
|
|
2787
|
+
const body = chatbotAppendMessageInputSchema.parse(input);
|
|
2788
|
+
return this.client.request({
|
|
2789
|
+
method: "POST",
|
|
2790
|
+
path: "chatbot/messages",
|
|
2791
|
+
body,
|
|
2792
|
+
responseSchema: chatbotMessagesResponseSchema,
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
/**
|
|
2797
|
+
* Visitor presses "Talk to a person." Plan-gated on
|
|
2798
|
+
* `chat.front_desk` — orgs on Launch get back
|
|
2799
|
+
* `{ available: false, reason: 'plan_required', requiredFeature, upgradeUrl }`.
|
|
2800
|
+
* Orgs on Growth+ get `{ available: true, status, placeInLine, ... }`.
|
|
2801
|
+
*
|
|
2802
|
+
* Branch on `result.available` to render an upgrade CTA vs the
|
|
2803
|
+
* "you're in line" UI.
|
|
2804
|
+
*
|
|
2805
|
+
* @example
|
|
2806
|
+
* ```ts
|
|
2807
|
+
* const result = await client.chatbot.requestHuman({ sessionId, visitorToken });
|
|
2808
|
+
* if (!result.available) return <UpgradeCard feature={result.requiredFeature} />;
|
|
2809
|
+
* return <Queue position={result.placeInLine} eta={result.estimateLabel} />;
|
|
2810
|
+
* ```
|
|
2811
|
+
*/
|
|
2812
|
+
async requestHuman(
|
|
2813
|
+
input: ChatbotRequestHumanInput,
|
|
2814
|
+
): Promise<ChatbotRequestHumanResponse> {
|
|
2815
|
+
const body = chatbotRequestHumanInputSchema.parse(input);
|
|
2816
|
+
return this.client.request({
|
|
2817
|
+
method: "POST",
|
|
2818
|
+
path: "chatbot/request-human",
|
|
2819
|
+
body,
|
|
2820
|
+
responseSchema: chatbotRequestHumanResponseSchema,
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
/**
|
|
2826
|
+
* Reads the active customer-portal context (organization + user) and exposes
|
|
2827
|
+
* profile mutations + a list of *only this user's* other customer
|
|
2828
|
+
* relationships.
|
|
2829
|
+
*
|
|
2830
|
+
* @remarks
|
|
2831
|
+
* **Privacy:** When `organizationId` is set, every request is pinned to that
|
|
2832
|
+
* org via the `X-BIAB-Customer-Portal-Org` header. The server-side
|
|
2833
|
+
* `customerPortalProcedure` will reject any access to a different org, so a
|
|
2834
|
+
* customer browsing a Dominoes-branded site can never accidentally see Pizza
|
|
2835
|
+
* Hut data even if their account is linked to both. This is the recommended
|
|
2836
|
+
* mode for embedding the SDK on a tenant's site or app.
|
|
2837
|
+
*/
|
|
2838
|
+
/**
|
|
2839
|
+
* Decode a base64 string to bytes. Uses `atob` (available in browsers and
|
|
2840
|
+
* Node >= 16), so it needs no Buffer polyfill in browser bundles.
|
|
2841
|
+
*/
|
|
2842
|
+
function decodeBase64ToBytes(base64: string): Uint8Array {
|
|
2843
|
+
const binary = atob(base64);
|
|
2844
|
+
const bytes = new Uint8Array(binary.length);
|
|
2845
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2846
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2847
|
+
}
|
|
2848
|
+
return bytes;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
export class BiabDevCustomerPortalClient {
|
|
2852
|
+
constructor(
|
|
2853
|
+
private readonly client: BiabDevClient,
|
|
2854
|
+
private readonly organizationId: string | null,
|
|
2855
|
+
/**
|
|
2856
|
+
* Optional tenantAuth session token. When set, requests include
|
|
2857
|
+
* an `X-BIAB-Session-Token` header and the BIAB customer-portal
|
|
2858
|
+
* routes resolve the calling user via the session payload
|
|
2859
|
+
* instead of the API key's user binding.
|
|
2860
|
+
*
|
|
2861
|
+
* This is the right path for consumer storefronts that don't
|
|
2862
|
+
* mint per-customer API keys: sign customers in via
|
|
2863
|
+
* `client.auth.signIn()`, store the resulting session token
|
|
2864
|
+
* (the SDK's `createAuthHandler` does this in an httpOnly
|
|
2865
|
+
* cookie automatically), then pass that token to
|
|
2866
|
+
* `client.customerPortal(orgId).withSession(token)` for every
|
|
2867
|
+
* customer-scoped call.
|
|
2868
|
+
*/
|
|
2869
|
+
private readonly sessionToken: string | null = null,
|
|
2870
|
+
) {}
|
|
2871
|
+
|
|
2872
|
+
/**
|
|
2873
|
+
* Return a customer-portal client that authenticates the calling
|
|
2874
|
+
* user via the tenantAuth session token instead of (or in
|
|
2875
|
+
* addition to) the API key's user binding. Chainable.
|
|
2876
|
+
*
|
|
2877
|
+
* @example
|
|
2878
|
+
* ```ts
|
|
2879
|
+
* const session = await getTenantSessionFromCookie({ cookieValue });
|
|
2880
|
+
* const work = await client
|
|
2881
|
+
* .customerPortal(orgId)
|
|
2882
|
+
* .withSession(session.sessionToken)
|
|
2883
|
+
* .getWork();
|
|
2884
|
+
* ```
|
|
2885
|
+
*/
|
|
2886
|
+
withSession(sessionToken: string): BiabDevCustomerPortalClient {
|
|
2887
|
+
return new BiabDevCustomerPortalClient(
|
|
2888
|
+
this.client,
|
|
2889
|
+
this.organizationId,
|
|
2890
|
+
sessionToken,
|
|
2891
|
+
);
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
private headers(): HeadersInit {
|
|
2895
|
+
const out: Record<string, string> = {};
|
|
2896
|
+
if (this.organizationId) {
|
|
2897
|
+
out["X-BIAB-Customer-Portal-Org"] = this.organizationId;
|
|
2898
|
+
}
|
|
2899
|
+
if (this.sessionToken) {
|
|
2900
|
+
out["X-BIAB-Session-Token"] = this.sessionToken;
|
|
2901
|
+
}
|
|
2902
|
+
return out;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
async context(): Promise<CustomerPortalContextResponse> {
|
|
2906
|
+
return this.client.request({
|
|
2907
|
+
path: "customer-portal/context",
|
|
2908
|
+
responseSchema: customerPortalContextResponseSchema,
|
|
2909
|
+
headers: this.headers(),
|
|
2910
|
+
});
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
async getProfile(): Promise<CustomerPortalProfile> {
|
|
2914
|
+
return this.client.request({
|
|
2915
|
+
path: "customer-portal/profile",
|
|
2916
|
+
responseSchema: customerPortalProfileSchema,
|
|
2917
|
+
headers: this.headers(),
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
async updateProfile(
|
|
2922
|
+
input: CustomerPortalUpdateProfileInput,
|
|
2923
|
+
): Promise<CustomerPortalUpdateProfileResponse> {
|
|
2924
|
+
const body = customerPortalUpdateProfileInputSchema.parse(input);
|
|
2925
|
+
return this.client.request({
|
|
2926
|
+
method: "POST",
|
|
2927
|
+
path: "customer-portal/profile",
|
|
2928
|
+
body,
|
|
2929
|
+
responseSchema: customerPortalUpdateProfileResponseSchema,
|
|
2930
|
+
headers: this.headers(),
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
async myOtherCustomerOrgs(): Promise<CustomerPortalOtherOrgsResponse> {
|
|
2935
|
+
return this.client.request({
|
|
2936
|
+
path: "customer-portal/other-orgs",
|
|
2937
|
+
responseSchema: customerPortalOtherOrgsResponseSchema,
|
|
2938
|
+
headers: this.headers(),
|
|
2939
|
+
});
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
/**
|
|
2943
|
+
* Read the signed-in customer's notification preferences (per category,
|
|
2944
|
+
* per channel) for the bound org. The response also includes the full
|
|
2945
|
+
* category + channel catalog so SDK consumers can render the UI
|
|
2946
|
+
* without hardcoding either — both grow over time on the server side.
|
|
2947
|
+
*
|
|
2948
|
+
* @example
|
|
2949
|
+
* ```ts
|
|
2950
|
+
* const { preferences, categories, channels } =
|
|
2951
|
+
* await client.customerPortal().getNotificationPreferences();
|
|
2952
|
+
*
|
|
2953
|
+
* categories.map((c) => (
|
|
2954
|
+
* <Row category={c} state={preferences[c.id]} />
|
|
2955
|
+
* ));
|
|
2956
|
+
* ```
|
|
2957
|
+
*/
|
|
2958
|
+
async getNotificationPreferences(): Promise<NotificationPreferencesResponse> {
|
|
2959
|
+
return this.client.request({
|
|
2960
|
+
path: "customer-portal/notification-preferences",
|
|
2961
|
+
responseSchema: notificationPreferencesResponseSchema,
|
|
2962
|
+
headers: this.headers(),
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
/**
|
|
2967
|
+
* Update notification preferences. Sparse: only send the toggles you
|
|
2968
|
+
* want to flip — omitted keys keep their persisted (or default) value.
|
|
2969
|
+
* Compliance-required channels (e.g. transactional email) silently
|
|
2970
|
+
* stay true regardless of what's sent.
|
|
2971
|
+
*
|
|
2972
|
+
* @example
|
|
2973
|
+
* ```ts
|
|
2974
|
+
* await client.customerPortal().updateNotificationPreferences({
|
|
2975
|
+
* preferences: { marketing: { email: false, sms: false } },
|
|
2976
|
+
* });
|
|
2977
|
+
* ```
|
|
2978
|
+
*/
|
|
2979
|
+
async updateNotificationPreferences(
|
|
2980
|
+
input: NotificationPreferencesUpdateInput,
|
|
2981
|
+
): Promise<NotificationPreferencesResponse> {
|
|
2982
|
+
const body = notificationPreferencesUpdateInputSchema.parse(input);
|
|
2983
|
+
return this.client.request({
|
|
2984
|
+
method: "POST",
|
|
2985
|
+
path: "customer-portal/notification-preferences",
|
|
2986
|
+
body,
|
|
2987
|
+
responseSchema: notificationPreferencesResponseSchema,
|
|
2988
|
+
headers: this.headers(),
|
|
2989
|
+
});
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/**
|
|
2993
|
+
* Read the signed-in customer's "what's on my plate" bundle: open
|
|
2994
|
+
* jobs, pending quotes, unpaid invoices, contracts awaiting signature,
|
|
2995
|
+
* and a recent payment history, plus a roll-up summary for the
|
|
2996
|
+
* consumer header.
|
|
2997
|
+
*
|
|
2998
|
+
* URLs are host-less paths — resolve them against the consumer's own
|
|
2999
|
+
* storefront origin to build clickable links.
|
|
3000
|
+
*
|
|
3001
|
+
* @example
|
|
3002
|
+
* ```ts
|
|
3003
|
+
* const work = await client.customerPortal().getWork();
|
|
3004
|
+
* const { openJobCount, unpaidBalance } = work.summary;
|
|
3005
|
+
* work.invoices.filter((i) => i.balanceDue > 0).map(...)
|
|
3006
|
+
* ```
|
|
3007
|
+
*/
|
|
3008
|
+
async getWork(): Promise<CustomerWorkBundle> {
|
|
3009
|
+
return this.client.request({
|
|
3010
|
+
path: "customer-portal/work",
|
|
3011
|
+
responseSchema: customerWorkBundleSchema,
|
|
3012
|
+
headers: this.headers(),
|
|
3013
|
+
});
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
/** Read a single job the signed-in customer owns. */
|
|
3017
|
+
async getJob(jobId: string): Promise<CustomerJobDetail> {
|
|
3018
|
+
return this.client.request({
|
|
3019
|
+
path: `customer-portal/jobs/${encodeURIComponent(jobId)}`,
|
|
3020
|
+
responseSchema: customerJobDetailSchema,
|
|
3021
|
+
headers: this.headers(),
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
/** Read a single quote the signed-in customer owns. */
|
|
3026
|
+
async getQuote(quoteId: string): Promise<CustomerQuoteDetail> {
|
|
3027
|
+
return this.client.request({
|
|
3028
|
+
path: `customer-portal/quotes/${encodeURIComponent(quoteId)}`,
|
|
3029
|
+
responseSchema: customerQuoteDetailSchema,
|
|
3030
|
+
headers: this.headers(),
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
/** Naming alias for {@link getQuote} — read a single quote the customer owns. */
|
|
3035
|
+
async viewQuote(quoteId: string): Promise<CustomerQuoteDetail> {
|
|
3036
|
+
return this.getQuote(quoteId);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
/**
|
|
3040
|
+
* Accept a quote as the signed-in customer. Idempotent — re-accepting
|
|
3041
|
+
* an already-accepted quote resolves the existing state without
|
|
3042
|
+
* additional side effects.
|
|
3043
|
+
*/
|
|
3044
|
+
async acceptQuote(quoteId: string): Promise<CustomerQuoteActionResponse> {
|
|
3045
|
+
return this.client.request({
|
|
3046
|
+
method: "POST",
|
|
3047
|
+
path: `customer-portal/quotes/${encodeURIComponent(quoteId)}/accept`,
|
|
3048
|
+
responseSchema: customerQuoteActionResponseSchema,
|
|
3049
|
+
headers: this.headers(),
|
|
3050
|
+
});
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
/** Reject a quote as the signed-in customer. Idempotent. */
|
|
3054
|
+
async rejectQuote(quoteId: string): Promise<CustomerQuoteActionResponse> {
|
|
3055
|
+
return this.client.request({
|
|
3056
|
+
method: "POST",
|
|
3057
|
+
path: `customer-portal/quotes/${encodeURIComponent(quoteId)}/reject`,
|
|
3058
|
+
responseSchema: customerQuoteActionResponseSchema,
|
|
3059
|
+
headers: this.headers(),
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
/** Read a single invoice the signed-in customer owns. */
|
|
3064
|
+
async getInvoice(invoiceId: string): Promise<CustomerInvoiceDetail> {
|
|
3065
|
+
return this.client.request({
|
|
3066
|
+
path: `customer-portal/invoices/${encodeURIComponent(invoiceId)}`,
|
|
3067
|
+
responseSchema: customerInvoiceDetailSchema,
|
|
3068
|
+
headers: this.headers(),
|
|
3069
|
+
});
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
/**
|
|
3073
|
+
* Start payment for an invoice the customer owns. Returns a Stripe-hosted
|
|
3074
|
+
* Checkout URL to redirect to — card entry happens entirely on Stripe, so
|
|
3075
|
+
* your app never touches card data (stays out of PCI scope).
|
|
3076
|
+
*
|
|
3077
|
+
* Pass your own `successUrl` / `cancelUrl` to return into your storefront,
|
|
3078
|
+
* or omit them to land back on the org's branded hosted invoice page.
|
|
3079
|
+
*
|
|
3080
|
+
* @example
|
|
3081
|
+
* ```ts
|
|
3082
|
+
* const { checkoutUrl } = await client
|
|
3083
|
+
* .customerPortal()
|
|
3084
|
+
* .payInvoice(invoiceId, { successUrl: "https://shop.example.com/paid" });
|
|
3085
|
+
* window.location.href = checkoutUrl;
|
|
3086
|
+
* ```
|
|
3087
|
+
*/
|
|
3088
|
+
async payInvoice(
|
|
3089
|
+
invoiceId: string,
|
|
3090
|
+
opts?: CustomerInvoicePayInput,
|
|
3091
|
+
): Promise<CustomerInvoicePayResponse> {
|
|
3092
|
+
const body = customerInvoicePayInputSchema.parse(opts ?? {});
|
|
3093
|
+
return this.client.request({
|
|
3094
|
+
method: "POST",
|
|
3095
|
+
path: `customer-portal/invoices/${encodeURIComponent(invoiceId)}/pay`,
|
|
3096
|
+
body,
|
|
3097
|
+
responseSchema: customerInvoicePayResponseSchema,
|
|
3098
|
+
headers: this.headers(),
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
/**
|
|
3103
|
+
* Download the customer's invoice as a PDF. Returns the file metadata plus
|
|
3104
|
+
* the decoded `bytes` (a `Uint8Array`) — the auth headers ride along on the
|
|
3105
|
+
* SDK request, so this works for a signed-in customer without exposing a
|
|
3106
|
+
* public URL. Once paid, the PDF carries a PAID marker + payment history.
|
|
3107
|
+
*
|
|
3108
|
+
* @example Browser download
|
|
3109
|
+
* ```ts
|
|
3110
|
+
* const file = await client.customerPortal(orgId).withSession(token)
|
|
3111
|
+
* .downloadInvoicePdf(invoiceId);
|
|
3112
|
+
* const url = URL.createObjectURL(
|
|
3113
|
+
* new Blob([file.bytes], { type: file.contentType }),
|
|
3114
|
+
* );
|
|
3115
|
+
* const a = document.createElement("a");
|
|
3116
|
+
* a.href = url;
|
|
3117
|
+
* a.download = file.filename;
|
|
3118
|
+
* a.click();
|
|
3119
|
+
* URL.revokeObjectURL(url);
|
|
3120
|
+
* ```
|
|
3121
|
+
*/
|
|
3122
|
+
async downloadInvoicePdf(
|
|
3123
|
+
invoiceId: string,
|
|
3124
|
+
): Promise<CustomerInvoicePdf & { bytes: Uint8Array }> {
|
|
3125
|
+
const res = await this.client.request({
|
|
3126
|
+
path: `customer-portal/invoices/${encodeURIComponent(invoiceId)}/pdf`,
|
|
3127
|
+
responseSchema: customerInvoicePdfSchema,
|
|
3128
|
+
headers: this.headers(),
|
|
3129
|
+
});
|
|
3130
|
+
return { ...res, bytes: decodeBase64ToBytes(res.base64) };
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
/** Read a single contract the signed-in customer owns. */
|
|
3134
|
+
async getContract(contractId: string): Promise<CustomerContractDetail> {
|
|
3135
|
+
return this.client.request({
|
|
3136
|
+
path: `customer-portal/contracts/${encodeURIComponent(contractId)}`,
|
|
3137
|
+
responseSchema: customerContractDetailSchema,
|
|
3138
|
+
headers: this.headers(),
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
/**
|
|
3143
|
+
* Begin signing a contract the customer owns. Returns a ready, absolute
|
|
3144
|
+
* signing URL on the org's branded hostname — redirect the customer there
|
|
3145
|
+
* to BIAB's hosted e-sign page, which captures the signature and generates
|
|
3146
|
+
* the executed PDF. No signature payload is collected client-side, so the
|
|
3147
|
+
* legal record stays consistent across consumers.
|
|
3148
|
+
*
|
|
3149
|
+
* Rejects with `contract_already_signed` (409) or `contract_not_signable`
|
|
3150
|
+
* (409) when the contract isn't in a signable state.
|
|
3151
|
+
*
|
|
3152
|
+
* @example
|
|
3153
|
+
* ```ts
|
|
3154
|
+
* const { signingUrl } = await client
|
|
3155
|
+
* .customerPortal()
|
|
3156
|
+
* .signContract(contractId);
|
|
3157
|
+
* window.location.href = signingUrl;
|
|
3158
|
+
* ```
|
|
3159
|
+
*/
|
|
3160
|
+
async signContract(
|
|
3161
|
+
contractId: string,
|
|
3162
|
+
): Promise<CustomerContractSignResponse> {
|
|
3163
|
+
return this.client.request({
|
|
3164
|
+
method: "POST",
|
|
3165
|
+
path: `customer-portal/contracts/${encodeURIComponent(contractId)}/sign`,
|
|
3166
|
+
responseSchema: customerContractSignResponseSchema,
|
|
3167
|
+
headers: this.headers(),
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
/** List reviews the signed-in customer has submitted in this org. */
|
|
3172
|
+
async listMyReviews(): Promise<CustomerReviewListResponse> {
|
|
3173
|
+
return this.client.request({
|
|
3174
|
+
path: "customer-portal/reviews",
|
|
3175
|
+
responseSchema: customerReviewListResponseSchema,
|
|
3176
|
+
headers: this.headers(),
|
|
3177
|
+
});
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
/**
|
|
3181
|
+
* Referral roll-up for the signed-in customer (as an affiliate), summed
|
|
3182
|
+
* across every program they're enrolled in. `enrolled: false` with zeroed
|
|
3183
|
+
* counters when the customer holds no affiliate record. Real data appears
|
|
3184
|
+
* once the org's referral attribution + payout pipeline records activity.
|
|
3185
|
+
*
|
|
3186
|
+
* @example
|
|
3187
|
+
* ```ts
|
|
3188
|
+
* const stats = await client.customerPortal().myReferralStats();
|
|
3189
|
+
* if (stats.enrolled) showEarnings(stats.earningsCents);
|
|
3190
|
+
* ```
|
|
3191
|
+
*/
|
|
3192
|
+
async myReferralStats(): Promise<CustomerReferralStats> {
|
|
3193
|
+
return this.client.request({
|
|
3194
|
+
path: "customer-portal/referrals/stats",
|
|
3195
|
+
responseSchema: customerReferralStatsSchema,
|
|
3196
|
+
headers: this.headers(),
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
/**
|
|
3201
|
+
* The customer's referral conversion feed (most recent first). Empty until
|
|
3202
|
+
* the attribution pipeline records conversions. `limit` 1–200 (default 50).
|
|
3203
|
+
*/
|
|
3204
|
+
async myReferralHistory(opts?: {
|
|
3205
|
+
limit?: number;
|
|
3206
|
+
}): Promise<CustomerReferralHistoryResponse> {
|
|
3207
|
+
const limit = opts?.limit;
|
|
3208
|
+
const qs =
|
|
3209
|
+
typeof limit === "number"
|
|
3210
|
+
? `?limit=${encodeURIComponent(String(limit))}`
|
|
3211
|
+
: "";
|
|
3212
|
+
return this.client.request({
|
|
3213
|
+
path: `customer-portal/referrals/history${qs}`,
|
|
3214
|
+
responseSchema: customerReferralHistoryResponseSchema,
|
|
3215
|
+
headers: this.headers(),
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
/** The customer's referral payout history + upcoming rows (newest first). */
|
|
3220
|
+
async myReferralPayouts(): Promise<CustomerReferralPayoutsResponse> {
|
|
3221
|
+
return this.client.request({
|
|
3222
|
+
path: "customer-portal/referrals/payouts",
|
|
3223
|
+
responseSchema: customerReferralPayoutsResponseSchema,
|
|
3224
|
+
headers: this.headers(),
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
/**
|
|
3229
|
+
* The signed-in customer's orders (newest first), each with a shipment
|
|
3230
|
+
* roll-up (`overallStatus`, `anyInTransit`, `latestEstimatedDelivery`) for
|
|
3231
|
+
* an orders/shipping dashboard list. `limit` 1–100 (default 30). Requires
|
|
3232
|
+
* the org's `ecommerce.shipping` entitlement.
|
|
3233
|
+
*
|
|
3234
|
+
* @example
|
|
3235
|
+
* ```ts
|
|
3236
|
+
* const { items } = await client.customerPortal().listOrders();
|
|
3237
|
+
* items.filter((o) => o.anyInTransit).map(renderTracker);
|
|
3238
|
+
* ```
|
|
3239
|
+
*/
|
|
3240
|
+
async listOrders(opts?: {
|
|
3241
|
+
limit?: number;
|
|
3242
|
+
}): Promise<CustomerOrderListResponse> {
|
|
3243
|
+
const limit = opts?.limit;
|
|
3244
|
+
const qs =
|
|
3245
|
+
typeof limit === "number"
|
|
3246
|
+
? `?limit=${encodeURIComponent(String(limit))}`
|
|
3247
|
+
: "";
|
|
3248
|
+
return this.client.request({
|
|
3249
|
+
path: `customer-portal/orders${qs}`,
|
|
3250
|
+
responseSchema: customerOrderListResponseSchema,
|
|
3251
|
+
headers: this.headers(),
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
|
|
3255
|
+
/**
|
|
3256
|
+
* The customer's full payment history with this org — invoice payments,
|
|
3257
|
+
* store-order charges, and refunds (negative amounts), newest first.
|
|
3258
|
+
* Powers a "Payments" tab on the customer dashboard.
|
|
3259
|
+
*
|
|
3260
|
+
* ```ts
|
|
3261
|
+
* const { items } = await client.customerPortal().listPayments();
|
|
3262
|
+
* items.map((p) => `${p.occurredAt} ${p.kind} $${p.amount}`);
|
|
3263
|
+
* ```
|
|
3264
|
+
*/
|
|
3265
|
+
async listPayments(opts?: {
|
|
3266
|
+
limit?: number;
|
|
3267
|
+
}): Promise<import("./contracts").CustomerPaymentHistoryResponse> {
|
|
3268
|
+
const limit = opts?.limit;
|
|
3269
|
+
const qs =
|
|
3270
|
+
typeof limit === "number"
|
|
3271
|
+
? `?limit=${encodeURIComponent(String(limit))}`
|
|
3272
|
+
: "";
|
|
3273
|
+
return this.client.request({
|
|
3274
|
+
path: `customer-portal/payments${qs}`,
|
|
3275
|
+
responseSchema: customerPaymentHistoryResponseSchema,
|
|
3276
|
+
headers: this.headers(),
|
|
3277
|
+
});
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
/**
|
|
3281
|
+
* One order the customer owns: line items + shipments, each shipment with
|
|
3282
|
+
* its carrier, tracking link, status, and `statusHistory` timeline (read
|
|
3283
|
+
* from stored data — webhook-fresh).
|
|
3284
|
+
*/
|
|
3285
|
+
async getOrder(orderId: string): Promise<CustomerOrderDetail> {
|
|
3286
|
+
return this.client.request({
|
|
3287
|
+
path: `customer-portal/orders/${encodeURIComponent(orderId)}`,
|
|
3288
|
+
responseSchema: customerOrderDetailSchema,
|
|
3289
|
+
headers: this.headers(),
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
|
|
3293
|
+
/**
|
|
3294
|
+
* Open a return / refund request on an order the customer owns. Creates a
|
|
3295
|
+
* `requested` record and notifies the org's review queue — the org's
|
|
3296
|
+
* approval (not this call) triggers the actual Stripe refund. Omit `items`
|
|
3297
|
+
* for a whole-order request. The request then shows up under
|
|
3298
|
+
* `getOrder(orderId).returnRequests`.
|
|
3299
|
+
*
|
|
3300
|
+
* @example
|
|
3301
|
+
* ```ts
|
|
3302
|
+
* await portal.submitReturnRequest(orderId, {
|
|
3303
|
+
* reason: "damaged",
|
|
3304
|
+
* customerNote: "Arrived cracked.",
|
|
3305
|
+
* items: [{ orderItemId, quantity: 1 }],
|
|
3306
|
+
* });
|
|
3307
|
+
* ```
|
|
3308
|
+
*/
|
|
3309
|
+
async submitReturnRequest(
|
|
3310
|
+
orderId: string,
|
|
3311
|
+
input?: CustomerReturnRequestInput,
|
|
3312
|
+
): Promise<CustomerReturnRequestCreated> {
|
|
3313
|
+
const body = customerReturnRequestInputSchema.parse(input ?? {});
|
|
3314
|
+
return this.client.request({
|
|
3315
|
+
method: "POST",
|
|
3316
|
+
path: `customer-portal/orders/${encodeURIComponent(orderId)}/return-request`,
|
|
3317
|
+
body,
|
|
3318
|
+
responseSchema: customerReturnRequestCreatedSchema,
|
|
3319
|
+
headers: this.headers(),
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
/**
|
|
3324
|
+
* List the digital downloads the customer is entitled to for a paid order
|
|
3325
|
+
* (P10d). Each item is a ready-to-use URL — a short-lived presigned link for
|
|
3326
|
+
* BIAB-hosted files (`expiresInSeconds` set) or the org's external link.
|
|
3327
|
+
* Returns an empty list until the order's payment settles.
|
|
3328
|
+
*
|
|
3329
|
+
* @example
|
|
3330
|
+
* ```ts
|
|
3331
|
+
* const { items } = await portal.getDownloads(orderId);
|
|
3332
|
+
* for (const d of items) console.log(d.productName, d.url);
|
|
3333
|
+
* ```
|
|
3334
|
+
*/
|
|
3335
|
+
async getDownloads(orderId: string): Promise<CustomerDownloadsResponse> {
|
|
3336
|
+
return this.client.request({
|
|
3337
|
+
path: `customer-portal/orders/${encodeURIComponent(orderId)}/downloads`,
|
|
3338
|
+
responseSchema: customerDownloadsResponseSchema,
|
|
3339
|
+
headers: this.headers(),
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
/**
|
|
3344
|
+
* Track a single shipment the customer owns (resolved through their own
|
|
3345
|
+
* order). Returns the stored shipment plus a best-effort live carrier status
|
|
3346
|
+
* (`liveStatus`) when it's still in transit.
|
|
3347
|
+
*/
|
|
3348
|
+
async trackShipment(shipmentId: string): Promise<CustomerShipmentTracking> {
|
|
3349
|
+
return this.client.request({
|
|
3350
|
+
path: `customer-portal/shipments/${encodeURIComponent(shipmentId)}/track`,
|
|
3351
|
+
responseSchema: customerShipmentTrackingSchema,
|
|
3352
|
+
headers: this.headers(),
|
|
3353
|
+
});
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
/**
|
|
3357
|
+
* Submit a review tied to the signed-in customer's email. Land
|
|
3358
|
+
* with `status: pending` so org staff can moderate before the
|
|
3359
|
+
* review surfaces on the public bundle. `jobId` is optional and
|
|
3360
|
+
* validated server-side against the customer's own jobs — a
|
|
3361
|
+
* stranger's id is rejected with a 403.
|
|
3362
|
+
*/
|
|
3363
|
+
async submitReview(
|
|
3364
|
+
input: CustomerReviewSubmitInput,
|
|
3365
|
+
): Promise<CustomerReviewSubmitResponse> {
|
|
3366
|
+
const body = customerReviewSubmitInputSchema.parse(input);
|
|
3367
|
+
return this.client.request({
|
|
3368
|
+
method: "POST",
|
|
3369
|
+
path: "customer-portal/reviews",
|
|
3370
|
+
body,
|
|
3371
|
+
responseSchema: customerReviewSubmitResponseSchema,
|
|
3372
|
+
headers: this.headers(),
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
/** List the comment thread on a job the customer owns. */
|
|
3377
|
+
async listJobComments(
|
|
3378
|
+
jobId: string,
|
|
3379
|
+
): Promise<CustomerJobCommentsListResponse> {
|
|
3380
|
+
return this.client.request({
|
|
3381
|
+
path: `customer-portal/jobs/${encodeURIComponent(jobId)}/comments`,
|
|
3382
|
+
responseSchema: customerJobCommentsListResponseSchema,
|
|
3383
|
+
headers: this.headers(),
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
/** Post a customer comment on one of the customer's own jobs. */
|
|
3388
|
+
async postJobComment(
|
|
3389
|
+
jobId: string,
|
|
3390
|
+
input: CustomerJobCommentPostInput,
|
|
3391
|
+
): Promise<CustomerJobComment> {
|
|
3392
|
+
const body = customerJobCommentPostInputSchema.parse(input);
|
|
3393
|
+
return this.client.request({
|
|
3394
|
+
method: "POST",
|
|
3395
|
+
path: `customer-portal/jobs/${encodeURIComponent(jobId)}/comments`,
|
|
3396
|
+
body,
|
|
3397
|
+
responseSchema: customerJobCommentSchema,
|
|
3398
|
+
headers: this.headers(),
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
/**
|
|
3403
|
+
* Open (or reuse) a front-desk chat session bound to the signed-in
|
|
3404
|
+
* customer and post their first message. Staff see the thread in
|
|
3405
|
+
* the chatbot inbox with `linkedContactId` set so they know who's
|
|
3406
|
+
* reaching out. Returns the `sessionId` + `visitorToken` so the
|
|
3407
|
+
* consumer can poll for staff replies or render the chatbot widget
|
|
3408
|
+
* over the same session via the existing chatbot embed flow.
|
|
3409
|
+
*/
|
|
3410
|
+
async contactCompany(
|
|
3411
|
+
input: CustomerContactCompanyInput,
|
|
3412
|
+
): Promise<CustomerContactCompanyResponse> {
|
|
3413
|
+
const body = customerContactCompanyInputSchema.parse(input);
|
|
3414
|
+
return this.client.request({
|
|
3415
|
+
method: "POST",
|
|
3416
|
+
path: "customer-portal/contact-company",
|
|
3417
|
+
body,
|
|
3418
|
+
responseSchema: customerContactCompanyResponseSchema,
|
|
3419
|
+
headers: this.headers(),
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
/**
|
|
3424
|
+
* Staff-on-the-way view for a job the customer owns. First-cut:
|
|
3425
|
+
* returns assigned-tech display names + dispatch status, the last
|
|
3426
|
+
* known GPS ping per tech (when available), and the most recent
|
|
3427
|
+
* ETA snapshot from the dispatch_offers cascade.
|
|
3428
|
+
*
|
|
3429
|
+
* `liveEtaAvailable: false` today — flips to true once the
|
|
3430
|
+
* dispatch travel-time recompute layer ships, at which point the
|
|
3431
|
+
* `eta` field switches sources without breaking this shape.
|
|
3432
|
+
*/
|
|
3433
|
+
async getJobEta(jobId: string): Promise<CustomerJobEtaResponse> {
|
|
3434
|
+
return this.client.request({
|
|
3435
|
+
path: `customer-portal/jobs/${encodeURIComponent(jobId)}/eta`,
|
|
3436
|
+
responseSchema: customerJobEtaResponseSchema,
|
|
3437
|
+
headers: this.headers(),
|
|
3438
|
+
});
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
/**
|
|
3442
|
+
* Unified job activity feed: structured events (status changes,
|
|
3443
|
+
* dispatch lifecycle, media uploads, quote/contract/invoice
|
|
3444
|
+
* transitions) interleaved with chat messages linked to this
|
|
3445
|
+
* customer's contact. One linear timeline so the customer can
|
|
3446
|
+
* see "what happened, then what was said, then what happened
|
|
3447
|
+
* next" without piecing together separate views.
|
|
3448
|
+
*
|
|
3449
|
+
* Each entry is `{ kind: 'event' | 'chat', id, at, ... }`. The
|
|
3450
|
+
* discriminated union means TypeScript narrows correctly when
|
|
3451
|
+
* you branch on `kind`.
|
|
3452
|
+
*/
|
|
3453
|
+
async getJobActivity(jobId: string): Promise<CustomerJobActivityResponse> {
|
|
3454
|
+
return this.client.request({
|
|
3455
|
+
path: `customer-portal/jobs/${encodeURIComponent(jobId)}/activity`,
|
|
3456
|
+
responseSchema: customerJobActivityResponseSchema,
|
|
3457
|
+
headers: this.headers(),
|
|
3458
|
+
});
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
/**
|
|
3463
|
+
* Creates a {@link BiabDevClient} for the given host `baseUrl` and package API key.
|
|
3464
|
+
*
|
|
3465
|
+
* @remarks
|
|
3466
|
+
* **Alpha:** This package is still in alpha release. APIs may change; it is **not** intended
|
|
3467
|
+
* for production use yet. Prefer non-production environments until a stable major is published.
|
|
3468
|
+
*/
|
|
3469
|
+
export function createBiabDevClient(options: BiabDevClientOptions) {
|
|
3470
|
+
return new BiabDevClient(options);
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
/**
|
|
3474
|
+
* Per-tenant auth: hosted sign-in/sign-up scoped to the org bound to
|
|
3475
|
+
* the API key. Used by the SDK's `createAuthHandler` (server) and
|
|
3476
|
+
* `<SignIn />` / `<SignUp />` / `<SignOut />` components (client).
|
|
3477
|
+
*/
|
|
3478
|
+
export class BiabDevAuthClient {
|
|
3479
|
+
constructor(private readonly client: BiabDevClient) {}
|
|
3480
|
+
|
|
3481
|
+
async start(input: TenantAuthStartInput): Promise<TenantAuthStartResponse> {
|
|
3482
|
+
const body = tenantAuthStartInputSchema.parse(input);
|
|
3483
|
+
return this.client.request({
|
|
3484
|
+
method: "POST",
|
|
3485
|
+
path: "auth/start",
|
|
3486
|
+
body,
|
|
3487
|
+
responseSchema: tenantAuthStartResponseSchema,
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
/**
|
|
3492
|
+
* Ergonomic shortcut for `start({ intent: "sign-in", ... })`. Returns
|
|
3493
|
+
* the platform-hosted redirect URL the caller should navigate to;
|
|
3494
|
+
* stateless on its own — pair with `exchange()` on your callback
|
|
3495
|
+
* route to mint the session.
|
|
3496
|
+
*/
|
|
3497
|
+
async signIn(input: {
|
|
3498
|
+
redirectUri: string;
|
|
3499
|
+
returnTo?: string;
|
|
3500
|
+
loginHint?: string;
|
|
3501
|
+
}): Promise<TenantAuthStartResponse> {
|
|
3502
|
+
return this.start({ intent: "sign-in", ...input });
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
/**
|
|
3506
|
+
* Ergonomic shortcut for `start({ intent: "sign-up", ... })`. Same
|
|
3507
|
+
* shape as `signIn`; only the platform-hosted page that opens differs.
|
|
3508
|
+
*/
|
|
3509
|
+
async signUp(input: {
|
|
3510
|
+
redirectUri: string;
|
|
3511
|
+
returnTo?: string;
|
|
3512
|
+
loginHint?: string;
|
|
3513
|
+
}): Promise<TenantAuthStartResponse> {
|
|
3514
|
+
return this.start({ intent: "sign-up", ...input });
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
/**
|
|
3518
|
+
* Alias for `requestPasswordReset`. Always resolves `{ ok: true }`
|
|
3519
|
+
* (the server doesn't reveal whether the email matched an account).
|
|
3520
|
+
*/
|
|
3521
|
+
async forgotPassword(
|
|
3522
|
+
input: TenantAuthPasswordResetInput,
|
|
3523
|
+
): Promise<TenantAuthPasswordResetResponse> {
|
|
3524
|
+
return this.requestPasswordReset(input);
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
async exchange(
|
|
3528
|
+
input: TenantAuthExchangeInput,
|
|
3529
|
+
): Promise<TenantAuthExchangeResponse> {
|
|
3530
|
+
const body = tenantAuthExchangeInputSchema.parse(input);
|
|
3531
|
+
return this.client.request({
|
|
3532
|
+
method: "POST",
|
|
3533
|
+
path: "auth/exchange",
|
|
3534
|
+
body,
|
|
3535
|
+
responseSchema: tenantAuthExchangeResponseSchema,
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
async me(input: { sessionToken: string }): Promise<TenantAuthMeResponse> {
|
|
3540
|
+
return this.client.request({
|
|
3541
|
+
method: "GET",
|
|
3542
|
+
path: "auth/me",
|
|
3543
|
+
headers: { "x-biab-session": input.sessionToken },
|
|
3544
|
+
responseSchema: tenantAuthMeResponseSchema,
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
async signOut(): Promise<TenantAuthSignOutResponse> {
|
|
3549
|
+
return this.client.request({
|
|
3550
|
+
method: "POST",
|
|
3551
|
+
path: "auth/sign-out",
|
|
3552
|
+
responseSchema: tenantAuthSignOutResponseSchema,
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
/**
|
|
3557
|
+
* Trigger a password-reset email for `input.email`. Always resolves
|
|
3558
|
+
* with `{ ok: true }` — the server intentionally does not reveal
|
|
3559
|
+
* whether a matching account exists. The email (when sent) contains
|
|
3560
|
+
* a platform-hosted reset URL; the consumer never needs to build a
|
|
3561
|
+
* "set new password" page.
|
|
3562
|
+
*
|
|
3563
|
+
* Server-only by design (the API key may not be exposed to the
|
|
3564
|
+
* browser). The matching browser-side `requestPasswordReset()` in
|
|
3565
|
+
* `@businessdash/sdk/react` proxies through the auth-handler.
|
|
3566
|
+
*/
|
|
3567
|
+
async requestPasswordReset(
|
|
3568
|
+
input: TenantAuthPasswordResetInput,
|
|
3569
|
+
): Promise<TenantAuthPasswordResetResponse> {
|
|
3570
|
+
const body = tenantAuthPasswordResetInputSchema.parse(input);
|
|
3571
|
+
return this.client.request({
|
|
3572
|
+
method: "POST",
|
|
3573
|
+
path: "auth/password-reset",
|
|
3574
|
+
body,
|
|
3575
|
+
responseSchema: tenantAuthPasswordResetResponseSchema,
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
}
|