@businessdash/sdk 0.9.70 → 0.9.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.json CHANGED
@@ -4785,6 +4785,113 @@
4785
4785
  "additionalProperties": false,
4786
4786
  "nullable": true
4787
4787
  },
4788
+ "customerPortalQuizResultsResponseSchema": {
4789
+ "type": "object",
4790
+ "properties": {
4791
+ "items": {
4792
+ "type": "array",
4793
+ "items": {
4794
+ "type": "object",
4795
+ "properties": {
4796
+ "invitationId": {
4797
+ "type": "string"
4798
+ },
4799
+ "quizName": {
4800
+ "type": "string"
4801
+ },
4802
+ "competency": {
4803
+ "type": "string",
4804
+ "nullable": true
4805
+ },
4806
+ "status": {
4807
+ "type": "string"
4808
+ },
4809
+ "completedAt": {
4810
+ "type": "string",
4811
+ "nullable": true
4812
+ },
4813
+ "scoreWithheld": {
4814
+ "type": "boolean"
4815
+ },
4816
+ "result": {
4817
+ "type": "object",
4818
+ "properties": {
4819
+ "status": {
4820
+ "type": "string"
4821
+ },
4822
+ "scorePercent": {
4823
+ "type": "number",
4824
+ "nullable": true
4825
+ },
4826
+ "provisionalScorePercent": {
4827
+ "type": "number",
4828
+ "nullable": true
4829
+ },
4830
+ "passed": {
4831
+ "type": "boolean",
4832
+ "nullable": true
4833
+ },
4834
+ "pointsEarned": {
4835
+ "type": "number",
4836
+ "nullable": true
4837
+ },
4838
+ "pointsPossible": {
4839
+ "type": "number",
4840
+ "nullable": true
4841
+ },
4842
+ "passingScore": {
4843
+ "type": "number"
4844
+ },
4845
+ "results": {
4846
+ "type": "array",
4847
+ "items": {
4848
+ "type": "object",
4849
+ "additionalProperties": {}
4850
+ }
4851
+ },
4852
+ "submittedAt": {
4853
+ "type": "string",
4854
+ "nullable": true
4855
+ },
4856
+ "durationSeconds": {
4857
+ "type": "number",
4858
+ "nullable": true
4859
+ }
4860
+ },
4861
+ "required": [
4862
+ "status",
4863
+ "scorePercent",
4864
+ "provisionalScorePercent",
4865
+ "passed",
4866
+ "pointsEarned",
4867
+ "pointsPossible",
4868
+ "passingScore",
4869
+ "results",
4870
+ "submittedAt",
4871
+ "durationSeconds"
4872
+ ],
4873
+ "additionalProperties": false,
4874
+ "nullable": true
4875
+ }
4876
+ },
4877
+ "required": [
4878
+ "invitationId",
4879
+ "quizName",
4880
+ "competency",
4881
+ "status",
4882
+ "completedAt",
4883
+ "scoreWithheld",
4884
+ "result"
4885
+ ],
4886
+ "additionalProperties": false
4887
+ }
4888
+ }
4889
+ },
4890
+ "required": [
4891
+ "items"
4892
+ ],
4893
+ "additionalProperties": false
4894
+ },
4788
4895
  "customerPortalUpdateProfileInputSchema": {
4789
4896
  "type": "object",
4790
4897
  "properties": {
@@ -12506,6 +12613,66 @@
12506
12613
  ]
12507
12614
  }
12508
12615
  },
