@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.
Files changed (268) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +899 -0
  3. package/dist/adapters-revalidate.d.ts +44 -0
  4. package/dist/adapters-revalidate.d.ts.map +1 -0
  5. package/dist/adapters-revalidate.js +68 -0
  6. package/dist/adapters-revalidate.js.map +1 -0
  7. package/dist/alpha.d.ts +21 -0
  8. package/dist/alpha.d.ts.map +1 -0
  9. package/dist/alpha.js +21 -0
  10. package/dist/alpha.js.map +1 -0
  11. package/dist/analytics-core.d.ts +34 -0
  12. package/dist/analytics-core.d.ts.map +1 -0
  13. package/dist/analytics-core.js +138 -0
  14. package/dist/analytics-core.js.map +1 -0
  15. package/dist/analytics-server.d.ts +54 -0
  16. package/dist/analytics-server.d.ts.map +1 -0
  17. package/dist/analytics-server.js +78 -0
  18. package/dist/analytics-server.js.map +1 -0
  19. package/dist/attribution-logo.d.ts +2 -0
  20. package/dist/attribution-logo.d.ts.map +1 -0
  21. package/dist/attribution-logo.js +5 -0
  22. package/dist/attribution-logo.js.map +1 -0
  23. package/dist/auth-handler.d.ts +92 -0
  24. package/dist/auth-handler.d.ts.map +1 -0
  25. package/dist/auth-handler.js +277 -0
  26. package/dist/auth-handler.js.map +1 -0
  27. package/dist/biab-forms.css +484 -0
  28. package/dist/cli.d.ts +46 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +1824 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/client.d.ts +1681 -0
  33. package/dist/client.d.ts.map +1 -0
  34. package/dist/client.js +2845 -0
  35. package/dist/client.js.map +1 -0
  36. package/dist/collections.d.ts +13 -0
  37. package/dist/collections.d.ts.map +1 -0
  38. package/dist/collections.js +13 -0
  39. package/dist/collections.js.map +1 -0
  40. package/dist/contracts.d.ts +14609 -0
  41. package/dist/contracts.d.ts.map +1 -0
  42. package/dist/contracts.js +2428 -0
  43. package/dist/contracts.js.map +1 -0
  44. package/dist/data-model-schema.d.ts +962 -0
  45. package/dist/data-model-schema.d.ts.map +1 -0
  46. package/dist/data-model-schema.js +2295 -0
  47. package/dist/data-model-schema.js.map +1 -0
  48. package/dist/distribution.d.ts +38 -0
  49. package/dist/distribution.d.ts.map +1 -0
  50. package/dist/distribution.js +61 -0
  51. package/dist/distribution.js.map +1 -0
  52. package/dist/element.d.ts +121 -0
  53. package/dist/element.d.ts.map +1 -0
  54. package/dist/element.js +276 -0
  55. package/dist/element.js.map +1 -0
  56. package/dist/enums.d.ts +25 -0
  57. package/dist/enums.d.ts.map +1 -0
  58. package/dist/enums.js +38 -0
  59. package/dist/enums.js.map +1 -0
  60. package/dist/errors.d.ts +195 -0
  61. package/dist/errors.d.ts.map +1 -0
  62. package/dist/errors.js +188 -0
  63. package/dist/errors.js.map +1 -0
  64. package/dist/forms-core/autocomplete.d.ts +37 -0
  65. package/dist/forms-core/autocomplete.d.ts.map +1 -0
  66. package/dist/forms-core/autocomplete.js +82 -0
  67. package/dist/forms-core/autocomplete.js.map +1 -0
  68. package/dist/forms-core/business-function.d.ts +61 -0
  69. package/dist/forms-core/business-function.d.ts.map +1 -0
  70. package/dist/forms-core/business-function.js +149 -0
  71. package/dist/forms-core/business-function.js.map +1 -0
  72. package/dist/forms-core/calculation.d.ts +72 -0
  73. package/dist/forms-core/calculation.d.ts.map +1 -0
  74. package/dist/forms-core/calculation.js +103 -0
  75. package/dist/forms-core/calculation.js.map +1 -0
  76. package/dist/forms-core/controller.d.ts +249 -0
  77. package/dist/forms-core/controller.d.ts.map +1 -0
  78. package/dist/forms-core/controller.js +590 -0
  79. package/dist/forms-core/controller.js.map +1 -0
  80. package/dist/forms-core/elements.d.ts +44 -0
  81. package/dist/forms-core/elements.d.ts.map +1 -0
  82. package/dist/forms-core/elements.js +68 -0
  83. package/dist/forms-core/elements.js.map +1 -0
  84. package/dist/forms-core/email-domains.d.ts +10 -0
  85. package/dist/forms-core/email-domains.d.ts.map +1 -0
  86. package/dist/forms-core/email-domains.js +69 -0
  87. package/dist/forms-core/email-domains.js.map +1 -0
  88. package/dist/forms-core/file-rules.d.ts +51 -0
  89. package/dist/forms-core/file-rules.d.ts.map +1 -0
  90. package/dist/forms-core/file-rules.js +105 -0
  91. package/dist/forms-core/file-rules.js.map +1 -0
  92. package/dist/forms-core/formula.d.ts +114 -0
  93. package/dist/forms-core/formula.d.ts.map +1 -0
  94. package/dist/forms-core/formula.js +1170 -0
  95. package/dist/forms-core/formula.js.map +1 -0
  96. package/dist/forms-core/index.d.ts +24 -0
  97. package/dist/forms-core/index.d.ts.map +1 -0
  98. package/dist/forms-core/index.js +28 -0
  99. package/dist/forms-core/index.js.map +1 -0
  100. package/dist/forms-core/units.d.ts +49 -0
  101. package/dist/forms-core/units.d.ts.map +1 -0
  102. package/dist/forms-core/units.js +255 -0
  103. package/dist/forms-core/units.js.map +1 -0
  104. package/dist/forms-core/us-states.d.ts +14 -0
  105. package/dist/forms-core/us-states.d.ts.map +1 -0
  106. package/dist/forms-core/us-states.js +66 -0
  107. package/dist/forms-core/us-states.js.map +1 -0
  108. package/dist/forms-core/validation.d.ts +266 -0
  109. package/dist/forms-core/validation.d.ts.map +1 -0
  110. package/dist/forms-core/validation.js +850 -0
  111. package/dist/forms-core/validation.js.map +1 -0
  112. package/dist/forms.d.ts +19 -0
  113. package/dist/forms.d.ts.map +1 -0
  114. package/dist/forms.js +29 -0
  115. package/dist/forms.js.map +1 -0
  116. package/dist/gallery-types.d.ts +32 -0
  117. package/dist/gallery-types.d.ts.map +1 -0
  118. package/dist/gallery-types.js +2 -0
  119. package/dist/gallery-types.js.map +1 -0
  120. package/dist/graphql.d.ts +97 -0
  121. package/dist/graphql.d.ts.map +1 -0
  122. package/dist/graphql.js +212 -0
  123. package/dist/graphql.js.map +1 -0
  124. package/dist/index.cjs +19448 -0
  125. package/dist/index.d.cts +19 -0
  126. package/dist/index.d.mts +19 -0
  127. package/dist/index.d.ts +19 -0
  128. package/dist/index.d.ts.map +1 -0
  129. package/dist/index.js +20 -0
  130. package/dist/index.js.map +1 -0
  131. package/dist/marketing-schema.d.ts +216 -0
  132. package/dist/marketing-schema.d.ts.map +1 -0
  133. package/dist/marketing-schema.js +200 -0
  134. package/dist/marketing-schema.js.map +1 -0
  135. package/dist/mcp.d.ts +42 -0
  136. package/dist/mcp.d.ts.map +1 -0
  137. package/dist/mcp.js +87 -0
  138. package/dist/mcp.js.map +1 -0
  139. package/dist/native.d.ts +45 -0
  140. package/dist/native.d.ts.map +1 -0
  141. package/dist/native.js +43 -0
  142. package/dist/native.js.map +1 -0
  143. package/dist/next-revalidate.d.ts +43 -0
  144. package/dist/next-revalidate.d.ts.map +1 -0
  145. package/dist/next-revalidate.js +84 -0
  146. package/dist/next-revalidate.js.map +1 -0
  147. package/dist/parallel-pages.d.ts +102 -0
  148. package/dist/parallel-pages.d.ts.map +1 -0
  149. package/dist/parallel-pages.js +108 -0
  150. package/dist/parallel-pages.js.map +1 -0
  151. package/dist/payment-icons.d.ts +66 -0
  152. package/dist/payment-icons.d.ts.map +1 -0
  153. package/dist/payment-icons.js +160 -0
  154. package/dist/payment-icons.js.map +1 -0
  155. package/dist/proxy.d.ts +67 -0
  156. package/dist/proxy.d.ts.map +1 -0
  157. package/dist/proxy.js +97 -0
  158. package/dist/proxy.js.map +1 -0
  159. package/dist/react-analytics.d.ts +39 -0
  160. package/dist/react-analytics.d.ts.map +1 -0
  161. package/dist/react-analytics.js +59 -0
  162. package/dist/react-analytics.js.map +1 -0
  163. package/dist/react-attribution.d.ts +99 -0
  164. package/dist/react-attribution.d.ts.map +1 -0
  165. package/dist/react-attribution.js +52 -0
  166. package/dist/react-attribution.js.map +1 -0
  167. package/dist/react-marketing.d.ts +181 -0
  168. package/dist/react-marketing.d.ts.map +1 -0
  169. package/dist/react-marketing.js +215 -0
  170. package/dist/react-marketing.js.map +1 -0
  171. package/dist/react.d.ts +727 -0
  172. package/dist/react.d.ts.map +1 -0
  173. package/dist/react.js +2418 -0
  174. package/dist/react.js.map +1 -0
  175. package/dist/reserved-paths.d.ts +24 -0
  176. package/dist/reserved-paths.d.ts.map +1 -0
  177. package/dist/reserved-paths.js +60 -0
  178. package/dist/reserved-paths.js.map +1 -0
  179. package/dist/revalidate-verify.d.ts +30 -0
  180. package/dist/revalidate-verify.d.ts.map +1 -0
  181. package/dist/revalidate-verify.js +74 -0
  182. package/dist/revalidate-verify.js.map +1 -0
  183. package/dist/sdk.d.ts +678 -0
  184. package/dist/sdk.d.ts.map +1 -0
  185. package/dist/sdk.js +801 -0
  186. package/dist/sdk.js.map +1 -0
  187. package/dist/seo.d.ts +218 -0
  188. package/dist/seo.d.ts.map +1 -0
  189. package/dist/seo.js +309 -0
  190. package/dist/seo.js.map +1 -0
  191. package/dist/socials.d.ts +47 -0
  192. package/dist/socials.d.ts.map +1 -0
  193. package/dist/socials.js +172 -0
  194. package/dist/socials.js.map +1 -0
  195. package/dist/static.d.ts +14 -0
  196. package/dist/static.d.ts.map +1 -0
  197. package/dist/static.js +14 -0
  198. package/dist/static.js.map +1 -0
  199. package/dist/url-pattern.d.ts +47 -0
  200. package/dist/url-pattern.d.ts.map +1 -0
  201. package/dist/url-pattern.js +124 -0
  202. package/dist/url-pattern.js.map +1 -0
  203. package/dist/vanilla.d.ts +77 -0
  204. package/dist/vanilla.d.ts.map +1 -0
  205. package/dist/vanilla.js +903 -0
  206. package/dist/vanilla.js.map +1 -0
  207. package/openapi.json +13779 -0
  208. package/package.json +204 -0
  209. package/src/adapters-revalidate.ts +88 -0
  210. package/src/alpha.ts +89 -0
  211. package/src/analytics-core.ts +175 -0
  212. package/src/analytics-server.ts +99 -0
  213. package/src/angular/biab-form.component.ts +1443 -0
  214. package/src/angular/index.ts +26 -0
  215. package/src/attribution-logo.ts +5 -0
  216. package/src/auth-handler.ts +352 -0
  217. package/src/cli.ts +2517 -0
  218. package/src/client.ts +3578 -0
  219. package/src/collections.ts +101 -0
  220. package/src/contracts.ts +3390 -0
  221. package/src/data-model-schema.ts +4042 -0
  222. package/src/distribution.ts +73 -0
  223. package/src/element.ts +321 -0
  224. package/src/enums.ts +65 -0
  225. package/src/errors.ts +338 -0
  226. package/src/forms-core/autocomplete.ts +97 -0
  227. package/src/forms-core/business-function.ts +209 -0
  228. package/src/forms-core/calculation.ts +167 -0
  229. package/src/forms-core/controller.ts +922 -0
  230. package/src/forms-core/elements.ts +86 -0
  231. package/src/forms-core/email-domains.ts +68 -0
  232. package/src/forms-core/file-rules.ts +123 -0
  233. package/src/forms-core/formula.ts +1339 -0
  234. package/src/forms-core/index.ts +122 -0
  235. package/src/forms-core/units.ts +308 -0
  236. package/src/forms-core/us-states.ts +68 -0
  237. package/src/forms-core/validation.ts +1037 -0
  238. package/src/forms.ts +63 -0
  239. package/src/gallery-types.ts +31 -0
  240. package/src/graphql.ts +295 -0
  241. package/src/index.ts +505 -0
  242. package/src/marketing-schema.ts +395 -0
  243. package/src/mcp.ts +108 -0
  244. package/src/native.ts +73 -0
  245. package/src/next-revalidate.ts +124 -0
  246. package/src/parallel-pages.ts +192 -0
  247. package/src/payment-icons.ts +205 -0
  248. package/src/proxy.ts +173 -0
  249. package/src/qwik.tsx +1234 -0
  250. package/src/react-analytics.tsx +76 -0
  251. package/src/react-attribution.tsx +192 -0
  252. package/src/react-marketing.tsx +338 -0
  253. package/src/react.tsx +5495 -0
  254. package/src/reserved-paths.ts +65 -0
  255. package/src/revalidate-verify.ts +98 -0
  256. package/src/sdk.ts +1671 -0
  257. package/src/seo.ts +488 -0
  258. package/src/socials.ts +230 -0
  259. package/src/solid.tsx +1466 -0
  260. package/src/static.ts +21 -0
  261. package/src/styles/biab-forms.css +484 -0
  262. package/src/svelte/BiabForm.svelte +1069 -0
  263. package/src/svelte/context.ts +57 -0
  264. package/src/svelte/index.ts +79 -0
  265. package/src/url-pattern.ts +153 -0
  266. package/src/vanilla.ts +1290 -0
  267. package/src/vue/BiabForm.vue +1538 -0
  268. package/src/vue/index.ts +119 -0