12616
+ "/consent": {
12617
+ "get": {
12618
+ "operationId": "get_consent",
12619
+ "parameters": [],
12620
+ "responses": {
12621
+ "200": {
12622
+ "description": "OK",
12623
+ "content": {
12624
+ "application/json": {
12625
+ "schema": {
12626
+ "type": "object"
12627
+ }
12628
+ }
12629
+ }
12630
+ },
12631
+ "402": {
12632
+ "description": "Access gate (plan/payment/suspension)"
12633
+ }
12634
+ },
12635
+ "security": [
12636
+ {
12637
+ "bearerAuth": []
12638
+ }
12639
+ ]
12640
+ },
12641
+ "post": {
12642
+ "operationId": "post_consent",
12643
+ "parameters": [],
12644
+ "responses": {
12645
+ "200": {
12646
+ "description": "OK",
12647
+ "content": {
12648
+ "application/json": {
12649
+ "schema": {
12650
+ "type": "object"
12651
+ }
12652
+ }
12653
+ }
12654
+ },
12655
+ "402": {
12656
+ "description": "Access gate (plan/payment/suspension)"
12657
+ }
12658
+ },
12659
+ "security": [
12660
+ {
12661
+ "bearerAuth": []
12662
+ }
12663
+ ],
12664
+ "requestBody": {
12665
+ "required": true,
12666
+ "content": {
12667
+ "application/json": {
12668
+ "schema": {
12669
+ "$ref": "#/components/schemas/blogCommentPostInputSchema"
12670
+ }
12671
+ }
12672
+ }
12673
+ }
12674
+ }
12675
+ },
12509
12676
  "/coupons": {
12510
12677
  "post": {
12511
12678
  "operationId": "post_coupons",
@@ -13824,6 +13991,41 @@
13824
13991
  }
13825
13992
  }
13826
13993
  },
13994
+ "/customer-portal/quiz-results": {
13995
+ "get": {
13996
+ "operationId": "get_customer_portal_quiz_results",
13997
+ "parameters": [
13998
+ {
13999
+ "name": "token",
14000
+ "in": "query",
14001
+ "required": false,
14002
+ "schema": {
14003
+ "type": "string"
14004
+ }
14005
+ }
14006
+ ],
14007
+ "responses": {
14008
+ "200": {
14009
+ "description": "OK",
14010
+ "content": {
14011
+ "application/json": {
14012
+ "schema": {
14013
+ "$ref": "#/components/schemas/customerPortalQuizResultsResponseSchema"
14014
+ }
14015
+ }
14016
+ }
14017
+ },
14018
+ "402": {
14019
+ "description": "Access gate (plan/payment/suspension)"
14020
+ }
14021
+ },
14022
+ "security": [
14023
+ {
14024
+ "bearerAuth": []
14025
+ }
14026
+ ]
14027
+ }
14028
+ },
13827
14029
  "/customer-portal/quotes": {
13828
14030
  "get": {
13829
14031
  "operationId": "get_customer_portal_quotes",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@businessdash/sdk",
3
- "version": "0.9.70",
3
+ "version": "0.9.80",
4
4
  "description": "BusinessDash developer SDK \u2014 the data layer that lets you build customer-facing surfaces on your own domain while BusinessDash handles the operations stack behind the scenes. 0.9.x ships a comprehensive programmatic surface: storefront (products, categories, cart, checkout via Stripe Connect with Link payment), customer portal (work feed, per-item detail, quote accept/reject, job activity, comment threads, reviews, notification preferences), blog (posts, categories, threaded comments, reactions, paywalls, Mux video), Front Desk chat (sessions, AI bot, live human handoff, queue + place-in-line), coupons (validation against synced Stripe Promotion Codes \u2014 BOGO/BXGY/free-shipping kinds), shipping (multi-carrier rates + tracking via Shippo), address validation (Google Places, platform-proxied), customer notification preferences across in-app/email/SMS/webhook, subscriptions admin + Stripe sync, and followers/subscribers. Also: tenant sign-in/up/out via the auth handler, schema-driven marketing pages with restore semantics, typed JSON-LD SEO builders (LocalBusiness/Service/FAQ/Article/Breadcrumb/Review), and a CLI (`biab-dev sync-schema`) for the schema flow. The consumer renders the UI in their own design system on their own domain; BusinessDash supplies the data, the business logic, and the Stripe/Mux/Google/Shippo plumbing.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -14,8 +14,8 @@
14
14
  "module": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
16
16
  "bin": {
17
- "biab-dev": "./dist/cli.js",
18
- "businessdash": "./dist/cli.js"
17
+ "biab-dev": "dist/cli.js",
18
+ "businessdash": "dist/cli.js"
19
19
  },
20
20
  "exports": {
21
21
  ".": {
@@ -221,6 +221,11 @@
221
221
  "import": "./dist/model3d-core/index.js",
222
222
  "require": "./dist/model3d-core/index.cjs"
223
223
  },
224
+ "./consent": {
225
+ "types": "./dist/consent-core/index.d.ts",
226
+ "import": "./dist/consent-core/index.js",
227
+ "require": "./dist/consent-core/index.cjs"
228
+ },
224
229
  "./biab-sw.js": "./assets/biab-sw.js"
225
230
  },
226
231
  "scripts": {
package/src/client.ts CHANGED
@@ -111,6 +111,7 @@ import {
111
111
  type CustomerPortalMessagesMarkReadResponse,
112
112
  type CustomerPortalOtherOrgsResponse,
113
113
  type CustomerPortalProfile,
114
+ type CustomerPortalQuizResultsResponse,
114
115
  type CustomerPortalUpdateProfileInput,
115
116
  type CustomerPortalUpdateProfileResponse,
116
117
  type CustomerProductReviewCreated,
@@ -204,6 +205,7 @@ import {
204
205
  customerPortalMessagesMarkReadResponseSchema,
205
206
  customerPortalOtherOrgsResponseSchema,
206
207
  customerPortalProfileSchema,
208
+ customerPortalQuizResultsResponseSchema,
207
209
  customerPortalUpdateProfileInputSchema,
208
210
  customerPortalUpdateProfileResponseSchema,
209
211
  customerProductReviewCreatedSchema,
@@ -593,6 +595,16 @@ export class BiabDevClient {
593
595
  return new BiabDevReviewsClient(this);
594
596
  }
595
597
 
598
+ /**
599
+ * Tracking consent for this org's site.
600
+ *
601
+ * Pair with `@businessdash/sdk/consent`, which handles the visitor cookie
602
+ * and gives you a gate to check before starting analytics.
603
+ */
604
+ get consent() {
605
+ return new BiabDevConsentClient(this);
606
+ }
607
+
596
608
  /** Public job postings — build a careers page on your own domain. */
597
609
  get hiring() {
598
610
  return new BiabDevHiringClient(this);
@@ -2827,6 +2839,73 @@ export class BiabDevLegalClient {
2827
2839
  }
2828
2840
  }
2829
2841
 
2842
+ const consentStateSchema = z.object({
2843
+ /** Categories this org uses at all — one banner row each. */
2844
+ categories: z.array(z.string()),
2845
+ noticeText: z.string().nullable(),
2846
+ noticeVersion: z.number(),
2847
+ /** What may be used right now. */
2848
+ allowed: z.array(z.string()),
2849
+ mustAsk: z.boolean(),
2850
+ reason: z.string(),
2851
+ });
2852
+
2853
+ /**
2854
+ * Tracking consent, scoped to the calling org.
2855
+ *
2856
+ * Every call is answered for the org whose key made it. There is no shape here
2857
+ * that could read or write another org's consent — consent is given to a
2858
+ * controller, and one controller's record is not another's to act on.
2859
+ *
2860
+ * The visitor is identified by an `x-biab-visitor` header carrying an opaque id
2861
+ * from the caller's own first-party cookie. `@businessdash/sdk/consent` manages
2862
+ * that for you; these methods are the transport underneath it.
2863
+ */
2864
+ export class BiabDevConsentClient {
2865
+ constructor(private readonly client: BiabDevClient) {}
2866
+
2867
+ /** The notice to show, and what this visitor currently allows. */
2868
+ async get(visitorKey?: string | null): Promise<{
2869
+ categories: string[];
2870
+ noticeText: string | null;
2871
+ noticeVersion: number;
2872
+ allowed: string[];
2873
+ mustAsk: boolean;
2874
+ reason: string;
2875
+ }> {
2876
+ return this.client.request({
2877
+ path: "consent",
2878
+ responseSchema: consentStateSchema,
2879
+ ...(visitorKey ? { headers: { "x-biab-visitor": visitorKey } } : {}),
2880
+ });
2881
+ }
2882
+
2883
+ /**
2884
+ * Record the visitor's answer.
2885
+ *
2886
+ * `granted: []` is a real answer — "asked, refused everything" — and is
2887
+ * stored as one, so the banner does not reappear on the next page.
2888
+ */
2889
+ async record(input: {
2890
+ visitorKey: string;
2891
+ granted: string[];
2892
+ noticeVersion: number;
2893
+ source?: "banner" | "settings";
2894
+ }): Promise<{ ok: boolean }> {
2895
+ return this.client.request({
2896
+ method: "POST",
2897
+ path: "consent",
2898
+ body: {
2899
+ granted: input.granted,
2900
+ noticeVersion: input.noticeVersion,
2901
+ source: input.source ?? "banner",
2902
+ },
2903
+ headers: { "x-biab-visitor": input.visitorKey },
2904
+ responseSchema: z.object({ ok: z.boolean() }),
2905
+ });
2906
+ }
2907
+ }
2908
+
2830
2909
  export class BiabDevChatbotClient {
2831
2910
  constructor(private readonly client: BiabDevClient) {}
2832
2911
 
@@ -3190,6 +3269,26 @@ export class BiabDevCustomerPortalClient {
3190
3269
  });
3191
3270
  }
3192
3271
 
3272
+ /**
3273
+ * The signed-in customer's quiz results at this org — the SDK side of
3274
+ * the account-gated reveal: an invitation completed with "create an
3275
+ * account to see how you did" surfaces its result here (and only to
3276
+ * the invited email). Interview-mode quizzes come back with
3277
+ * `scoreWithheld: true` and no verdict, by the org's own setting.
3278
+ */
3279
+ async myQuizResults(options?: {
3280
+ token?: string;
3281
+ }): Promise<CustomerPortalQuizResultsResponse> {
3282
+ const params = new URLSearchParams();
3283
+ if (options?.token) params.set("token", options.token);
3284
+ const qs = params.toString();
3285
+ return this.client.request({
3286
+ path: `customer-portal/quiz-results${qs ? `?${qs}` : ""}`,
3287
+ responseSchema: customerPortalQuizResultsResponseSchema,
3288
+ headers: this.headers(),
3289
+ });
3290
+ }
3291
+
3193
3292
  async updateProfile(
3194
3293
  input: CustomerPortalUpdateProfileInput,
3195
3294
  ): Promise<CustomerPortalUpdateProfileResponse> {
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `@businessdash/sdk/consent` — tracking consent, per org.
3
+ *
4
+ * ## The rule that shapes everything here
5
+ *
6
+ * Consent is given to a **controller**, and each org is a separate controller.
7
+ * It does not travel between them in either direction:
8
+ *
9
+ * - A visitor who refuses at org 2 has said nothing to org 1. Org 1 keeps
10
+ * collecting.
11
+ * - A visitor who consents at org 1 has NOT consented at org 2. Org 2 may not
12
+ * start collecting on the strength of it.
13
+ *
14
+ * The second half is the one that gets skipped, and it is the one with legal
15
+ * teeth: acting on another controller's consent is processing without a lawful
16
+ * basis.
17
+ *
18
+ * That is why the visitor key lives in a **first-party cookie on the org's own
19
+ * domain**, and why every call carries the org's own API key. Two sites cannot
20
+ * see each other's cookies, so the isolation is enforced by the browser rather
21
+ * than by us remembering to scope a query.
22
+ *
23
+ * ## Gate analytics on it
24
+ *
25
+ * ```ts
26
+ * const consent = await loadConsent({ client });
27
+ * if (consent.allows('analytics')) {
28
+ * initBiabAnalytics({ siteId, baseUrl, apiKey });
29
+ * }
30
+ * if (consent.mustAsk) showYourBanner(consent);
31
+ * ```
32
+ *
33
+ * `initBiabAnalytics` is deliberately NOT made to call this itself. A gate that
34
+ * fires implicitly is one nobody can see in review, and the decision of what
35
+ * happens before consent — render nothing, render a placeholder, collect
36
+ * essential-only — belongs to the site, not to us.
37
+ *
38
+ * @module
39
+ */
40
+
41
+ export type ConsentCategory = "analytics" | "marketing" | "personalisation";
42
+
43
+ /** Why consent resolved the way it did. Useful in an org's audit view. */
44
+ export type ConsentReason =
45
+ | "browser_signal"
46
+ | "explicit"
47
+ | "stale_notice"
48
+ | "default_allow"
49
+ | "default_deny";
50
+
51
+ export type ConsentState = {
52
+ /** Categories this org uses at all. Render one banner row per entry. */
53
+ categories: ConsentCategory[];
54
+ noticeText: string | null;
55
+ noticeVersion: number;
56
+ /** What may be used right now. */
57
+ allowed: ConsentCategory[];
58
+ /** True when the visitor has not answered and a banner should show. */
59
+ mustAsk: boolean;
60
+ reason: ConsentReason;
61
+ /** `allowed.includes(category)`, spelled as a question. */
62
+ allows(category: ConsentCategory): boolean;
63
+ };
64
+
65
+ /** The subset of the SDK client this needs. */
66
+ export type ConsentClientLike = {
67
+ request(input: {
68
+ method?: string;
69
+ path: string;
70
+ body?: unknown;
71
+ headers?: Record<string, string>;
72
+ responseSchema: unknown;
73
+ }): Promise<unknown>;
74
+ };
75
+
76
+ const VISITOR_COOKIE = "biab_visitor";
77
+ const VISITOR_HEADER = "x-biab-visitor";
78
+
79
+ /**
80
+ * The visitor's id for THIS site, minted on first need.
81
+ *
82
+ * A first-party cookie on the org's own domain, which is what makes the
83
+ * per-org isolation structural: the same person visiting two orgs gets two
84
+ * unrelated keys, because two domains cannot read each other's cookies. There
85
+ * is no cross-site identifier here and deliberately no way to add one.
86
+ *
87
+ * Returns null on the server, where there is no visitor to identify.
88
+ */
89
+ export function visitorKey(): string | null {
90
+ if (typeof document === "undefined") return null;
91
+
92
+ const found = document.cookie
93
+ .split(";")
94
+ .map((part) => part.trim())
95
+ .find((part) => part.startsWith(`${VISITOR_COOKIE}=`));
96
+ if (found) return decodeURIComponent(found.slice(VISITOR_COOKIE.length + 1));
97
+
98
+ const key =
99
+ typeof crypto !== "undefined" && "randomUUID" in crypto
100
+ ? crypto.randomUUID()
101
+ : `v${Date.now()}${Math.floor(Math.random() * 1e9)}`;
102
+
103
+ // SameSite=Lax and no Secure flag would leak on http; Secure is set unless
104
+ // we are plainly on localhost, where it would stop the cookie working at all.
105
+ const secure = location.protocol === "https:" ? "; Secure" : "";
106
+ // biome-ignore lint/suspicious/noDocumentCookie: the suggested CookieStore
107
+ // API has no Safari or Firefox support. A consent banner has to work in
108
+ // every browser a visitor might arrive in — that is the entire point of it —
109
+ // so the universally supported API is the correct one here.
110
+ document.cookie = `${VISITOR_COOKIE}=${encodeURIComponent(key)}; Path=/; Max-Age=31536000; SameSite=Lax${secure}`;
111
+ return key;
112
+ }
113
+
114
+ /** True when the browser is signalling a global opt-out (DNT or GPC). */
115
+ export function browserOptOut(): boolean {
116
+ if (typeof navigator === "undefined") return false;
117
+ const nav = navigator as Navigator & {
118
+ doNotTrack?: string;
119
+ globalPrivacyControl?: boolean;
120
+ };
121
+ if (nav.globalPrivacyControl === true) return true;
122
+ const dnt =
123
+ nav.doNotTrack ??
124
+ (typeof window === "undefined"
125
+ ? undefined
126
+ : (window as Window & { doNotTrack?: string }).doNotTrack);
127
+ return dnt === "1" || dnt === "yes";
128
+ }
129
+
130
+ function toState(raw: {
131
+ categories?: string[];
132
+ noticeText?: string | null;
133
+ noticeVersion?: number;
134
+ allowed?: string[];
135
+ mustAsk?: boolean;
136
+ reason?: string;
137
+ }): ConsentState {
138
+ const categories = (raw.categories ?? []) as ConsentCategory[];
139
+ const allowed = (raw.allowed ?? []) as ConsentCategory[];
140
+ return {
141
+ categories,
142
+ noticeText: raw.noticeText ?? null,
143
+ noticeVersion: raw.noticeVersion ?? 1,
144
+ allowed,
145
+ mustAsk: raw.mustAsk ?? true,
146
+ reason: (raw.reason ?? "default_deny") as ConsentReason,
147
+ allows: (category) => allowed.includes(category),
148
+ };
149
+ }
150
+
151
+ /**
152
+ * What this visitor currently allows, at this org.
153
+ *
154
+ * On any failure this returns a **deny-everything** state rather than throwing.
155
+ * A consent gate that fails open collects data nobody agreed to; one that fails
156
+ * closed loses some analytics. Only one of those is a compliance incident.
157
+ */
158
+ export async function loadConsent(options: {
159
+ client: ConsentClientLike;
160
+ }): Promise<ConsentState> {
161
+ const key = visitorKey();
162
+ try {
163
+ const raw = (await options.client.request({
164
+ path: "consent",
165
+ ...(key ? { headers: { [VISITOR_HEADER]: key } } : {}),
166
+ responseSchema: { parse: (v: unknown) => v },
167
+ })) as Parameters<typeof toState>[0];
168
+ return toState(raw);
169
+ } catch {
170
+ return {
171
+ categories: [],
172
+ noticeText: null,
173
+ noticeVersion: 1,
174
+ allowed: [],
175
+ mustAsk: false,
176
+ reason: "default_deny",
177
+ allows: () => false,
178
+ };
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Record the visitor's answer.
184
+ *
185
+ * `granted: []` is a real answer — "asked, refused everything" — and is stored
186
+ * as one, so the banner does not reappear on the next page.
187
+ */
188
+ export async function recordConsent(options: {
189
+ client: ConsentClientLike;
190
+ granted: ConsentCategory[];
191
+ noticeVersion: number;
192
+ source?: "banner" | "settings";
193
+ }): Promise<{ ok: boolean }> {
194
+ const key = visitorKey();
195
+ if (!key) return { ok: false };
196
+
197
+ try {
198
+ await options.client.request({
199
+ method: "POST",
200
+ path: "consent",
201
+ body: {
202
+ granted: options.granted,
203
+ noticeVersion: options.noticeVersion,
204
+ source: options.source ?? "banner",
205
+ },
206
+ headers: { [VISITOR_HEADER]: key },
207
+ responseSchema: { parse: (v: unknown) => v },
208
+ });
209
+ return { ok: true };
210
+ } catch {
211
+ return { ok: false };
212
+ }
213
+ }
package/src/contracts.ts CHANGED
@@ -995,6 +995,44 @@ export const customerPortalProfileSchema = z
995
995
  })
996
996
  .nullable();
997
997
 
998
+ /**
999
+ * One quiz outcome for the signed-in customer. `scoreWithheld` marks an
1000
+ * interview-mode quiz: the attempt exists, the verdict is staff-only —
1001
+ * render "completed" and nothing else.
1002
+ */
1003
+ export const customerPortalQuizResultSchema = z.object({
1004
+ invitationId: z.string(),
1005
+ quizName: z.string(),
1006
+ competency: z.string().nullable(),
1007
+ status: z.string(),
1008
+ completedAt: z.string().nullable(),
1009
+ scoreWithheld: z.boolean(),
1010
+ result: z
1011
+ .object({
1012
+ status: z.string(),
1013
+ scorePercent: z.number().nullable(),
1014
+ provisionalScorePercent: z.number().nullable(),
1015
+ passed: z.boolean().nullable(),
1016
+ pointsEarned: z.number().nullable(),
1017
+ pointsPossible: z.number().nullable(),
1018
+ passingScore: z.number(),
1019
+ results: z.array(z.record(z.unknown())),
1020
+ submittedAt: z.string().nullable(),
1021
+ durationSeconds: z.number().nullable(),
1022
+ })
1023
+ .nullable(),
1024
+ });
1025
+ export type CustomerPortalQuizResult = z.infer<
1026
+ typeof customerPortalQuizResultSchema
1027
+ >;
1028
+
1029
+ export const customerPortalQuizResultsResponseSchema = z.object({
1030
+ items: z.array(customerPortalQuizResultSchema),
1031
+ });
1032
+ export type CustomerPortalQuizResultsResponse = z.infer<
1033
+ typeof customerPortalQuizResultsResponseSchema
1034
+ >;
1035
+
998
1036
  export const customerPortalUpdateProfileInputSchema = z.object({
999
1037
  displayName: z.string().min(1).max(120).optional(),
1000
1038
  profile: z.record(z.unknown()).optional(),