@@ -0,0 +1,2428 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Declare a server-owned enum on a RESPONSE field.
4
+ *
5
+ * ## Why this is not `z.enum([...])`
6
+ *
7
+ * Response schemas here are parsed at runtime — `client.ts` ends every request
8
+ * with `options.responseSchema.parse(body)`, and `.parse` **throws**. So a
9
+ * `z.enum` on a response is a promise that the set never grows. It does grow:
10
+ * these mirror pg enums, and a migration can add a member and ship long before
11
+ * a consumer upgrades their pinned `@businessdash/sdk`. The old value keeps
12
+ * working; the new one takes down every call that returns a row carrying it.
13
+ *
14
+ * So runtime stays permissive (`z.string()` — a string is still a string) and
15
+ * the precision goes into the TYPE, where being wrong costs a stale hint
16
+ * instead of an outage. The cast is sound rather than a convenience: every
17
+ * `string` genuinely is an `OpenEnum<T>`.
18
+ *
19
+ * The known members aren't lost — they ride along in `.describe()`, so they
20
+ * reach `openapi.json` and the clients generated from it.
21
+ *
22
+ * Use `z.enum` freely on REQUEST fields: rejecting a bad value the caller
23
+ * just typed is the entire point, and the blast radius is one call.
24
+ */
25
+ export function openEnum(...values) {
26
+ return z
27
+ .string()
28
+ .describe(`Known values: ${values.join(" | ")}. The server may add more — treat unrecognised values as forward-compatible rather than invalid.`);
29
+ }
30
+ export const siteIdSchema = z.string().uuid();
31
+ export const collectionIdSchema = z.string().uuid();
32
+ export const rowIdSchema = z.string().uuid();
33
+ export const fieldIdSchema = z.string().uuid();
34
+ export const apiKeyIdSchema = z.string().uuid();
35
+ export const timestampSchema = z.string().min(1);
36
+ export const collectionSlugSchema = z
37
+ .string()
38
+ .min(1)
39
+ .max(80)
40
+ .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/);
41
+ export const collectionFieldNameSchema = z
42
+ .string()
43
+ .min(1)
44
+ .max(80)
45
+ .regex(/^[a-zA-Z][a-zA-Z0-9_]*$/);
46
+ export const collectionFieldTypeValues = [
47
+ "string",
48
+ "number",
49
+ "boolean",
50
+ "json",
51
+ "date",
52
+ "richtext",
53
+ ];
54
+ export const collectionFieldTypeSchema = z.enum(collectionFieldTypeValues);
55
+ export const jsonValueSchema = z.lazy(() => z.union([
56
+ z.string(),
57
+ z.number(),
58
+ z.boolean(),
59
+ z.null(),
60
+ z.record(z.unknown()),
61
+ z.array(z.unknown()),
62
+ ]));
63
+ export const collectionFieldInputSchema = z.object({
64
+ name: collectionFieldNameSchema,
65
+ label: z.string().max(120).optional(),
66
+ type: collectionFieldTypeSchema,
67
+ required: z.boolean().optional(),
68
+ searchable: z.boolean().optional(),
69
+ queryable: z.boolean().optional(),
70
+ validation: z.record(z.unknown()).optional(),
71
+ displayConfig: z.record(z.unknown()).optional(),
72
+ position: z.number().int().min(0).optional(),
73
+ });
74
+ export const collectionFieldSchema = z.object({
75
+ id: fieldIdSchema,
76
+ tableId: collectionIdSchema,
77
+ siteId: siteIdSchema,
78
+ name: collectionFieldNameSchema,
79
+ label: z.string().max(120).nullable().optional(),
80
+ type: collectionFieldTypeSchema,
81
+ required: z.boolean(),
82
+ searchable: z.boolean(),
83
+ queryable: z.boolean(),
84
+ validation: z.record(z.unknown()).nullable().optional(),
85
+ displayConfig: z.record(z.unknown()).nullable().optional(),
86
+ position: z.number().int(),
87
+ createdAt: timestampSchema,
88
+ updatedAt: timestampSchema,
89
+ });
90
+ export const collectionSchema = z.object({
91
+ id: collectionIdSchema,
92
+ siteId: siteIdSchema,
93
+ name: z.string().min(1).max(120),
94
+ slug: collectionSlugSchema,
95
+ description: z.string().max(500).nullable().optional(),
96
+ createdByUserId: z.string().uuid().nullable().optional(),
97
+ createdAt: timestampSchema,
98
+ updatedAt: timestampSchema,
99
+ });
100
+ export const createCollectionInputSchema = z.object({
101
+ name: z.string().min(1).max(120),
102
+ slug: collectionSlugSchema.optional(),
103
+ description: z.string().max(500).optional(),
104
+ fields: z.array(collectionFieldInputSchema).max(64).optional(),
105
+ });
106
+ export const createCollectionResponseSchema = z.object({
107
+ collection: collectionSchema,
108
+ fields: z.array(collectionFieldSchema),
109
+ });
110
+ export const listCollectionsResponseSchema = z.object({
111
+ collections: z.array(collectionSchema),
112
+ });
113
+ export const getCollectionResponseSchema = z.object({
114
+ collection: collectionSchema,
115
+ fields: z.array(collectionFieldSchema),
116
+ });
117
+ export const rowDataSchema = z.record(z.unknown());
118
+ export const collectionRowSchema = z.object({
119
+ id: rowIdSchema,
120
+ tableId: collectionIdSchema,
121
+ siteId: siteIdSchema,
122
+ data: rowDataSchema,
123
+ createdByUserId: z.string().uuid().nullable().optional(),
124
+ updatedByUserId: z.string().uuid().nullable().optional(),
125
+ createdAt: timestampSchema,
126
+ updatedAt: timestampSchema,
127
+ });
128
+ export const listRowsInputSchema = z.object({
129
+ limit: z.number().int().min(1).max(100).optional(),
130
+ });
131
+ export const listRowsResponseSchema = z.object({
132
+ rows: z.array(collectionRowSchema),
133
+ });
134
+ export const upsertRowInputSchema = z.object({
135
+ rowId: rowIdSchema.optional(),
136
+ data: rowDataSchema,
137
+ });
138
+ export const upsertRowResponseSchema = z.object({
139
+ row: collectionRowSchema,
140
+ });
141
+ export const queryOperatorValues = ["equals", "contains"];
142
+ export const queryOperatorSchema = z.enum(queryOperatorValues);
143
+ export const queryFilterSchema = z.object({
144
+ fieldName: collectionFieldNameSchema,
145
+ value: z.union([z.string().min(1), z.number(), z.boolean()]),
146
+ operator: queryOperatorSchema.optional(),
147
+ });
148
+ export const queryRowsInputSchema = z.object({
149
+ filters: z.array(queryFilterSchema).min(1).max(10),
150
+ limit: z.number().int().min(1).max(100).optional(),
151
+ });
152
+ export const queryRowsResponseSchema = z.object({
153
+ rows: z.array(collectionRowSchema),
154
+ });
155
+ export const siteAssetInputSchema = z.object({
156
+ filename: z.string().min(1),
157
+ url: z.string().url(),
158
+ mimeType: z.string().optional(),
159
+ sizeBytes: z.number().int().nonnegative().optional(),
160
+ altText: z.string().optional(),
161
+ folder: z.string().optional(),
162
+ });
163
+ export const siteAssetSchema = z.object({
164
+ id: z.number().int(),
165
+ siteId: siteIdSchema,
166
+ filename: z.string(),
167
+ url: z.string().url(),
168
+ mimeType: z.string().nullable().optional(),
169
+ sizeBytes: z.number().int().nullable().optional(),
170
+ altText: z.string().nullable().optional(),
171
+ folder: z.string().nullable().optional(),
172
+ createdAt: timestampSchema,
173
+ });
174
+ export const uploadSiteAssetResponseSchema = z.object({
175
+ asset: siteAssetSchema,
176
+ });
177
+ export const followerSchema = z.object({
178
+ id: z.string().uuid(),
179
+ email: z.string().email(),
180
+ name: z.string().nullable().optional(),
181
+ source: z.string().nullable().optional(),
182
+ isActive: z.boolean(),
183
+ joinedAt: timestampSchema,
184
+ unsubscribedAt: timestampSchema.nullable().optional(),
185
+ });
186
+ export const followerJoinInputSchema = z.object({
187
+ email: z.string().email(),
188
+ name: z.string().min(1).max(200).optional(),
189
+ source: z.string().min(1).max(80).optional(),
190
+ });
191
+ export const followerJoinResponseSchema = z.object({
192
+ follower: followerSchema,
193
+ created: z.boolean(),
194
+ });
195
+ export const followerMeInputSchema = z.object({
196
+ email: z.string().email(),
197
+ });
198
+ export const followerMeResponseSchema = z.object({
199
+ follower: followerSchema.nullable(),
200
+ });
201
+ export const followerEditInputSchema = z.object({
202
+ email: z.string().email(),
203
+ patch: z.object({
204
+ name: z.string().min(1).max(200).optional(),
205
+ source: z.string().min(1).max(80).optional(),
206
+ }),
207
+ });
208
+ export const followerEditResponseSchema = z.object({
209
+ follower: followerSchema,
210
+ });
211
+ export const followerLeaveInputSchema = z.object({
212
+ email: z.string().email(),
213
+ });
214
+ export const followerLeaveResponseSchema = z.object({
215
+ follower: followerSchema.nullable(),
216
+ });
217
+ export const dashboardSessionResponseSchema = z.object({
218
+ url: z.string().url(),
219
+ token: z.string().min(1),
220
+ expiresAt: timestampSchema,
221
+ });
222
+ // ── Storefront / Cart / Checkout / Blog embed sessions ─────────────────────
223
+ const baseEmbedSessionResponse = z.object({
224
+ url: z.string().url(),
225
+ token: z.string().min(1),
226
+ expiresAt: timestampSchema,
227
+ orgSlug: z.string().min(1),
228
+ });
229
+ export const storefrontSessionInputSchema = z
230
+ .object({
231
+ productId: z.string().uuid().optional(),
232
+ })
233
+ .optional();
234
+ export const storefrontSessionResponseSchema = baseEmbedSessionResponse;
235
+ export const cartSessionResponseSchema = baseEmbedSessionResponse;
236
+ export const checkoutLineSchema = z.object({
237
+ productId: z.string().uuid(),
238
+ variantId: z.string().uuid(),
239
+ combinationKey: z.string().optional(),
240
+ quantity: z.number().int().positive(),
241
+ });
242
+ export const checkoutSessionInputSchema = z.object({
243
+ lines: z.array(checkoutLineSchema).min(1),
244
+ customerEmail: z.string().email().optional(),
245
+ successUrl: z.string().url().optional(),
246
+ cancelUrl: z.string().url().optional(),
247
+ });
248
+ export const checkoutSessionResponseSchema = z.object({
249
+ url: z.string().url(),
250
+ stripeUrl: z.string().url(),
251
+ sessionId: z.string().min(1),
252
+ totalAmountCents: z.number().int(),
253
+ currency: z.string().min(1),
254
+ token: z.string().min(1),
255
+ expiresAt: timestampSchema,
256
+ });
257
+ // ── Programmatic checkout-from-cart (Batch II, 0.9.0) ────────────────
258
+ export const checkoutStartInputSchema = z
259
+ .object({
260
+ customerEmail: z.string().email().optional(),
261
+ successUrl: z.string().url().optional(),
262
+ cancelUrl: z.string().url().optional(),
263
+ })
264
+ .optional();
265
+ export const checkoutStartResponseSchema = z.object({
266
+ sessionId: z.string().min(1),
267
+ stripeUrl: z.string().url(),
268
+ totalAmountCents: z.number().int(),
269
+ currency: z.string().min(1),
270
+ });
271
+ export const checkoutStatusResponseSchema = z.object({
272
+ sessionId: z.string().min(1),
273
+ paymentStatus: z.enum(["no_payment_required", "paid", "unpaid"]),
274
+ amountTotalCents: z.number().int().nullable(),
275
+ currency: z.string().nullable(),
276
+ customerEmail: z.string().nullable(),
277
+ customerName: z.string().nullable(),
278
+ });
279
+ export const blogSessionInputSchema = z
280
+ .object({
281
+ postSlug: z.string().min(1).max(200).optional(),
282
+ })
283
+ .optional();
284
+ export const blogSessionResponseSchema = baseEmbedSessionResponse;
285
+ // ── Chatbot ────────────────────────────────────────────────────────────────
286
+ export const chatbotSessionResponseSchema = baseEmbedSessionResponse;
287
+ /** How proactively the bot should suggest "Talk to a person" — server-driven. */
288
+ export const chatbotHandoffThresholdSchema = z.enum([
289
+ "never",
290
+ "low",
291
+ "medium",
292
+ "high",
293
+ "always",
294
+ ]);
295
+ export const chatbotConfigSchema = z.object({
296
+ systemPrompt: z.string(),
297
+ knowledgeBase: z.string(),
298
+ model: z.string().min(1),
299
+ temperature: z.number().min(0).max(2),
300
+ lastUpdated: timestampSchema.nullable(),
301
+ usingDefaults: z.boolean(),
302
+ /**
303
+ * Live-handoff dial. The host bakes the matching guidance into the
304
+ * system prompt; SDK consumers can still branch on it to render their
305
+ * own UI hints.
306
+ */
307
+ handoffThreshold: chatbotHandoffThresholdSchema.default("medium"),
308
+ /**
309
+ * `true` when the org's subscription plan unlocks live front-desk
310
+ * handoff (Growth+ on the host's pricing). Use this to render a
311
+ * "Talk to a person" button or hide it when the org is on a tier that
312
+ * is bot-only.
313
+ */
314
+ frontDeskEnabled: z.boolean().default(false),
315
+ /**
316
+ * `true` while the org's visitor file-upload "receive window" is open. Use it
317
+ * to show/hide an upload affordance. Uploads also require the
318
+ * `chat.file_uploads` entitlement (enforced on the upload route).
319
+ */
320
+ fileUploadsEnabled: z.boolean().default(false),
321
+ /**
322
+ * Whether/how the chat should ask the visitor for an email: `"request"` is a
323
+ * skippable ask, `"require"` blocks the first message until one is provided.
324
+ */
325
+ emailCaptureMode: z.enum(["off", "request", "require"]).default("off"),
326
+ });
327
+ /** Response from the chat file-upload presign endpoint. */
328
+ export const chatbotUploadUrlResponseSchema = z.object({
329
+ uploadUrl: z.string(),
330
+ mediaId: z.string(),
331
+ publicUrl: z.string(),
332
+ storageId: z.string(),
333
+ uploadExpiresAt: z.string(),
334
+ });
335
+ export const chatbotConfigUpdateInputSchema = z.object({
336
+ systemPrompt: z.string().min(1).max(10_000).optional(),
337
+ knowledgeBase: z.string().max(50_000).optional(),
338
+ model: z.string().min(1).max(120).optional(),
339
+ temperature: z.number().min(0).max(2).optional(),
340
+ });
341
+ export const chatbotMessageSchema = z.object({
342
+ role: z.enum(["user", "assistant", "system"]),
343
+ content: z.string().max(16_000),
344
+ });
345
+ /**
346
+ * Per-call provider override (BYO key). When set, the turn runs against the
347
+ * SDK consumer's own LLM account — their `apiKey`, their billing — instead
348
+ * of the BIAB platform's configured provider, and the platform's monthly
349
+ * token budget is bypassed for that call.
350
+ *
351
+ * **Server-only.** Never send this from a browser: the key would be visible
352
+ * in DevTools. Hold it in your server env (e.g. `process.env.OPENAI_API_KEY`)
353
+ * and pass it on the server-side `client.chatbot.chat()` call. The
354
+ * `useChatbot` browser hook deliberately does not surface this field.
355
+ */
356
+ export const chatbotByoConfigSchema = z.object({
357
+ provider: z.enum(["openai", "anthropic", "grok"]),
358
+ apiKey: z.string().min(1).max(500),
359
+ model: z.string().min(1).max(120).optional(),
360
+ });
361
+ export const chatbotChatInputSchema = z.object({
362
+ messages: z.array(chatbotMessageSchema).min(1).max(40),
363
+ /** See {@link chatbotByoConfigSchema}. Server-side use only. */
364
+ byo: chatbotByoConfigSchema.optional(),
365
+ });
366
+ /**
367
+ * Side-effect the assistant requested mid-turn. The SDK consumer renders
368
+ * each action — currently only `show_form`, which the consumer resolves by
369
+ * fetching the form definition and rendering inline in the chat thread.
370
+ */
371
+ export const chatbotUiActionSchema = z.discriminatedUnion("kind", [
372
+ z.object({
373
+ kind: z.literal("show_form"),
374
+ formSlug: z.string().min(1).max(120),
375
+ }),
376
+ ]);
377
+ /**
378
+ * Live front-desk availability for the org. Mirrors what the public-site
379
+ * widget surfaces in its presence strip; SDK consumers can render a
380
+ * "Talk to a person" button conditional on `staffAvailable`, and a
381
+ * wait-time hint from `estimateLabel`.
382
+ */
383
+ export const chatbotAvailabilitySchema = z.object({
384
+ frontDeskEnabled: z.boolean(),
385
+ staffAvailable: z.boolean(),
386
+ offline: z.boolean(),
387
+ estimateLabel: z.string().nullable(),
388
+ onlineHandlers: z.array(z.object({
389
+ userId: z.string(),
390
+ name: z.string(),
391
+ avatarUrl: z.string().nullable(),
392
+ })),
393
+ });
394
+ export const chatbotChatResponseSchema = z.object({
395
+ text: z.string(),
396
+ model: z.string(),
397
+ usingDefaults: z.boolean(),
398
+ uiActions: z.array(chatbotUiActionSchema).default([]),
399
+ availability: chatbotAvailabilitySchema.optional(),
400
+ /** True when the turn ran against a BYO provider key (consumer-paid). */
401
+ byo: z.boolean().default(false),
402
+ });
403
+ /**
404
+ * Minimal field shape the SDK needs to render an inline form. Mirrors the
405
+ * host's `FormField` type but kept loose (only the keys the renderer
406
+ * actually reads) so additive server-side changes don't break old SDK
407
+ * builds.
408
+ */
409
+ export const chatbotFormFieldSchema = z
410
+ .object({
411
+ id: z.string(),
412
+ label: z.string(),
413
+ type: z.string(),
414
+ required: z.boolean().default(false),
415
+ placeholder: z.string().optional(),
416
+ helpText: z.string().optional(),
417
+ options: z.array(z.string()).optional(),
418
+ })
419
+ .passthrough();
420
+ export const chatbotFormDefinitionSchema = z.object({
421
+ id: z.string(),
422
+ slug: z.string(),
423
+ name: z.string(),
424
+ description: z.string(),
425
+ fields: z.array(chatbotFormFieldSchema),
426
+ addsInquiry: z.boolean(),
427
+ addsLead: z.boolean(),
428
+ });
429
+ export const chatbotFormSubmitInputSchema = z.object({
430
+ data: z.record(z.unknown()),
431
+ submitterEmail: z.string().email().optional(),
432
+ submitterName: z.string().optional(),
433
+ priorChat: z
434
+ .array(z.object({
435
+ role: z.string().min(1).max(40),
436
+ content: z.string().min(1).max(16_000),
437
+ createdAt: z.number().optional(),
438
+ }))
439
+ .max(200)
440
+ .optional(),
441
+ });
442
+ export const chatbotFormSubmitResponseSchema = z.object({
443
+ success: z.literal(true),
444
+ submissionId: z.string(),
445
+ slug: z.string(),
446
+ });
447
+ // ── Front Desk programmatic surface (Batch JJ, 0.9.0) ────────────────
448
+ export const chatbotPersistedSessionInputSchema = z.object({
449
+ visitorToken: z.string().min(8).max(200),
450
+ visitorName: z.string().max(200).optional(),
451
+ visitorEmail: z.string().email().optional(),
452
+ });
453
+ export const chatbotPersistedSessionResponseSchema = z.object({
454
+ sessionId: z.string().uuid(),
455
+ /** Mirrors the `chat_session_status` pg enum. */
456
+ status: openEnum("bot", "awaiting_human", "assigned", "closed"),
457
+ startedAt: timestampSchema,
458
+ visitorToken: z.string(),
459
+ });
460
+ export const chatbotPersistedMessageSchema = z.object({
461
+ id: z.string(),
462
+ /** Mirrors the `chat_message_role` pg enum. */
463
+ role: openEnum("visitor", "assistant_bot", "staff", "system", "staff_note"),
464
+ content: z.string(),
465
+ createdAt: timestampSchema,
466
+ authorUserId: z.string().nullable(),
467
+ });
468
+ export const chatbotSessionStatusSchema = z.object({
469
+ sessionId: z.string(),
470
+ /** Mirrors the `chat_session_status` pg enum. */
471
+ status: openEnum("bot", "awaiting_human", "assigned", "closed"),
472
+ assignedUserId: z.string().nullable(),
473
+ placeInLine: z.number().int().nullable(),
474
+ stalled: z.boolean(),
475
+ });
476
+ export const chatbotMessagesResponseSchema = z.object({
477
+ session: chatbotSessionStatusSchema,
478
+ messages: z.array(chatbotPersistedMessageSchema),
479
+ });
480
+ export const chatbotAppendMessageInputSchema = z.object({
481
+ sessionId: z.string().uuid(),
482
+ visitorToken: z.string().min(8).max(200),
483
+ content: z.string().min(1).max(16_000),
484
+ role: z.enum(["visitor", "assistant_bot"]).default("visitor"),
485
+ });
486
+ /**
487
+ * Plan-gate body shape. Identical across every gated endpoint —
488
+ * lift this to model upgrade-card props in one place.
489
+ */
490
+ export const planGatedUnavailableSchema = z.object({
491
+ available: z.literal(false),
492
+ reason: z.literal("plan_required"),
493
+ requiredFeature: z.string(),
494
+ requiredPlan: z.string(),
495
+ planDisplayName: z.string(),
496
+ upgradeUrl: z.string(),
497
+ });
498
+ export const chatbotRequestHumanInputSchema = z.object({
499
+ sessionId: z.string().uuid(),
500
+ visitorToken: z.string().min(8).max(200),
501
+ });
502
+ export const chatbotRequestHumanAvailableSchema = z.object({
503
+ available: z.literal(true),
504
+ status: z.string(),
505
+ requestedAt: z.string().nullable(),
506
+ placeInLine: z.number().int().nullable(),
507
+ estimateLabel: z.string().nullable(),
508
+ staffAvailable: z.boolean(),
509
+ onlineHandlers: z.array(z.object({
510
+ userId: z.string(),
511
+ name: z.string(),
512
+ avatarUrl: z.string().nullable(),
513
+ })),
514
+ });
515
+ /**
516
+ * Discriminated union — every Front Desk write returns one of these.
517
+ * Branch on `.available` to render the upgrade CTA vs the queue UI.
518
+ */
519
+ export const chatbotRequestHumanResponseSchema = z.union([
520
+ chatbotRequestHumanAvailableSchema,
521
+ planGatedUnavailableSchema,
522
+ ]);
523
+ export const executeActionInputSchema = z.object({
524
+ payload: z.record(z.unknown()).default({}),
525
+ });
526
+ export const executeActionResponseSchema = z.object({
527
+ actionName: z.string().min(1),
528
+ result: z.unknown(),
529
+ });
530
+ export const siteMarketingPageKeySchema = z
531
+ .string()
532
+ .min(1)
533
+ .max(120)
534
+ .regex(/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/);
535
+ export const siteMarketingPageSchema = z.object({
536
+ siteId: siteIdSchema,
537
+ pageKey: siteMarketingPageKeySchema,
538
+ payload: jsonValueSchema,
539
+ updatedAt: timestampSchema,
540
+ });
541
+ export const getSiteMarketingPageResponseSchema = z.object({
542
+ page: siteMarketingPageSchema.nullable(),
543
+ });
544
+ /**
545
+ * Lightweight directory entry — `pageKey` + `updatedAt` only. Pair with
546
+ * a per-page `get(pageKey)` (or React `useMarketingPage(pageKey)`) to
547
+ * load the payload on demand. Splitting list from content keeps a
548
+ * many-page site from paying the full-payload cost just to render a
549
+ * navigation index.
550
+ */
551
+ export const siteMarketingPageSummarySchema = z.object({
552
+ siteId: siteIdSchema,
553
+ pageKey: siteMarketingPageKeySchema,
554
+ updatedAt: timestampSchema,
555
+ });
556
+ export const listSiteMarketingPagesResponseSchema = z.object({
557
+ pages: z.array(siteMarketingPageSummarySchema),
558
+ });
559
+ export const authIntrospectResponseSchema = z.object({
560
+ apiKeyId: apiKeyIdSchema.optional(),
561
+ orgId: z.string().uuid(),
562
+ userId: z.string().uuid().nullable().optional(),
563
+ siteIds: z.array(siteIdSchema).optional(),
564
+ scopes: z.array(z.string()).default([]),
565
+ expiresAt: timestampSchema.nullable().optional(),
566
+ label: z.string().optional(),
567
+ });
568
+ export const emailTemplateRendererValues = ["native-email-builder"];
569
+ export const emailTemplateRendererSchema = z.enum(emailTemplateRendererValues);
570
+ export const emailTemplateCategoryValues = [
571
+ "system",
572
+ "marketing",
573
+ "transactional",
574
+ ];
575
+ export const emailTemplateCategorySchema = z.enum(emailTemplateCategoryValues);
576
+ export const emailTemplateVariableTypeValues = [
577
+ "string",
578
+ "number",
579
+ "url",
580
+ "currency",
581
+ "date",
582
+ ];
583
+ export const emailTemplateVariableTypeSchema = z.enum(emailTemplateVariableTypeValues);
584
+ export const emailTemplateVariableSchema = z.object({
585
+ key: z.string().min(1),
586
+ label: z.string().min(1),
587
+ type: emailTemplateVariableTypeSchema,
588
+ required: z.boolean(),
589
+ defaultValue: z.unknown().optional(),
590
+ });
591
+ export const emailTemplateSummarySchema = z.object({
592
+ id: z.number().int(),
593
+ name: z.string().min(1),
594
+ slug: z.string().min(1),
595
+ description: z.string().nullable().optional(),
596
+ ownerId: z.string().min(1),
597
+ organizationId: z.string().nullable().optional(),
598
+ renderer: emailTemplateRendererSchema,
599
+ category: emailTemplateCategorySchema,
600
+ isActive: z.boolean(),
601
+ isArchived: z.boolean(),
602
+ createdAt: timestampSchema,
603
+ updatedAt: timestampSchema,
604
+ });
605
+ export const emailTemplateVersionSchema = z.object({
606
+ id: z.number().int(),
607
+ templateId: z.number().int(),
608
+ version: z.number().int(),
609
+ subject: z.string().min(1),
610
+ renderer: emailTemplateRendererSchema,
611
+ unlayerDesign: z.unknown().optional(),
612
+ builderDocument: z.unknown().optional(),
613
+ html: z.string().nullable().optional(),
614
+ reactComponent: z.string().nullable().optional(),
615
+ reactPropsSchema: z.unknown().optional(),
616
+ variables: z.array(emailTemplateVariableSchema),
617
+ createdBy: z.string().min(1),
618
+ createdAt: timestampSchema,
619
+ isPublished: z.boolean(),
620
+ });
621
+ export const listEmailTemplatesInputSchema = z.object({
622
+ category: emailTemplateCategorySchema.optional(),
623
+ activeOnly: z.boolean().optional(),
624
+ });
625
+ export const listEmailTemplatesResponseSchema = z.object({
626
+ templates: z.array(emailTemplateSummarySchema),
627
+ });
628
+ export const getEmailTemplateResponseSchema = z.object({
629
+ template: emailTemplateSummarySchema,
630
+ versions: z.array(emailTemplateVersionSchema),
631
+ });
632
+ export const createEmailTemplateInputSchema = z.object({
633
+ name: z.string().min(1),
634
+ slug: z.string().min(1),
635
+ description: z.string().optional(),
636
+ category: emailTemplateCategorySchema,
637
+ isActive: z.boolean().optional(),
638
+ });
639
+ export const createEmailTemplateResponseSchema = z.object({
640
+ template: emailTemplateSummarySchema,
641
+ });
642
+ export const createEmailTemplateVersionInputSchema = z.object({
643
+ subject: z.string().min(1),
644
+ builderDocument: z.unknown().optional(),
645
+ html: z.string().optional(),
646
+ reactComponent: z.string().optional(),
647
+ reactPropsSchema: z.unknown().optional(),
648
+ variables: z.array(emailTemplateVariableSchema).optional(),
649
+ isPublished: z.boolean().optional(),
650
+ });
651
+ export const createEmailTemplateVersionResponseSchema = z.object({
652
+ version: emailTemplateVersionSchema,
653
+ });
654
+ // ── Customer Portal ──────────────────────────────────────────────────────────
655
+ //
656
+ // The customer portal is host- or session-scoped to one organization. The
657
+ // contracts mirror `(public)/portal/*` routes on the platform and the
658
+ // `customerPortal` tRPC router. Hosts: a tenant subdomain like
659
+ // `app.<slug>.<platform>` resolves to one org; in absence of a slug the
660
+ // caller's session organization id is used.
661
+ export const customerPortalOrgSchema = z.object({
662
+ id: z.string().uuid(),
663
+ name: z.string().nullable(),
664
+ /** @deprecated Internal auth-provider org id — use `id` (the BIAB org id).
665
+ * Kept for backwards compatibility; will be removed in a future release. */
666
+ workosOrganizationId: z.string().nullable().optional(),
667
+ });
668
+ export const customerPortalUserSchema = z.object({
669
+ id: z.string().uuid(),
670
+ email: z.string().nullable(),
671
+ displayName: z.string().nullable(),
672
+ avatarUrl: z.string().nullable(),
673
+ });
674
+ export const customerPortalContextResponseSchema = z.object({
675
+ organization: customerPortalOrgSchema,
676
+ user: customerPortalUserSchema,
677
+ isCustomer: z.boolean(),
678
+ });
679
+ export const customerPortalProfileSchema = z
680
+ .object({
681
+ externalCustomerId: z.string().nullable(),
682
+ profile: z.record(z.unknown()).nullable(),
683
+ lastSeenAt: timestampSchema.nullable(),
684
+ joinedAt: timestampSchema.nullable(),
685
+ })
686
+ .nullable();
687
+ export const customerPortalUpdateProfileInputSchema = z.object({
688
+ displayName: z.string().min(1).max(120).optional(),
689
+ profile: z.record(z.unknown()).optional(),
690
+ });
691
+ export const customerPortalUpdateProfileResponseSchema = z.object({
692
+ ok: z.literal(true),
693
+ });
694
+ export const customerPortalOtherOrgSchema = z.object({
695
+ orgId: z.string().uuid(),
696
+ name: z.string().nullable(),
697
+ /** @deprecated Internal auth-provider org id — use `orgId` (the BIAB org id).
698
+ * Kept for backwards compatibility; will be removed in a future release. */
699
+ workosOrganizationId: z.string().nullable().optional(),
700
+ });
701
+ export const customerPortalOtherOrgsResponseSchema = z.array(customerPortalOtherOrgSchema);
702
+ // ── Customer notification preferences (per-customer, per-channel) ─────
703
+ //
704
+ // The matrix shape is `{ [categoryId]: { email, sms, push, in_app } }`.
705
+ // The server returns a fully-defaulted matrix plus the category metadata
706
+ // so SDK consumers can render the UI without hardcoding either the
707
+ // categories or the channels list — both grow over time.
708
+ export const notificationChannelSchema = z.enum([
709
+ "email",
710
+ "sms",
711
+ "push",
712
+ "in_app",
713
+ ]);
714
+ export const notificationChannelMapSchema = z.object({
715
+ email: z.boolean(),
716
+ sms: z.boolean(),
717
+ push: z.boolean(),
718
+ in_app: z.boolean(),
719
+ });
720
+ export const notificationCategoryDefSchema = z.object({
721
+ id: z.string(),
722
+ label: z.string(),
723
+ description: z.string(),
724
+ defaults: notificationChannelMapSchema,
725
+ required: z.record(notificationChannelSchema, z.literal(true)).optional(),
726
+ });
727
+ export const notificationPreferencesResponseSchema = z.object({
728
+ preferences: z.record(notificationChannelMapSchema),
729
+ categories: z.array(notificationCategoryDefSchema),
730
+ channels: z.array(notificationChannelSchema),
731
+ });
732
+ export const notificationPreferencesUpdateInputSchema = z.object({
733
+ /**
734
+ * Sparse — only the categories / channels you want to flip need to
735
+ * be present. Omitted keys keep their persisted (or default) value.
736
+ */
737
+ preferences: z.record(z.object({
738
+ email: z.boolean().optional(),
739
+ sms: z.boolean().optional(),
740
+ push: z.boolean().optional(),
741
+ in_app: z.boolean().optional(),
742
+ })),
743
+ });
744
+ // ── Customer-portal "work" bundle (jobs / quotes / invoices / contracts / payments) ─────
745
+ export const customerJobSummarySchema = z.object({
746
+ id: z.string(),
747
+ name: z.string().nullable(),
748
+ status: z.string(),
749
+ startDate: z.string().nullable(),
750
+ dueDate: z.string().nullable(),
751
+ statusCompletedAt: z.string().nullable(),
752
+ });
753
+ export const customerQuoteSummarySchema = z.object({
754
+ id: z.string(),
755
+ quoteNumber: z.string(),
756
+ /** Mirrors the `quote_status` pg enum. */
757
+ status: openEnum("draft", "sent", "accepted", "rejected", "converted", "expired"),
758
+ totalAmount: z.number(),
759
+ validUntil: z.string(),
760
+ publicAcceptUrl: z.string().nullable(),
761
+ });
762
+ export const customerInvoiceSummarySchema = z.object({
763
+ id: z.string(),
764
+ invoiceNumber: z.string(),
765
+ /** Mirrors the `invoice_status` pg enum. */
766
+ status: openEnum("draft", "sent", "partial_paid", "paid", "overdue", "cancelled"),
767
+ /** Mirrors the `invoice_job_status` pg enum. */
768
+ jobStatus: openEnum("pending", "in_progress", "complete").nullable(),
769
+ totalAmount: z.number(),
770
+ paidAmount: z.number(),
771
+ balanceDue: z.number(),
772
+ dueDate: z.string(),
773
+ issueDate: z.string(),
774
+ paidAt: z.string().nullable(),
775
+ publicPayUrl: z.string().nullable(),
776
+ });
777
+ export const customerContractSummarySchema = z.object({
778
+ id: z.string(),
779
+ title: z.string(),
780
+ /** Mirrors the `contract_status` pg enum. */
781
+ status: openEnum("draft", "sent", "signed", "expired", "cancelled"),
782
+ sentAt: z.string().nullable(),
783
+ signedAt: z.string().nullable(),
784
+ signingUrl: z.string().nullable(),
785
+ signedDocumentUrl: z.string().nullable(),
786
+ });
787
+ export const customerPaymentSummarySchema = z.object({
788
+ id: z.string(),
789
+ invoiceId: z.string(),
790
+ amount: z.number(),
791
+ /** Mirrors the `payment_method` pg enum. */
792
+ method: openEnum("card", "bank_transfer", "cash", "check", "other"),
793
+ date: z.string(),
794
+ });
795
+ export const customerWorkBundleSchema = z
796
+ .object({
797
+ /** True when the customer's email matches no CRM contact in this org. */
798
+ unlinked: z.boolean(),
799
+ jobs: z.array(customerJobSummarySchema),
800
+ quotes: z.array(customerQuoteSummarySchema),
801
+ invoices: z.array(customerInvoiceSummarySchema),
802
+ contracts: z.array(customerContractSummarySchema),
803
+ payments: z.array(customerPaymentSummarySchema),
804
+ summary: z.object({
805
+ openJobCount: z.number(),
806
+ pendingQuoteCount: z.number(),
807
+ unpaidInvoiceCount: z.number(),
808
+ unpaidBalance: z.number(),
809
+ awaitingSignatureCount: z.number(),
810
+ }),
811
+ })
812
+ .passthrough();
813
+ // ── Customer-portal per-item detail responses ─────────────────────────
814
+ export const customerJobDetailSchema = customerJobSummarySchema.passthrough();
815
+ export const customerQuoteDetailSchema = z
816
+ .object({
817
+ id: z.string(),
818
+ quoteNumber: z.string(),
819
+ /** Mirrors the `quote_status` pg enum. */
820
+ status: openEnum("draft", "sent", "accepted", "rejected", "converted", "expired"),
821
+ items: z.array(z.unknown()),
822
+ subtotalAmount: z.number(),
823
+ discountAmount: z.number(),
824
+ taxRate: z.number(),
825
+ taxAmount: z.number(),
826
+ totalAmount: z.number(),
827
+ validUntil: z.string(),
828
+ publicAcceptUrl: z.string().nullable(),
829
+ notes: z.string().nullable(),
830
+ })
831
+ .passthrough();
832
+ /** One entry in the customer's cross-surface payment history — invoice
833
+ * payments, store-order charges, and refunds (negative amount), newest
834
+ * first. `reference` is the confirmation # (Stripe PaymentIntent id for
835
+ * card, provider refund id, or a manual reference). */
836
+ export const customerPaymentHistoryEntrySchema = z.object({
837
+ id: z.string(),
838
+ kind: z.enum(["invoice_payment", "order_charge", "order_refund"]),
839
+ /** Dollars. Negative for refunds. */
840
+ amount: z.number(),
841
+ /** Integer cents for exact math. Negative for refunds. */
842
+ amountCents: z.number(),
843
+ /** Mirrors the `payment_method` pg enum. */
844
+ method: openEnum("card", "bank_transfer", "cash", "check", "other").nullable(),
845
+ status: z.string(),
846
+ reference: z.string().nullable(),
847
+ occurredAt: z.string(),
848
+ invoiceId: z.string().optional(),
849
+ invoiceNumber: z.string().optional(),
850
+ orderId: z.string().optional(),
851
+ orderNumber: z.string().optional(),
852
+ });
853
+ export const customerPaymentHistoryResponseSchema = z.object({
854
+ items: z.array(customerPaymentHistoryEntrySchema),
855
+ });
856
+ /** A single recorded payment/refund on an invoice. */
857
+ export const customerInvoicePaymentSchema = z.object({
858
+ amount: z.number(),
859
+ /** Mirrors the `payment_method` pg enum. */
860
+ method: openEnum("card", "bank_transfer", "cash", "check", "other"),
861
+ date: z.string(),
862
+ /** Stripe PaymentIntent id (card) or manual reference (check #, etc.). */
863
+ reference: z.string().nullable(),
864
+ /**
865
+ * These three are what the platform writes today, but unlike the other
866
+ * statuses in this file there is no enum behind them — `payments.status` is
867
+ * plain `text` with a `"completed"` default, so the column accepts anything.
868
+ * `openEnum` is the honest shape for that: helpful to callers, and it can't
869
+ * reject a row some other writer put there.
870
+ */
871
+ status: openEnum("completed", "refunded", "failed"),
872
+ });
873
+ export const customerInvoiceDetailSchema = z
874
+ .object({
875
+ id: z.string(),
876
+ invoiceNumber: z.string(),
877
+ /** Mirrors the `invoice_status` pg enum. */
878
+ status: openEnum("draft", "sent", "partial_paid", "paid", "overdue", "cancelled"),
879
+ /** Mirrors the `invoice_job_status` pg enum. */
880
+ jobStatus: openEnum("pending", "in_progress", "complete").nullable(),
881
+ totalAmount: z.number(),
882
+ paidAmount: z.number(),
883
+ balanceDue: z.number(),
884
+ dueDate: z.string(),
885
+ issueDate: z.string(),
886
+ paidAt: z.string().nullable(),
887
+ paymentSchedule: z.unknown().nullable(),
888
+ notes: z.string().nullable(),
889
+ terms: z.string().nullable(),
890
+ publicPayUrl: z.string().nullable(),
891
+ // Optional so the SDK stays compatible with older BIAB deployments
892
+ // that don't return these yet.
893
+ /** Recorded payments/refunds, oldest first. */
894
+ payments: z.array(customerInvoicePaymentSchema).optional(),
895
+ /** Most-recent completed payment reference — the confirmation number. */
896
+ confirmationNumber: z.string().nullable().optional(),
897
+ /** Host-less path to the downloadable invoice PDF. */
898
+ pdfPath: z.string().nullable().optional(),
899
+ })
900
+ .passthrough();
901
+ /**
902
+ * Response of `downloadInvoicePdf` — the invoice rendered as a PDF, base64
903
+ * encoded (the SDK decodes it to bytes for you).
904
+ */
905
+ export const customerInvoicePdfSchema = z.object({
906
+ invoiceNumber: z.string(),
907
+ filename: z.string(),
908
+ contentType: z.string(),
909
+ base64: z.string(),
910
+ });
911
+ export const customerContractDetailSchema = z
912
+ .object({
913
+ id: z.string(),
914
+ title: z.string(),
915
+ /** Mirrors the `contract_status` pg enum. */
916
+ status: openEnum("draft", "sent", "signed", "expired", "cancelled"),
917
+ contractNumber: z.string().nullable(),
918
+ sentAt: z.string().nullable(),
919
+ signedAt: z.string().nullable(),
920
+ signedDocumentUrl: z.string().nullable(),
921
+ signingUrl: z.string().nullable(),
922
+ pdfUrl: z.string().nullable(),
923
+ })
924
+ .passthrough();
925
+ export const customerQuoteActionResponseSchema = z.object({
926
+ ok: z.literal(true),
927
+ status: z.string(),
928
+ dealId: z.string().nullable().optional(),
929
+ });
930
+ /** Optional storefront return pages for the hosted Stripe Checkout. */
931
+ export const customerInvoicePayInputSchema = z.object({
932
+ successUrl: z.string().url().max(2000).optional(),
933
+ cancelUrl: z.string().url().max(2000).optional(),
934
+ });
935
+ /** Response of `payInvoice` — a Stripe-hosted Checkout URL to redirect to. */
936
+ export const customerInvoicePayResponseSchema = z.object({
937
+ invoiceId: z.string(),
938
+ status: z.string(),
939
+ checkoutUrl: z.string().url(),
940
+ checkoutSessionId: z.string(),
941
+ });
942
+ /** Response of `signContract` — a ready, absolute hosted signing URL. */
943
+ export const customerContractSignResponseSchema = z.object({
944
+ contractId: z.string(),
945
+ status: z.string(),
946
+ signingUrl: z.string().url(),
947
+ });
948
+ // ── Customer-portal: referrals (the customer as an affiliate) ──────────
949
+ // Read-only self surface. Counters are real (denormalized on the affiliate
950
+ // row); they read zero / empty until the org's referral attribution +
951
+ // payout pipeline records conversions for this customer.
952
+ /** Roll-up of the customer's referral activity, summed across programs. */
953
+ export const customerReferralStatsSchema = z.object({
954
+ /** True when the customer holds at least one affiliate record. */
955
+ enrolled: z.boolean(),
956
+ programCount: z.number(),
957
+ claimsCount: z.number(),
958
+ conversionsCount: z.number(),
959
+ revenueCents: z.number(),
960
+ earningsCents: z.number(),
961
+ nextPayoutCents: z.number(),
962
+ nextPayoutAt: z.string().nullable(),
963
+ });
964
+ export const customerReferralConversionSchema = z.object({
965
+ id: z.string(),
966
+ programId: z.string(),
967
+ amountCents: z.number(),
968
+ commissionCents: z.number(),
969
+ currency: z.string(),
970
+ /** Mirrors the `referral_conversion_status` pg enum. */
971
+ status: openEnum("pending", "confirmed", "reversed"),
972
+ createdAt: z.string(),
973
+ });
974
+ export const customerReferralHistoryResponseSchema = z.object({
975
+ items: z.array(customerReferralConversionSchema),
976
+ });
977
+ export const customerReferralPayoutSchema = z.object({
978
+ id: z.string(),
979
+ programId: z.string(),
980
+ periodStart: z.string(),
981
+ periodEnd: z.string(),
982
+ amountCents: z.number(),
983
+ feeCents: z.number(),
984
+ totalCents: z.number(),
985
+ currency: z.string(),
986
+ /** Mirrors the `referral_payout_status` pg enum. */
987
+ status: openEnum("upcoming", "pending", "paid", "failed"),
988
+ paidAt: z.string().nullable(),
989
+ });
990
+ export const customerReferralPayoutsResponseSchema = z.object({
991
+ items: z.array(customerReferralPayoutSchema),
992
+ });
993
+ // ── Customer-portal: orders + shipping status tracker ──────────────────
994
+ /** One order in the customer's list, with a shipment roll-up for the chip. */
995
+ export const customerOrderSummarySchema = z.object({
996
+ id: z.string(),
997
+ orderNumber: z.string(),
998
+ /** Mirrors the `order_status` pg enum. */
999
+ status: openEnum("pending", "confirmed", "processing", "packed", "shipped", "delivered", "cancelled", "refunded"),
1000
+ /** Mirrors the `order_payment_status` pg enum. */
1001
+ paymentStatus: openEnum("unpaid", "paid", "partial", "refunded"),
1002
+ totalCents: z.number(),
1003
+ createdAt: z.string(),
1004
+ shipmentCount: z.number(),
1005
+ /** Rolled up across the order's shipments. Mirrors `order_shipment_status`. */
1006
+ overallStatus: openEnum("pending", "label_created", "in_transit", "delivered", "returned"),
1007
+ anyInTransit: z.boolean(),
1008
+ allDelivered: z.boolean(),
1009
+ latestEstimatedDelivery: z.string().nullable(),
1010
+ });
1011
+ export const customerOrderListResponseSchema = z.object({
1012
+ items: z.array(customerOrderSummarySchema),
1013
+ });
1014
+ export const customerOrderItemSchema = z.object({
1015
+ id: z.string(),
1016
+ productName: z.string(),
1017
+ variantName: z.string().nullable(),
1018
+ sku: z.string().nullable(),
1019
+ quantity: z.number(),
1020
+ unitPriceCents: z.number(),
1021
+ totalCents: z.number(),
1022
+ requiresShipping: z.boolean(),
1023
+ });
1024
+ export const customerShipmentSchema = z.object({
1025
+ id: z.string(),
1026
+ carrier: z.string().nullable(),
1027
+ trackingNumber: z.string().nullable(),
1028
+ trackingUrl: z.string().nullable(),
1029
+ serviceName: z.string().nullable(),
1030
+ /** Mirrors the `order_shipment_status` pg enum. */
1031
+ status: openEnum("pending", "label_created", "in_transit", "delivered", "returned"),
1032
+ estimatedDelivery: z.string().nullable(),
1033
+ shippedAt: z.string().nullable(),
1034
+ deliveredAt: z.string().nullable(),
1035
+ isReturn: z.boolean(),
1036
+ /** Append-only carrier-event timeline (one entry per status transition). */
1037
+ statusHistory: z.array(z.unknown()),
1038
+ });
1039
+ /** A return/refund request the customer opened on this order (P10b). */
1040
+ export const customerReturnRequestSchema = z.object({
1041
+ id: z.string(),
1042
+ /**
1043
+ * Mirrors `order_return_request_status`. Note the spelling: this enum uses
1044
+ * `cancelled` (two Ls), while `booking_status` uses `canceled` (one). They
1045
+ * are separate enums and each is spelled as the database spells it.
1046
+ */
1047
+ status: openEnum("requested", "approved", "denied", "refunded", "cancelled"),
1048
+ reason: z.string().nullable(),
1049
+ items: z
1050
+ .array(z.object({
1051
+ orderItemId: z.string(),
1052
+ productName: z.string(),
1053
+ quantity: z.number(),
1054
+ }))
1055
+ .nullable(),
1056
+ customerNote: z.string().nullable(),
1057
+ resolutionNote: z.string().nullable(),
1058
+ createdAt: z.string(),
1059
+ resolvedAt: z.string().nullable(),
1060
+ });
1061
+ export const customerOrderDetailSchema = z.object({
1062
+ id: z.string(),
1063
+ orderNumber: z.string(),
1064
+ /** Mirrors the `order_status` pg enum. */
1065
+ status: openEnum("pending", "confirmed", "processing", "packed", "shipped", "delivered", "cancelled", "refunded"),
1066
+ /** Mirrors the `order_payment_status` pg enum. */
1067
+ paymentStatus: openEnum("unpaid", "paid", "partial", "refunded"),
1068
+ subtotalCents: z.number(),
1069
+ shippingCents: z.number(),
1070
+ taxCents: z.number(),
1071
+ discountCents: z.number(),
1072
+ totalCents: z.number(),
1073
+ createdAt: z.string(),
1074
+ items: z.array(customerOrderItemSchema),
1075
+ shipments: z.array(customerShipmentSchema),
1076
+ // Optional for back-compat with older BIAB deployments.
1077
+ returnRequests: z.array(customerReturnRequestSchema).optional(),
1078
+ });
1079
+ /** Input for `submitReturnRequest`. Omit `items` for a whole-order request. */
1080
+ export const customerReturnRequestInputSchema = z.object({
1081
+ reason: z.string().max(120).optional(),
1082
+ customerNote: z.string().max(2000).optional(),
1083
+ items: z
1084
+ .array(z.object({
1085
+ orderItemId: z.string().uuid(),
1086
+ quantity: z.number().int().positive(),
1087
+ }))
1088
+ .optional(),
1089
+ });
1090
+ /** Response of `submitReturnRequest` — the newly opened request. */
1091
+ export const customerReturnRequestCreatedSchema = z.object({
1092
+ id: z.string().nullable(),
1093
+ /** Mirrors the `order_return_request_status` pg enum. */
1094
+ status: openEnum("requested", "approved", "denied", "refunded", "cancelled"),
1095
+ createdAt: z.string().nullable(),
1096
+ });
1097
+ /** One downloadable digital deliverable for a paid order (P10d). */
1098
+ export const customerDownloadSchema = z.object({
1099
+ productName: z.string(),
1100
+ fileName: z.string().nullable(),
1101
+ /** Short-lived presigned URL (BIAB-hosted) or the org's external link. */
1102
+ url: z.string(),
1103
+ /** Seconds until `url` expires; null for a non-expiring external link. */
1104
+ expiresInSeconds: z.number().nullable(),
1105
+ });
1106
+ /** Response of `getDownloads` — the order's entitlement-checked downloads. */
1107
+ export const customerDownloadsResponseSchema = z.object({
1108
+ items: z.array(customerDownloadSchema),
1109
+ });
1110
+ /** `trackShipment` — stored shipment + a best-effort live carrier status. */
1111
+ export const customerShipmentTrackingSchema = z.object({
1112
+ id: z.string(),
1113
+ orderId: z.string().nullable(),
1114
+ carrier: z.string().nullable(),
1115
+ trackingNumber: z.string().nullable(),
1116
+ trackingUrl: z.string().nullable(),
1117
+ serviceName: z.string().nullable(),
1118
+ /** Mirrors the `order_shipment_status` pg enum. */
1119
+ status: openEnum("pending", "label_created", "in_transit", "delivered", "returned"),
1120
+ estimatedDelivery: z.string().nullable(),
1121
+ shippedAt: z.string().nullable(),
1122
+ deliveredAt: z.string().nullable(),
1123
+ statusHistory: z.array(z.unknown()),
1124
+ /** Live Shippo tracking_status when polled; null otherwise. */
1125
+ liveStatus: z.unknown().nullable(),
1126
+ });
1127
+ // ── Storefront branding ────────────────────────────────────────────────
1128
+ /** Org storefront branding — logo + favicon (site media) + org name/icon. */
1129
+ export const siteBrandingSchema = z.object({
1130
+ siteId: z.string(),
1131
+ siteName: z.string().nullable(),
1132
+ logoUrl: z.string().nullable(),
1133
+ faviconUrl: z.string().nullable(),
1134
+ orgName: z.string().nullable(),
1135
+ orgIcon: z.string().nullable(),
1136
+ });
1137
+ // ── Customer-portal: first-party reviews ──────────────────────────────
1138
+ export const customerReviewSummarySchema = z.object({
1139
+ id: z.string(),
1140
+ rating: z.number().int(),
1141
+ title: z.string().nullable(),
1142
+ body: z.string(),
1143
+ /** Mirrors the `first_party_review_status` pg enum. */
1144
+ status: openEnum("pending", "published", "hidden"),
1145
+ jobId: z.string().nullable(),
1146
+ submittedAt: z.string(),
1147
+ publishedAt: z.string().nullable(),
1148
+ });
1149
+ export const customerReviewListResponseSchema = z.object({
1150
+ items: z.array(customerReviewSummarySchema),
1151
+ });
1152
+ export const customerReviewSubmitInputSchema = z.object({
1153
+ rating: z.number().int().min(1).max(5),
1154
+ title: z.string().max(200).optional(),
1155
+ body: z.string().min(1).max(5000),
1156
+ jobId: z.string().uuid().optional(),
1157
+ });
1158
+ export const customerReviewSubmitResponseSchema = z
1159
+ .object({
1160
+ id: z.string().optional(),
1161
+ status: z.string().optional(),
1162
+ submittedAt: z.string().nullable().optional(),
1163
+ })
1164
+ .passthrough();
1165
+ // ── Customer-portal: per-job comments ─────────────────────────────────
1166
+ export const customerJobCommentSchema = z.object({
1167
+ id: z.string(),
1168
+ authorKind: z.enum(["customer", "staff", "system"]),
1169
+ authorName: z.string().nullable(),
1170
+ body: z.string(),
1171
+ createdAt: z.string(),
1172
+ });
1173
+ export const customerJobCommentsListResponseSchema = z.object({
1174
+ items: z.array(customerJobCommentSchema),
1175
+ });
1176
+ export const customerJobCommentPostInputSchema = z.object({
1177
+ body: z.string().min(1).max(5000),
1178
+ });
1179
+ // ── Customer-portal: contact-the-company (front-desk session start) ───
1180
+ export const customerContactCompanyInputSchema = z.object({
1181
+ message: z.string().min(1).max(8000),
1182
+ subject: z.string().max(200).optional(),
1183
+ });
1184
+ export const customerContactCompanyResponseSchema = z.object({
1185
+ sessionId: z.string(),
1186
+ visitorToken: z.string(),
1187
+ status: z.string(),
1188
+ });
1189
+ // ── Customer-portal: staff ETA on a job ────────────────────────────────
1190
+ export const customerJobEtaAssignmentSchema = z.object({
1191
+ displayName: z.string(),
1192
+ role: z.string().nullable(),
1193
+ dispatchStatus: z.string(),
1194
+ lastKnownLocation: z
1195
+ .object({
1196
+ lat: z.number(),
1197
+ lng: z.number(),
1198
+ capturedAt: z.string(),
1199
+ })
1200
+ .nullable(),
1201
+ });
1202
+ export const customerJobEtaResponseSchema = z.object({
1203
+ assignments: z.array(customerJobEtaAssignmentSchema),
1204
+ eta: z
1205
+ .object({
1206
+ seconds: z.number().nullable(),
1207
+ distanceMeters: z.number().nullable(),
1208
+ snapshotAt: z.string(),
1209
+ source: z.literal("dispatch_offer_snapshot"),
1210
+ })
1211
+ .nullable(),
1212
+ liveEtaAvailable: z.boolean(),
1213
+ });
1214
+ // ── Customer-portal: job activity feed (events + chat interleaved) ────
1215
+ export const customerJobActivityEntrySchema = z.discriminatedUnion("kind", [
1216
+ z
1217
+ .object({
1218
+ kind: z.literal("event"),
1219
+ id: z.string(),
1220
+ at: z.string(),
1221
+ event: z.object({
1222
+ eventKind: z.string(),
1223
+ title: z.string(),
1224
+ body: z.string().nullable(),
1225
+ actorName: z.string().nullable(),
1226
+ metadata: z.record(z.string(), z.unknown()).nullable(),
1227
+ }),
1228
+ })
1229
+ .passthrough(),
1230
+ z
1231
+ .object({
1232
+ kind: z.literal("chat"),
1233
+ id: z.string(),
1234
+ at: z.string(),
1235
+ chat: z.object({
1236
+ role: z.string(),
1237
+ content: z.string(),
1238
+ sessionId: z.string(),
1239
+ }),
1240
+ })
1241
+ .passthrough(),
1242
+ ]);
1243
+ export const customerJobActivityResponseSchema = z.object({
1244
+ items: z.array(customerJobActivityEntrySchema),
1245
+ });
1246
+ // ── Blog programmatic surface (0.9.0) ─────────────────────────────────
1247
+ export const blogPublicPostSchema = z
1248
+ .object({
1249
+ id: z.string(),
1250
+ orgId: z.string().nullable(),
1251
+ title: z.string(),
1252
+ slug: z.string(),
1253
+ content: z.string(),
1254
+ excerpt: z.string().nullable(),
1255
+ imageUrl: z.string().nullable(),
1256
+ imageUrlDark: z.string().nullable(),
1257
+ muxPlaybackId: z.string().nullable(),
1258
+ muxStatus: z.string().nullable(),
1259
+ accessLevel: z.enum(["public", "followers", "paid"]),
1260
+ downloadAccessLevel: z.enum(["public", "followers", "paid"]),
1261
+ publishedAt: z.string().nullable(),
1262
+ isPinned: z.boolean(),
1263
+ authorName: z.string().nullable(),
1264
+ authorAvatarUrl: z.string().nullable(),
1265
+ tags: z.array(z.string()),
1266
+ likes: z.number(),
1267
+ commentsCount: z.number(),
1268
+ viewCount: z.number(),
1269
+ createdAt: z.string(),
1270
+ updatedAt: z.string(),
1271
+ ctaLabel: z.string().nullable(),
1272
+ ctaHref: z.string().nullable(),
1273
+ ctaDescription: z.string().nullable(),
1274
+ })
1275
+ .passthrough();
1276
+ export const blogListPostsResponseSchema = z.object({
1277
+ items: z.array(blogPublicPostSchema),
1278
+ nextCursor: z.number().nullable(),
1279
+ });
1280
+ export const blogPostBySlugResponseSchema = z.object({
1281
+ post: blogPublicPostSchema,
1282
+ access: z.enum(["granted", "paywall"]),
1283
+ });
1284
+ export const blogCategorySchema = z.object({
1285
+ id: z.string(),
1286
+ name: z.string(),
1287
+ slug: z.string(),
1288
+ description: z.string().nullable(),
1289
+ });
1290
+ export const blogCategoryListResponseSchema = z.object({
1291
+ items: z.array(blogCategorySchema),
1292
+ });
1293
+ export const blogTagListResponseSchema = z.object({
1294
+ items: z.array(z.string()),
1295
+ });
1296
+ export const blogPublicCommentSchema = z.object({
1297
+ id: z.string(),
1298
+ postId: z.string(),
1299
+ parentId: z.string().nullable(),
1300
+ content: z.string(),
1301
+ authorName: z.string().nullable(),
1302
+ authorAvatarUrl: z.string().nullable(),
1303
+ createdAt: z.string(),
1304
+ });
1305
+ export const blogCommentListResponseSchema = z.object({
1306
+ items: z.array(blogPublicCommentSchema),
1307
+ nextCursor: z.number().nullable(),
1308
+ });
1309
+ export const blogCommentPostInputSchema = z.object({
1310
+ content: z.string().min(1).max(5000),
1311
+ parentId: z.string().uuid().optional(),
1312
+ });
1313
+ export const blogCommentPostResponseSchema = z.object({
1314
+ comment: z
1315
+ .object({
1316
+ id: z.string(),
1317
+ postId: z.string(),
1318
+ content: z.string(),
1319
+ isApproved: z.boolean(),
1320
+ /** Mirrors the `blog_comment_screening_status` pg enum. */
1321
+ screeningStatus: openEnum("ok", "flagged", "blocked", "reviewed_ok", "reviewed_blocked"),
1322
+ createdAt: z.string(),
1323
+ })
1324
+ .passthrough(),
1325
+ moderation: z.object({
1326
+ status: z.string(),
1327
+ isApproved: z.boolean(),
1328
+ }),
1329
+ });
1330
+ /** Like state + count returned by GET/POST `blog/posts/:slug/like`. */
1331
+ export const blogLikeResponseSchema = z.object({
1332
+ liked: z.boolean(),
1333
+ likesCount: z.number(),
1334
+ });
1335
+ // ── Storefront programmatic surface (0.9.0) ───────────────────────────
1336
+ /**
1337
+ * Loose-shape storefront product row. Server-side schema has many
1338
+ * columns; we keep this `.passthrough()` so consumers don't break
1339
+ * when new columns ship and so cross-variant fields (deferred to
1340
+ * 0.10) flow through untyped today.
1341
+ */
1342
+ /** Resolved category attached to a product row/card (mirrors a storefront category). */
1343
+ export const storefrontProductCategorySchema = z.object({
1344
+ id: z.string(),
1345
+ name: z.string(),
1346
+ description: z.string().nullable(),
1347
+ });
1348
+ export const storefrontProductRowSchema = z
1349
+ .object({
1350
+ id: z.string(),
1351
+ orgId: z.string(),
1352
+ name: z.string(),
1353
+ isLive: z.boolean(),
1354
+ categoryId: z.string().nullable().optional(),
1355
+ /** Resolved category (name/description), when the product is categorized. */
1356
+ category: storefrontProductCategorySchema.nullable().optional(),
1357
+ images: z.array(z.string()).nullable().optional(),
1358
+ createdAt: z.string(),
1359
+ })
1360
+ .passthrough();
1361
+ export const storefrontListProductsResponseSchema = z.object({
1362
+ items: z.array(storefrontProductRowSchema),
1363
+ nextCursor: z.number().nullable(),
1364
+ });
1365
+ export const storefrontProductDetailSchema = storefrontProductRowSchema.and(z.object({
1366
+ variants: z.array(z.record(z.string(), z.unknown())),
1367
+ productImages: z.array(z.record(z.string(), z.unknown())),
1368
+ variantImages: z.array(z.record(z.string(), z.unknown())),
1369
+ // Cross-variant option groups (Size/Color/…) — the combinatorial picker.
1370
+ crossVariantTitles: z.array(z.record(z.string(), z.unknown())),
1371
+ crossVariants: z.array(z.record(z.string(), z.unknown())),
1372
+ // Raw specifications jsonb (shape is org-defined) — rides on the product
1373
+ // row; surfaced here for typing.
1374
+ specifications: z.unknown().optional(),
1375
+ }));
1376
+ /** A single approved product review. Lenient — server columns evolve. */
1377
+ export const storefrontReviewSchema = z
1378
+ .object({
1379
+ id: z.string(),
1380
+ productId: z.string(),
1381
+ rating: z.number(),
1382
+ title: z.string().nullable().optional(),
1383
+ body: z.string().nullable().optional(),
1384
+ content: z.string().nullable().optional(),
1385
+ reviewerName: z.string().nullable().optional(),
1386
+ createdAt: z.string().nullable().optional(),
1387
+ })
1388
+ .passthrough();
1389
+ export const storefrontProductReviewsResponseSchema = z.object({
1390
+ items: z.array(storefrontReviewSchema),
1391
+ nextCursor: z.number().nullable(),
1392
+ avgRating: z.number(),
1393
+ totalCount: z.number(),
1394
+ });
1395
+ /** A "you may also like" recommendation. */
1396
+ export const storefrontRelatedProductSchema = z
1397
+ .object({
1398
+ id: z.string(),
1399
+ name: z.string(),
1400
+ description: z.string().nullable(),
1401
+ categoryId: z.string().nullable(),
1402
+ coverImageUrl: z.string().nullable(),
1403
+ minPriceDollars: z.number().nullable(),
1404
+ reasons: z.array(z.string()).optional(),
1405
+ })
1406
+ .passthrough();
1407
+ export const storefrontRelatedProductsResponseSchema = z.object({
1408
+ items: z.array(storefrontRelatedProductSchema),
1409
+ });
1410
+ /** A companion/exclusive cross-sell addon for the "complete your X" rail. */
1411
+ export const storefrontAddonSchema = z
1412
+ .object({
1413
+ id: z.string(),
1414
+ addonProductId: z.string(),
1415
+ addonName: z.string(),
1416
+ addonDescription: z.string().nullable(),
1417
+ kind: z.string(),
1418
+ groupLabel: z.string().nullable(),
1419
+ priceCents: z.number().nullable(),
1420
+ originalPriceCents: z.number().nullable(),
1421
+ imageUrl: z.string().nullable(),
1422
+ variants: z.array(z.record(z.string(), z.unknown())),
1423
+ crossVariantTitles: z.array(z.record(z.string(), z.unknown())),
1424
+ })
1425
+ .passthrough();
1426
+ export const storefrontAddonsResponseSchema = z.object({
1427
+ items: z.array(storefrontAddonSchema),
1428
+ });
1429
+ /**
1430
+ * Storefront sort options for the listing grid (mirrors the platform shop demo).
1431
+ */
1432
+ export const storefrontSortValues = [
1433
+ "featured",
1434
+ "newest",
1435
+ "price-asc",
1436
+ "price-desc",
1437
+ "rating-desc",
1438
+ ];
1439
+ /** Enriched grid card — price, ratings, badges, cover image — for the
1440
+ * filterable listing page (`listProductsWithMeta`). */
1441
+ export const storefrontProductCardSchema = z.object({
1442
+ id: z.string(),
1443
+ name: z.string(),
1444
+ description: z.string().nullable(),
1445
+ categoryId: z.string().nullable(),
1446
+ /** Resolved category (name/description), when the product is categorized. */
1447
+ category: storefrontProductCategorySchema.nullable().optional(),
1448
+ coverImage: z.string().nullable(),
1449
+ cheapestPriceCents: z.number().nullable(),
1450
+ comparePriceCents: z.number().nullable(),
1451
+ isOnSale: z.boolean(),
1452
+ isLowStock: z.boolean(),
1453
+ hasMultipleVariants: z.boolean(),
1454
+ avgRating: z.number(),
1455
+ reviewCount: z.number(),
1456
+ isBestSeller: z.boolean(),
1457
+ isNew: z.boolean(),
1458
+ });
1459
+ export const storefrontProductsWithMetaResponseSchema = z.object({
1460
+ items: z.array(storefrontProductCardSchema),
1461
+ categoryCounts: z.array(z.object({ categoryId: z.string(), count: z.number() })),
1462
+ priceRange: z.object({ minDollars: z.number(), maxDollars: z.number() }),
1463
+ });
1464
+ export const storefrontCategorySchema = z.object({
1465
+ id: z.string(),
1466
+ name: z.string(),
1467
+ description: z.string().nullable(),
1468
+ });
1469
+ export const storefrontCategoriesResponseSchema = z.object({
1470
+ items: z.array(storefrontCategorySchema),
1471
+ });
1472
+ // ── Cart programmatic surface (0.9.0) ─────────────────────────────────
1473
+ export const cartItemSnapshotSchema = z.object({
1474
+ id: z.string(),
1475
+ productId: z.string(),
1476
+ variantId: z.string().nullable(),
1477
+ quantity: z.number().int(),
1478
+ options: z.record(z.string(), z.unknown()).nullable(),
1479
+ addedAt: z.string(),
1480
+ updatedAt: z.string(),
1481
+ productName: z.string().nullable(),
1482
+ productImage: z.string().nullable(),
1483
+ variantTitle: z.string().nullable(),
1484
+ unitPrice: z.number(),
1485
+ currency: z.string(),
1486
+ subtotal: z.number(),
1487
+ });
1488
+ export const cartSnapshotSchema = z.object({
1489
+ id: z.string(),
1490
+ orgId: z.string(),
1491
+ visitorToken: z.string().nullable(),
1492
+ customerUserId: z.string().nullable(),
1493
+ couponCode: z.string().nullable(),
1494
+ currency: z.string(),
1495
+ items: z.array(cartItemSnapshotSchema),
1496
+ subtotal: z.number(),
1497
+ itemCount: z.number(),
1498
+ lastActivityAt: z.string(),
1499
+ createdAt: z.string(),
1500
+ updatedAt: z.string(),
1501
+ });
1502
+ export const cartAddItemInputSchema = z.object({
1503
+ productId: z.string().uuid(),
1504
+ variantId: z.string().uuid().optional(),
1505
+ quantity: z.number().int().min(1).max(999).default(1),
1506
+ options: z.record(z.string(), z.unknown()).nullable().optional(),
1507
+ });
1508
+ export const cartUpdateItemInputSchema = z.object({
1509
+ quantity: z.number().int().min(0).max(999),
1510
+ });
1511
+ export const cartCouponInputSchema = z.object({
1512
+ code: z.string().min(1).max(80),
1513
+ });
1514
+ // ── Shipping (Batch shipping, 0.9.0) ──────────────────────────────────
1515
+ export const shippingDeliveryAddressSchema = z.object({
1516
+ name: z.string().min(1).max(120),
1517
+ street1: z.string().min(1).max(200),
1518
+ street2: z.string().max(200).optional(),
1519
+ city: z.string().min(1).max(120),
1520
+ state: z.string().min(1).max(80),
1521
+ postalCode: z.string().min(1).max(20),
1522
+ country: z.string().length(2),
1523
+ phone: z.string().max(40).optional(),
1524
+ email: z.string().email().optional(),
1525
+ });
1526
+ export const shippingRatesInputSchema = z.object({
1527
+ deliveryAddress: shippingDeliveryAddressSchema,
1528
+ });
1529
+ export const shippingOptionBreakdownSchema = z.object({
1530
+ originId: z.string().nullable(),
1531
+ shippoShipmentId: z.string(),
1532
+ shippoRateId: z.string(),
1533
+ amountCents: z.number().int().nonnegative(),
1534
+ parcelSummary: z.string(),
1535
+ });
1536
+ export const shippingOptionSchema = z.object({
1537
+ id: z.string(),
1538
+ displayName: z.string(),
1539
+ carrierName: z.string(),
1540
+ serviceToken: z.string(),
1541
+ totalCents: z.number().int().nonnegative(),
1542
+ currency: z.string().min(3),
1543
+ estimatedDays: z.number().int().nullable(),
1544
+ durationTerms: z.string().nullable(),
1545
+ breakdown: z.array(shippingOptionBreakdownSchema),
1546
+ });
1547
+ export const shippingRatesResponseSchema = z.object({
1548
+ options: z.array(shippingOptionSchema),
1549
+ requiresShipping: z.boolean(),
1550
+ });
1551
+ export const shippingTrackResponseSchema = z.object({
1552
+ shipmentId: z.string(),
1553
+ orderId: z.string(),
1554
+ carrier: z.string().nullable(),
1555
+ trackingNumber: z.string().nullable(),
1556
+ trackingUrl: z.string().nullable(),
1557
+ labelUrl: z.string().nullable(),
1558
+ /** Mirrors the `order_shipment_status` pg enum. */
1559
+ status: openEnum("pending", "label_created", "in_transit", "delivered", "returned"),
1560
+ estimatedDelivery: z.string().nullable(),
1561
+ shippedAt: z.string().nullable(),
1562
+ deliveredAt: z.string().nullable(),
1563
+ serviceName: z.string().nullable(),
1564
+ rateAmountCents: z.number().int().nullable(),
1565
+ currency: z.string().nullable(),
1566
+ statusHistory: z.unknown(),
1567
+ liveStatus: z
1568
+ .object({
1569
+ status: z.string(),
1570
+ status_details: z.string().optional(),
1571
+ status_date: z.string().optional(),
1572
+ location: z
1573
+ .object({
1574
+ city: z.string().optional(),
1575
+ state: z.string().optional(),
1576
+ zip: z.string().optional(),
1577
+ country: z.string().optional(),
1578
+ })
1579
+ .nullable()
1580
+ .optional(),
1581
+ })
1582
+ .nullable(),
1583
+ });
1584
+ /**
1585
+ * The org's storefront-facing shipping config. Sanitized: no API keys/secrets,
1586
+ * and only a coarse city/state/country "ships from" (never the precise origin
1587
+ * street).
1588
+ */
1589
+ export const shippingSettingsResponseSchema = z.object({
1590
+ enabled: z.boolean(),
1591
+ shipsFrom: z
1592
+ .object({
1593
+ city: z.string().nullable(),
1594
+ state: z.string().nullable(),
1595
+ country: z.string().nullable(),
1596
+ })
1597
+ .nullable(),
1598
+ /** ISO-3166-1 alpha-2 list. null = ships anywhere; [] = ships nowhere. */
1599
+ allowedDestinationCountries: z.array(z.string()).nullable(),
1600
+ /** DDU = customer pays duties, DDP = seller pays, FCA = free carrier. */
1601
+ defaultIncoterm: openEnum("DDU", "DDP", "FCA"),
1602
+ preferredCarriers: z.array(z.string()).nullable(),
1603
+ });
1604
+ // ── Customer notification preferences (Batch shipping, 0.9.0) ─────────
1605
+ // Distinct from the BIAB-user notification prefs in this file —
1606
+ // these are per-customer-per-org preferences exposed via the SDK so
1607
+ // the *consumer's* site can render a preferences UI for its signed-
1608
+ // in shoppers (in-app / email / SMS / outbound webhook).
1609
+ export const customerNotificationEventMapSchema = z.record(z.string(), z.boolean());
1610
+ export const customerNotificationPreferencesResponseSchema = z.object({
1611
+ exists: z.boolean(),
1612
+ inAppEvents: customerNotificationEventMapSchema,
1613
+ emailEvents: customerNotificationEventMapSchema,
1614
+ smsEvents: customerNotificationEventMapSchema,
1615
+ webhookEvents: customerNotificationEventMapSchema,
1616
+ emailAddress: z.string().nullable(),
1617
+ phoneNumber: z.string().nullable(),
1618
+ webhookUrl: z.string().nullable(),
1619
+ webhookHasSigningSecret: z.boolean(),
1620
+ emailVerified: z.boolean(),
1621
+ phoneVerified: z.boolean(),
1622
+ });
1623
+ export const customerNotificationPreferencesUpdateInputSchema = z.object({
1624
+ inAppEvents: customerNotificationEventMapSchema.optional(),
1625
+ emailEvents: customerNotificationEventMapSchema.optional(),
1626
+ smsEvents: customerNotificationEventMapSchema.optional(),
1627
+ webhookEvents: customerNotificationEventMapSchema.optional(),
1628
+ emailAddress: z.string().email().nullable().optional(),
1629
+ phoneNumber: z.string().min(7).max(40).nullable().optional(),
1630
+ webhookUrl: z.string().url().nullable().optional(),
1631
+ webhookSigningSecret: z.string().min(16).max(128).nullable().optional(),
1632
+ });
1633
+ export const customerNotificationPreferencesUpdateResponseSchema = z.object({
1634
+ ok: z.literal(true),
1635
+ });
1636
+ // ── Customer destination verification (Batch shipping, 0.9.0) ─────────
1637
+ export const startVerificationInputSchema = z.object({
1638
+ kind: z.enum(["email", "phone"]),
1639
+ destination: z.string().min(3).max(200),
1640
+ });
1641
+ export const startVerificationResponseSchema = z.object({
1642
+ ok: z.literal(true),
1643
+ message: z.string(),
1644
+ flow: z.enum(["link", "otp"]),
1645
+ });
1646
+ export const confirmVerificationInputSchema = z.object({
1647
+ token: z.string().min(4).max(128),
1648
+ });
1649
+ export const confirmVerificationResponseSchema = z.object({
1650
+ ok: z.literal(true),
1651
+ verifiedKind: z.enum(["email", "phone"]),
1652
+ destination: z.string(),
1653
+ });
1654
+ // ── Address search + verify (0.9.0) ───────────────────────────────────
1655
+ // Field shape mirrors Resources/addressParser/src/types/address.ts so
1656
+ // the working reference React components copy in cleanly.
1657
+ export const addressSearchInputSchema = z.object({
1658
+ query: z.string().min(1).max(200),
1659
+ });
1660
+ export const addressPlacePredictionSchema = z.object({
1661
+ place_id: z.string(),
1662
+ structured_formatting: z.object({
1663
+ main_text: z.string(),
1664
+ secondary_text: z.string(),
1665
+ }),
1666
+ });
1667
+ export const addressSearchResponseSchema = z.union([
1668
+ z.object({
1669
+ enabled: z.literal(true),
1670
+ predictions: z.array(addressPlacePredictionSchema),
1671
+ }),
1672
+ z.object({
1673
+ enabled: z.literal(false),
1674
+ reason: z.string(),
1675
+ predictions: z.array(addressPlacePredictionSchema),
1676
+ }),
1677
+ ]);
1678
+ /**
1679
+ * Structured address as Google returns it. Pipe through
1680
+ * `googleAddressToFormValues` (exported from the SDK helpers) to
1681
+ * fill a form whose fields match the reference `AddressFormValues`
1682
+ * shape.
1683
+ */
1684
+ export const googleAddressSchema = z.object({
1685
+ street_number: z.string(),
1686
+ route: z.string(),
1687
+ subpremise: z.string().optional(),
1688
+ locality: z.string(),
1689
+ administrative_area_level_1: z.string(),
1690
+ postal_code: z.string(),
1691
+ country: z.string(),
1692
+ });
1693
+ export const addressDetailsResponseSchema = z.union([
1694
+ z.object({ enabled: z.literal(true), address: googleAddressSchema }),
1695
+ z.object({
1696
+ enabled: z.literal(false),
1697
+ reason: z.string(),
1698
+ address: z.null(),
1699
+ }),
1700
+ ]);
1701
+ export const addressFormValuesSchema = z.object({
1702
+ address_ln1: z.string().min(1).max(200),
1703
+ address_ln2: z.string().max(200).optional(),
1704
+ city: z.string().min(1).max(120),
1705
+ state: z.string().min(1).max(80),
1706
+ zip: z.string().min(1).max(20),
1707
+ country: z.string().min(2).max(80),
1708
+ });
1709
+ export const addressVerifyResponseSchema = z.object({
1710
+ isVerified: z.boolean(),
1711
+ googleAddress: z.string().nullable(),
1712
+ });
1713
+ // ── Subscriptions (consumer-facing) ───────────────────────────────────
1714
+ export const subscriptionOfferingSchema = z.object({
1715
+ id: z.string().uuid(),
1716
+ name: z.string(),
1717
+ description: z.string().nullable(),
1718
+ imageUrl: z.string().nullable(),
1719
+ amountCents: z.number().int(),
1720
+ interval: z.enum(["day", "week", "month", "year"]),
1721
+ createdAt: z.string(),
1722
+ updatedAt: z.string(),
1723
+ });
1724
+ export const subscriptionOfferingsListResponseSchema = z.object({
1725
+ items: z.array(subscriptionOfferingSchema),
1726
+ });
1727
+ export const subscriptionCheckoutInputSchema = z
1728
+ .object({
1729
+ customerEmail: z.string().email().optional(),
1730
+ successUrl: z.string().url().optional(),
1731
+ cancelUrl: z.string().url().optional(),
1732
+ })
1733
+ .optional();
1734
+ export const subscriptionCheckoutResponseSchema = z.object({
1735
+ sessionId: z.string(),
1736
+ stripeUrl: z.string().url(),
1737
+ amountCents: z.number().int(),
1738
+ currency: z.string(),
1739
+ /** Mirrors the `subscription_interval` pg enum. */
1740
+ interval: openEnum("day", "week", "month", "year"),
1741
+ });
1742
+ // ── Coupons (Batch LL, 0.9.0) ─────────────────────────────────────────
1743
+ export const couponValidateInputSchema = z.object({
1744
+ code: z.string().min(1).max(80),
1745
+ });
1746
+ export const couponValidateValidSchema = z.object({
1747
+ valid: z.literal(true),
1748
+ code: z.string(),
1749
+ discountType: z.enum(["percent", "amount"]),
1750
+ percentOff: z.number().nullable(),
1751
+ amountOffCents: z.number().int().nullable(),
1752
+ currency: z.string().nullable(),
1753
+ duration: z.enum(["once", "forever", "repeating"]),
1754
+ durationInMonths: z.number().int().nullable(),
1755
+ expiresAt: z.string().nullable(),
1756
+ });
1757
+ export const couponValidateInvalidSchema = z.object({
1758
+ valid: z.literal(false),
1759
+ reason: z.enum([
1760
+ "unknown_code",
1761
+ "expired",
1762
+ "inactive",
1763
+ "no_stripe_account",
1764
+ "lookup_failed",
1765
+ ]),
1766
+ message: z.string(),
1767
+ });
1768
+ export const couponValidateResponseSchema = z.union([
1769
+ couponValidateValidSchema,
1770
+ couponValidateInvalidSchema,
1771
+ ]);
1772
+ // ── Tenant auth (per-org hosted sign-in / sign-up / sign-out) ─────────
1773
+ export const tenantAuthIntentSchema = z.enum(["sign-in", "sign-up"]);
1774
+ export const tenantAuthStartInputSchema = z.object({
1775
+ intent: tenantAuthIntentSchema,
1776
+ /** Where the tenant's site should send the user after auth completes. */
1777
+ returnTo: z.string().url().optional(),
1778
+ /** Pre-fills the email field on the platform-hosted page. */
1779
+ loginHint: z.string().email().optional(),
1780
+ /**
1781
+ * The tenant's installed callback URL — must match what was registered with
1782
+ * the platform. Required because the tenant lives on its own domain.
1783
+ */
1784
+ redirectUri: z.string().url(),
1785
+ });
1786
+ export const tenantAuthStartResponseSchema = z.object({
1787
+ url: z.string().url(),
1788
+ });
1789
+ export const tenantAuthExchangeInputSchema = z.object({
1790
+ code: z.string().min(1),
1791
+ /** Sealed OAuth `state` returned on the callback URL — contains the PKCE verifier for the hosted auth flow. */
1792
+ state: z.string().min(1),
1793
+ /** The same redirectUri used when starting; the platform validates this matches. */
1794
+ redirectUri: z.string().url(),
1795
+ });
1796
+ export const tenantAuthUserSchema = z.object({
1797
+ id: z.string(),
1798
+ email: z.string().email().nullable(),
1799
+ firstName: z.string().nullable(),
1800
+ lastName: z.string().nullable(),
1801
+ });
1802
+ export const tenantAuthSessionSchema = z.object({
1803
+ user: tenantAuthUserSchema,
1804
+ organizationId: z.string(),
1805
+ role: z.string().nullable(),
1806
+ /** Opaque token the tenant cookie should store; pass back as Bearer for `/me`. */
1807
+ sessionToken: z.string(),
1808
+ expiresAt: timestampSchema,
1809
+ });
1810
+ export const tenantAuthExchangeResponseSchema = tenantAuthSessionSchema;
1811
+ export const tenantAuthMeResponseSchema = z
1812
+ .object({
1813
+ user: tenantAuthUserSchema,
1814
+ organizationId: z.string(),
1815
+ role: z.string().nullable(),
1816
+ })
1817
+ .nullable();
1818
+ export const tenantAuthSignOutResponseSchema = z.object({
1819
+ ok: z.literal(true),
1820
+ });
1821
+ /**
1822
+ * Programmatic password-reset request. Always succeeds from the caller's
1823
+ * perspective — the server returns `ok: true` regardless of whether an
1824
+ * account matched, to avoid leaking which emails are registered. The
1825
+ * actual reset link is mailed out-of-band when a match exists.
1826
+ */
1827
+ export const tenantAuthPasswordResetInputSchema = z.object({
1828
+ email: z.string().email().max(320),
1829
+ /**
1830
+ * Optional override for the redirect after the user sets a new password.
1831
+ * Falls back to the org's default sign-in landing page server-side.
1832
+ */
1833
+ returnTo: z.string().url().max(2048).optional(),
1834
+ });
1835
+ export const tenantAuthPasswordResetResponseSchema = z.object({
1836
+ ok: z.literal(true),
1837
+ });
1838
+ // ── Schema-driven marketing flow (Phase 2 — V7 dashboard) ────────────────
1839
+ //
1840
+ // The bundle endpoint is the live-site read path. Each section is tagged
1841
+ // `{ ok: true, data, source } | { ok: false, error }` so one malformed
1842
+ // row degrades to empty in the consumer's renderer instead of crashing
1843
+ // the page. `source` records whether the value came from the requested
1844
+ // locale, fell back to EN, or was deep-merged from both.
1845
+ export const siteMarketingSectionEntrySchema = z.discriminatedUnion("ok", [
1846
+ z.object({
1847
+ ok: z.literal(true),
1848
+ sectionKey: z.string(),
1849
+ data: z.unknown(),
1850
+ source: z.enum(["locale", "en-fallback", "merged"]),
1851
+ updatedAt: timestampSchema,
1852
+ }),
1853
+ z.object({
1854
+ ok: z.literal(false),
1855
+ sectionKey: z.string(),
1856
+ error: z.string(),
1857
+ }),
1858
+ ]);
1859
+ export const siteMarketingSeoDataSchema = z.object({
1860
+ seoTitle: z.string().nullable(),
1861
+ seoDescription: z.string().nullable(),
1862
+ canonicalUrl: z.string().nullable(),
1863
+ noIndex: z.boolean(),
1864
+ ogTitle: z.string().nullable(),
1865
+ ogDescription: z.string().nullable(),
1866
+ ogImageUrl: z.string().nullable(),
1867
+ twitterCard: z.enum(["summary", "summary_large_image", "app", "player"]),
1868
+ keywords: z.array(z.string()),
1869
+ jsonldNodes: z.array(z.unknown()),
1870
+ hreflang: z.record(z.string()),
1871
+ });
1872
+ // ──────────────────────────────────────────────────────────────────────
1873
+ // Bundle: brand / company / gallery / reviews surfaces
1874
+ //
1875
+ // Added in SDK 0.8.4. Prior versions zod-stripped these keys at the
1876
+ // response boundary even though the BIAB server already shipped them
1877
+ // (the underlying `.passthrough()` in this file is on a chatbot
1878
+ // schema, NOT on the bundle response). Consumers that need the
1879
+ // stripped data on older SDK versions can raw-fetch
1880
+ // /sites/{id}/marketing/bundle directly — same auth, same caching —
1881
+ // until they bump.
1882
+ //
1883
+ // The new keys are all optional + null-able to match the
1884
+ // server's opt-in posture: company.profile is null when the org
1885
+ // hasn't filled out /settings/company-profile; gallery is null when
1886
+ // no public-flagged media exists; reviews is null when the org
1887
+ // hasn't set their Google Place ID. Consumers branch on null and
1888
+ // either render an empty section or hide it.
1889
+ const brandTokensSchema = z.object({
1890
+ logoUrl: z.string().nullable(),
1891
+ faviconUrl: z.string().nullable(),
1892
+ });
1893
+ export const companyAddressSchema = z
1894
+ .object({
1895
+ addressLine1: z.string().nullable().optional(),
1896
+ addressLine2: z.string().nullable().optional(),
1897
+ city: z.string().nullable().optional(),
1898
+ state: z.string().nullable().optional(),
1899
+ zip: z.string().nullable().optional(),
1900
+ country: z.string().nullable().optional(),
1901
+ })
1902
+ .nullable();
1903
+ export const companyLocationSchema = z.object({
1904
+ id: z.string(),
1905
+ name: z.string(),
1906
+ slug: z.string(),
1907
+ isPrimary: z.boolean(),
1908
+ isPublic: z.boolean(),
1909
+ type: z
1910
+ .enum([
1911
+ "office",
1912
+ "store",
1913
+ "showroom",
1914
+ "warehouse",
1915
+ "service_center",
1916
+ "other",
1917
+ ])
1918
+ .optional(),
1919
+ businessType: z.string().nullable().optional(),
1920
+ address: companyAddressSchema.unwrap(),
1921
+ phone: z.string().nullable().optional(),
1922
+ webPhone: z.string().nullable().optional(),
1923
+ supportEmail: z.string().nullable().optional(),
1924
+ website: z.string().nullable().optional(),
1925
+ googlePlaceId: z.string().nullable().optional(),
1926
+ latitude: z.number().nullable().optional(),
1927
+ longitude: z.number().nullable().optional(),
1928
+ timezone: z.string().nullable().optional(),
1929
+ operationHours: z.record(z.string(), z.unknown()).nullable().optional(),
1930
+ serviceAreas: z.record(z.string(), z.unknown()).nullable().optional(),
1931
+ });
1932
+ /**
1933
+ * Loose-typed CompanyProfileRow. The server-side shape is fully
1934
+ * typed; we keep this looser at the SDK boundary so adding a column
1935
+ * on the BIAB side doesn't break older SDK consumers.
1936
+ */
1937
+ const companyProfileRowSchema = z
1938
+ .object({
1939
+ id: z.string(),
1940
+ orgId: z.string(),
1941
+ website: z.string().nullable(),
1942
+ phone: z.string().nullable(),
1943
+ webPhone: z.string().nullable(),
1944
+ supportEmail: z.string().nullable(),
1945
+ aboutShort: z.string().nullable(),
1946
+ aboutLongHtml: z.string().nullable(),
1947
+ shareImageUrl: z.string().nullable(),
1948
+ googlePlaceId: z.string().nullable().optional(),
1949
+ address: companyAddressSchema,
1950
+ locations: z.array(companyLocationSchema).nullable().optional(),
1951
+ operationHours: z.record(z.string(), z.unknown()).nullable(),
1952
+ serviceAreas: z.record(z.string(), z.unknown()).nullable(),
1953
+ paymentOptions: z.record(z.string(), z.unknown()).nullable(),
1954
+ warranties: z.record(z.string(), z.unknown()).nullable(),
1955
+ socials: z.record(z.string(), z.unknown()).nullable(),
1956
+ })
1957
+ .passthrough()
1958
+ .nullable();
1959
+ const bundleCompanySchema = z.object({
1960
+ profile: companyProfileRowSchema,
1961
+ jsonLd: z.record(z.string(), z.unknown()),
1962
+ locationJsonLd: z.array(z.record(z.string(), z.unknown())).optional(),
1963
+ shareMetadata: z.object({
1964
+ title: z.string(),
1965
+ description: z.string().nullable(),
1966
+ imageUrl: z.string().nullable(),
1967
+ siteName: z.string(),
1968
+ }),
1969
+ });
1970
+ const bundleGalleryItemSchema = z.object({
1971
+ id: z.string(),
1972
+ type: z.enum(["image", "video"]),
1973
+ src: z.string(),
1974
+ width: z.number().nullable(),
1975
+ height: z.number().nullable(),
1976
+ blurDataURL: z.string().nullable(),
1977
+ alt: z.string().nullable(),
1978
+ title: z.string().nullable(),
1979
+ category: z.string().nullable(),
1980
+ jobId: z.string().nullable(),
1981
+ jobName: z.string().nullable(),
1982
+ takenAt: z.string().nullable(),
1983
+ location: z.string().nullable(),
1984
+ system: z.string().nullable(),
1985
+ year: z.number().nullable(),
1986
+ aboutHtml: z.string().nullable(),
1987
+ });
1988
+ const bundleGallerySchema = z
1989
+ .object({
1990
+ items: z.array(bundleGalleryItemSchema),
1991
+ totalCount: z.number(),
1992
+ })
1993
+ .nullable();
1994
+ const bundleReviewSchema = z.object({
1995
+ reviewee: z.string(),
1996
+ rating: z.number(),
1997
+ image: z.string().nullable(),
1998
+ description: z.string(),
1999
+ date: z.string(),
2000
+ // Reviewer-attached gallery photos (URLs). Optional for back-compat with
2001
+ // bundles served by pre-photo servers; empty/absent when the review has none.
2002
+ photos: z.array(z.string()).optional(),
2003
+ // Widened from the 0.9.x `"Google"` literal: reviews now come from the
2004
+ // org's unified review store (Google + Yelp + manual + CSV), so the
2005
+ // platform can be any of those. Consumers should render this as a label.
2006
+ platform: z.string(),
2007
+ });
2008
+ /** Per-rating histogram bucket (e.g. `{ rating: 5, count: 182 }`). */
2009
+ const bundleReviewCountSchema = z.object({
2010
+ rating: z.number(),
2011
+ count: z.number(),
2012
+ });
2013
+ const bundleReviewsSchema = z
2014
+ .object({
2015
+ enabled: z.literal(true),
2016
+ // `org_external_reviews` is the canonical store; `google_places` is the
2017
+ // legacy Place-ID cache, kept as a fallback. Widened from the 0.9.x
2018
+ // `"google_places"` literal.
2019
+ source: z.enum(["org_external_reviews", "google_places"]),
2020
+ // Null when the payload didn't originate from a Place-ID source.
2021
+ placeId: z.string().nullable(),
2022
+ // Average + count are computed over ALL of the org's reviews, not just
2023
+ // the `items` page below.
2024
+ rating: z.number().nullable(),
2025
+ totalCount: z.number().nullable(),
2026
+ // Per-rating distribution over ALL reviews (newest 5→1), so the consumer
2027
+ // renders the histogram without holding every review client-side.
2028
+ // Optional for back-compat with bundles served by pre-0.9.4 servers.
2029
+ counts: z.array(bundleReviewCountSchema).optional(),
2030
+ // Only the first `reviewsLimit` reviews (newest-first). The rest are
2031
+ // fetched lazily via `client.reviews.list({ offset })`.
2032
+ items: z.array(bundleReviewSchema),
2033
+ // items.length — paired with `totalCount` so the consumer knows whether
2034
+ // more pages exist. Optional for back-compat.
2035
+ returnedCount: z.number().optional(),
2036
+ cachedAt: z.string(),
2037
+ })
2038
+ .nullable();
2039
+ // ── Reviews wall: paginated public reviews ────────────────────────────
2040
+ // Backs `client.reviews.list({ limit, offset, source })` → the key-scoped
2041
+ // GET /reviews route. The marketing bundle ships the first page + the
2042
+ // aggregate; this endpoint fetches deeper pages on demand (View All / scroll).
2043
+ export const reviewWallItemSchema = z.object({
2044
+ id: z.string(),
2045
+ source: z.enum(["google", "yelp", "housecall_pro", "other"]),
2046
+ rating: z.number(),
2047
+ text: z.string(),
2048
+ reviewUrl: z.string().nullable(),
2049
+ reviewerName: z.string(),
2050
+ reviewerImageUrl: z.string().nullable(),
2051
+ /** Reviewer-attached gallery photos (URLs); optional for back-compat,
2052
+ * empty/absent when the review has no media. */
2053
+ photos: z.array(z.string()).optional(),
2054
+ /** ISO 8601 timestamp of the original review. */
2055
+ timeCreated: z.string(),
2056
+ verified: z.boolean(),
2057
+ });
2058
+ export const reviewWallListResponseSchema = z.object({
2059
+ items: z.array(reviewWallItemSchema),
2060
+ /** Total reviews matching the (optional) source filter — for "N reviews". */
2061
+ totalCount: z.number(),
2062
+ /** Offset to pass back for the next page, or null when the list is exhausted. */
2063
+ nextOffset: z.number().nullable(),
2064
+ });
2065
+ /**
2066
+ * Org-owned marketing-page banner (announcement strip / marquee).
2067
+ * Mirrors the platform-banner shape so consumer storefronts can
2068
+ * render a near-identical UI to BIAB's own marketing banner.
2069
+ * `null` when the org hasn't enabled one or has no visible messages.
2070
+ */
2071
+ const bundleBannerMessageSchema = z
2072
+ .object({
2073
+ id: z.string(),
2074
+ enabled: z.boolean(),
2075
+ text: z.string(),
2076
+ marqueeLines: z.array(z.string()).optional(),
2077
+ linkUrl: z.string(),
2078
+ buttonText: z.string(),
2079
+ openInNewTab: z.boolean(),
2080
+ linkStyle: z.enum(["link", "button"]).optional(),
2081
+ urgent: z.boolean().optional(),
2082
+ appearance: z.enum(["always", "once", "until_dismiss", "every"]),
2083
+ everyIntervalHours: z.number().optional(),
2084
+ scheduleActive: z.boolean().optional(),
2085
+ displayFromUtc: z.string().nullable().optional(),
2086
+ displayUntilUtc: z.string().nullable().optional(),
2087
+ })
2088
+ .passthrough();
2089
+ const bundleBannerSchema = z
2090
+ .object({
2091
+ enabled: z.boolean(),
2092
+ hiddenOnHome: z.boolean(),
2093
+ messages: z.array(bundleBannerMessageSchema),
2094
+ })
2095
+ .passthrough()
2096
+ .nullable();
2097
+ /**
2098
+ * Org Updates feed — cached Google Business Profile "Posts" (the Updates tab):
2099
+ * a blurb + image(s) + deep link, optionally with an offer/event window. Lets a
2100
+ * consumer storefront render a "News"/"Updates" feed from the bundle without a
2101
+ * second API call. `null` when the org has none.
2102
+ */
2103
+ const bundleUpdateItemSchema = z
2104
+ .object({
2105
+ id: z.string(),
2106
+ kind: z.string().nullable(),
2107
+ title: z.string().nullable(),
2108
+ body: z.string(),
2109
+ link: z.string().nullable(),
2110
+ imageUrl: z.string().nullable(),
2111
+ images: z.array(z.string()),
2112
+ postedAt: z.string().nullable(),
2113
+ startsAt: z.string().nullable(),
2114
+ endsAt: z.string().nullable(),
2115
+ })
2116
+ .passthrough();
2117
+ const bundleUpdatesSchema = z
2118
+ .object({
2119
+ items: z.array(bundleUpdateItemSchema),
2120
+ })
2121
+ .passthrough()
2122
+ .nullable();
2123
+ export const getMarketingPageBundleResponseSchema = z
2124
+ .object({
2125
+ pageKey: z.string(),
2126
+ locale: z.string(),
2127
+ sections: z.record(z.string(), siteMarketingSectionEntrySchema),
2128
+ availableLocales: z.array(z.string()),
2129
+ seo: siteMarketingSeoDataSchema,
2130
+ // New in 0.8.4:
2131
+ brand: brandTokensSchema.optional(),
2132
+ company: bundleCompanySchema.optional(),
2133
+ gallery: bundleGallerySchema.optional(),
2134
+ reviews: bundleReviewsSchema.optional(),
2135
+ // New in 0.8.9:
2136
+ banner: bundleBannerSchema.optional(),
2137
+ // New in 0.9.6:
2138
+ updates: bundleUpdatesSchema.optional(),
2139
+ })
2140
+ // `.passthrough()` at the top level so any future BIAB-side
2141
+ // addition (e.g. bundle.events, bundle.faq, etc.) flows through
2142
+ // to consumers without an SDK release — the lesson from the
2143
+ // brand/company/gallery/reviews oversight on this version.
2144
+ .passthrough();
2145
+ export const marketingSchemaSlotRowSchema = z.object({
2146
+ slot: z.enum(["draft", "published"]),
2147
+ schema: z.record(z.string(), z.unknown()),
2148
+ checksum: z.string().length(64),
2149
+ version: z.number().int().positive(),
2150
+ updatedAt: timestampSchema,
2151
+ });
2152
+ export const getPublishedMarketingSchemaResponseSchema = z.object({
2153
+ schema: marketingSchemaSlotRowSchema.nullable(),
2154
+ });
2155
+ export const upsertDraftMarketingSchemaInputSchema = z.object({
2156
+ schema: z.record(z.string(), z.unknown()),
2157
+ checksum: z.string().length(64),
2158
+ note: z.string().max(280).nullable().optional(),
2159
+ });
2160
+ export const upsertDraftMarketingSchemaResponseSchema = z.object({
2161
+ ok: z.literal(true),
2162
+ version: z.number().int().positive(),
2163
+ checksum: z.string().length(64),
2164
+ noop: z.boolean(),
2165
+ });
2166
+ export const getMarketingLocalesResponseSchema = z.object({
2167
+ locales: z.array(z.string()),
2168
+ brandTokens: z.record(z.string()),
2169
+ });
2170
+ /**
2171
+ * Bulk section upsert — sent by `biab-dev sync-content`.
2172
+ *
2173
+ * One request carries up to 256 items (server-enforced). The CLI batches
2174
+ * larger imports automatically. Each item is a single
2175
+ * `(pageKey, locale, sectionKey)` value and goes through the same
2176
+ * validation pipeline as a dashboard save, so a partial failure on one
2177
+ * row reports `ok: false, error: "..."` without rolling back the rest.
2178
+ */
2179
+ export const publishMarketingSectionsItemSchema = z.object({
2180
+ pageKey: z.string().min(1).max(120),
2181
+ locale: z.string().min(1).max(20),
2182
+ sectionKey: z.string().min(1).max(120),
2183
+ payload: z.unknown(),
2184
+ });
2185
+ export const publishMarketingSectionsInputSchema = z.object({
2186
+ items: z.array(publishMarketingSectionsItemSchema).min(1).max(256),
2187
+ validate: z.boolean().default(true).optional(),
2188
+ note: z.string().max(280).nullable().optional(),
2189
+ });
2190
+ export const publishMarketingSectionsResultItemSchema = z.discriminatedUnion("ok", [
2191
+ z.object({
2192
+ ok: z.literal(true),
2193
+ pageKey: z.string(),
2194
+ locale: z.string(),
2195
+ sectionKey: z.string(),
2196
+ version: z.number().int().positive(),
2197
+ }),
2198
+ z.object({
2199
+ ok: z.literal(false),
2200
+ pageKey: z.string(),
2201
+ locale: z.string(),
2202
+ sectionKey: z.string(),
2203
+ error: z.string(),
2204
+ }),
2205
+ ]);
2206
+ export const publishMarketingSectionsResponseSchema = z.object({
2207
+ results: z.array(publishMarketingSectionsResultItemSchema),
2208
+ });
2209
+ // ── Data model: reading records ──────────────────────────────────────
2210
+ //
2211
+ // The read half of the custom data model. Until these existed the model was
2212
+ // write-only from outside: `sync-data-model` pushed a schema and `sync-records`
2213
+ // seeded rows, and nothing could read either back.
2214
+ //
2215
+ // Note this is NOT the `collections:*` family above. Those read `site_data_rows`
2216
+ // (Site Data / the site builder's tables). Custom-object rows live in
2217
+ // `org_object_records` and are reached only through here.
2218
+ /** One edge out of a record. Relations are links, not embedded rows. */
2219
+ export const recordRelationLinkSchema = z.object({
2220
+ /** The linked row's `id`. Read it with a follow-up `listRecords` call. */
2221
+ recordId: z.string().uuid(),
2222
+ /** `universalIdentifier` of the object the linked row belongs to. */
2223
+ object: z.string(),
2224
+ });
2225
+ export const dataModelRecordSchema = z.object({
2226
+ id: z.string().uuid(),
2227
+ /**
2228
+ * The identity a seed file gave this row, or null when a human created it
2229
+ * in the dashboard. Null means "not reproducible from your config".
2230
+ */
2231
+ universalIdentifier: z.string().uuid().nullable(),
2232
+ /** Scalar values, keyed by field name. */
2233
+ fields: z.record(z.string(), z.unknown()),
2234
+ /**
2235
+ * Keyed by field name. EVERY declared relation field is present — `[]` when
2236
+ * it has no links — so an empty relation is never mistaken for a missing one.
2237
+ */
2238
+ relations: z.record(z.string(), z.array(recordRelationLinkSchema)),
2239
+ createdAt: timestampSchema,
2240
+ updatedAt: timestampSchema,
2241
+ });
2242
+ export const listDataModelRecordsInputSchema = z.object({
2243
+ /**
2244
+ * The object's `universalIdentifier` — the same value your config declares,
2245
+ * and the same one `sync-records` writes against. Not its name, which can
2246
+ * change without the identity changing.
2247
+ */
2248
+ object: z.string().min(1),
2249
+ limit: z.number().int().min(1).max(200).optional(),
2250
+ /** Pass back the previous page's `nextCursor`. Opaque; don't construct one. */
2251
+ cursor: z.string().optional(),
2252
+ });
2253
+ export const listDataModelRecordsAvailableSchema = z.object({
2254
+ available: z.literal(true),
2255
+ records: z.array(dataModelRecordSchema),
2256
+ /** Null on the last page. */
2257
+ nextCursor: z.string().nullable(),
2258
+ });
2259
+ /**
2260
+ * Discriminated union — branch on `.available`. Custom objects are an à-la-carte
2261
+ * surface (`crm.custom_objects`), so an org without it gets the upgrade shape
2262
+ * rather than an exception.
2263
+ */
2264
+ export const listDataModelRecordsResponseSchema = z.union([
2265
+ listDataModelRecordsAvailableSchema,
2266
+ planGatedUnavailableSchema,
2267
+ ]);
2268
+ // ── Analytics: reading visitor counts ────────────────────────────────
2269
+ //
2270
+ // The read half of site analytics. The `<BIABAnalytics/>` tracker records
2271
+ // pageviews (write-only); these read the totals back so a site can show them.
2272
+ export const viewCountSchema = z.object({
2273
+ /** Pageviews (`kind = 'pageview'` events). */
2274
+ views: z.number().int().nonnegative(),
2275
+ /**
2276
+ * Distinct anonId — an approximation of unique visitors. anonId rotates every
2277
+ * UTC day, so this is exact within a day and, over a longer window, counts a
2278
+ * returning visitor once per day they came (visitor-days). Never identifies
2279
+ * anyone; never undercounts.
2280
+ */
2281
+ visitors: z.number().int().nonnegative(),
2282
+ });
2283
+ export const pageViewsInputSchema = z.object({
2284
+ /**
2285
+ * Exact paths to count. Omit (or pass `[]`) for a SITE-WIDE total. Each path
2286
+ * comes back in `paths`, zero-filled if it has no views yet.
2287
+ */
2288
+ paths: z.array(z.string().min(1)).max(50).optional(),
2289
+ /** Window in days. Omit for all-time. */
2290
+ days: z.number().int().min(1).optional(),
2291
+ });
2292
+ export const pageViewsAvailableSchema = z.object({
2293
+ available: z.literal(true),
2294
+ /** ISO start of the window, or null for all-time. */
2295
+ since: z.string().nullable(),
2296
+ /** Over the whole scope: site-wide when no paths, else summed over them. */
2297
+ total: viewCountSchema,
2298
+ /** One entry per requested path (empty for a site-wide query). */
2299
+ paths: z.array(viewCountSchema.extend({ path: z.string() })),
2300
+ });
2301
+ /**
2302
+ * Discriminated union — branch on `.available`. Visitor analytics is an
2303
+ * entitlement (`analytics.web`), so an org without it gets the upgrade shape
2304
+ * rather than an exception.
2305
+ */
2306
+ export const pageViewsResponseSchema = z.union([
2307
+ pageViewsAvailableSchema,
2308
+ planGatedUnavailableSchema,
2309
+ ]);
2310
+ // ── AI-crawler visibility (AEO) ──────────────────────────────────────
2311
+ export const aiVisibilityInputSchema = z.object({
2312
+ /** Window in days (default 30 server-side). */
2313
+ days: z.number().int().min(1).optional(),
2314
+ });
2315
+ export const aiCrawlerVisibilitySchema = z.object({
2316
+ /** Stable crawler name, e.g. "GPTBot". */
2317
+ name: z.string(),
2318
+ /** Operator, e.g. "OpenAI". Null when the platform can't say. */
2319
+ vendor: z.string().nullable(),
2320
+ /**
2321
+ * What the fetch means: "training" (corpus building), "search" (answer-
2322
+ * engine indexing — citation supply), or "user" (a live assistant reading
2323
+ * on a person's behalf). Null when unknown.
2324
+ */
2325
+ intent: z.enum(["training", "search", "user"]).nullable(),
2326
+ /** Fetches in the window. */
2327
+ fetches: z.number().int().nonnegative(),
2328
+ /** Distinct paths this crawler read. */
2329
+ distinctPages: z.number().int().nonnegative(),
2330
+ /** ISO timestamp of the crawler's most recent fetch. */
2331
+ lastSeenAt: z.string(),
2332
+ });
2333
+ export const aiVisibilityAvailableSchema = z.object({
2334
+ available: z.literal(true),
2335
+ /** ISO start of the window. */
2336
+ since: z.string(),
2337
+ /** Ordered most-fetches-first. Empty = no AI crawler came. */
2338
+ crawlers: z.array(aiCrawlerVisibilitySchema),
2339
+ });
2340
+ /** Branch on `.available` — same entitlement gating as pageViews. */
2341
+ export const aiVisibilityResponseSchema = z.union([
2342
+ aiVisibilityAvailableSchema,
2343
+ planGatedUnavailableSchema,
2344
+ ]);
2345
+ // ── Attribution footer ───────────────────────────────────────────────
2346
+ //
2347
+ // The "Powered by BusinessDash" badge's one narrow function. READ its policy;
2348
+ // WRITE an (untrusted) heartbeat. See the plan in the app repo.
2349
+ export const attributionPolicySchema = z.object({
2350
+ /** The site the policy is for — build the badge's `?ref=<siteId>` from this. */
2351
+ siteId: z.string().uuid(),
2352
+ /**
2353
+ * Whether the badge must stay. `true` on launch plans (which can't remove
2354
+ * it); `false` on growth/scale. Advisory — the real check is server-side, but
2355
+ * an honest site uses this to decide whether it may hide the badge.
2356
+ */
2357
+ required: z.boolean(),
2358
+ });
2359
+ export const attributionHeartbeatInputSchema = z.object({
2360
+ /** Absolute URL the badge rendered on. Normalized + de-duped server-side. */
2361
+ url: z.string().min(1).max(2048),
2362
+ });
2363
+ export const attributionHeartbeatResponseSchema = z.object({
2364
+ /** `false` when the URL was unparseable — a heartbeat never errors. */
2365
+ recorded: z.boolean(),
2366
+ });
2367
+ /* ---------------------------------------------------------------------------
2368
+ Hiring — public job postings for a consumer-built careers page.
2369
+
2370
+ Read-only and deliberately narrower than the dashboard's job row: internal
2371
+ hiring state (openings filled, application caps, default permissions/roles,
2372
+ the role-ladder pointer, interview questions) never crosses this boundary.
2373
+ Applications ride the existing forms surface via `applicationFormId`.
2374
+ --------------------------------------------------------------------------- */
2375
+ export const jobCategorySchema = z.object({
2376
+ id: z.string(),
2377
+ name: z.string(),
2378
+ slug: z.string(),
2379
+ description: z.string().nullable(),
2380
+ /** Free-form chip tint set in the dashboard (hex or CSS colour). */
2381
+ color: z.string().nullable(),
2382
+ });
2383
+ export const jobCategoryWithCountSchema = jobCategorySchema.extend({
2384
+ /** Open postings currently in this category. */
2385
+ openCount: z.number(),
2386
+ });
2387
+ export const jobPostingSchema = z.object({
2388
+ id: z.string(),
2389
+ slug: z.string(),
2390
+ title: z.string(),
2391
+ /** Rich-text HTML, sanitized server-side. Safe to render. */
2392
+ description: z.string(),
2393
+ department: z.string().nullable(),
2394
+ level: z.string().nullable(),
2395
+ employmentType: z.string(),
2396
+ workMode: z.string(),
2397
+ location: z.string().nullable(),
2398
+ region: z.object({ id: z.string(), name: z.string() }).nullable(),
2399
+ /** The posting's single group. `null` when uncategorized. */
2400
+ category: jobCategorySchema.nullable(),
2401
+ /** Free-form labels for filtering/search; never a grouping. */
2402
+ tags: z.array(z.string()),
2403
+ responsibilities: z.array(z.string()).nullable(),
2404
+ requirements: z.string().nullable(),
2405
+ languages: z.array(z.string()).nullable(),
2406
+ payRangeText: z.string().nullable(),
2407
+ salaryMin: z.number().nullable(),
2408
+ salaryMax: z.number().nullable(),
2409
+ currency: z.string().nullable(),
2410
+ /**
2411
+ * Pass to `client.forms.schema()` / `<BiabForm/>` to render the application
2412
+ * form. `null` means the posting is listed but not yet accepting applications.
2413
+ */
2414
+ applicationFormId: z.string().nullable(),
2415
+ publishedAt: z.string().nullable(),
2416
+ updatedAt: z.string(),
2417
+ });
2418
+ export const jobPostingListResponseSchema = z.object({
2419
+ items: z.array(jobPostingSchema),
2420
+ /** Total open postings matching the (optional) category filter. */
2421
+ totalCount: z.number(),
2422
+ /** Offset to pass back for the next page, or null when exhausted. */
2423
+ nextOffset: z.number().nullable(),
2424
+ });
2425
+ export const jobCategoryListResponseSchema = z.object({
2426
+ items: z.array(jobCategoryWithCountSchema),
2427
+ });
2428
+ //# sourceMappingURL=contracts.js.map