@businessdash/sdk 0.9.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +899 -0
- package/dist/adapters-revalidate.d.ts +44 -0
- package/dist/adapters-revalidate.d.ts.map +1 -0
- package/dist/adapters-revalidate.js +68 -0
- package/dist/adapters-revalidate.js.map +1 -0
- package/dist/alpha.d.ts +21 -0
- package/dist/alpha.d.ts.map +1 -0
- package/dist/alpha.js +21 -0
- package/dist/alpha.js.map +1 -0
- package/dist/analytics-core.d.ts +34 -0
- package/dist/analytics-core.d.ts.map +1 -0
- package/dist/analytics-core.js +138 -0
- package/dist/analytics-core.js.map +1 -0
- package/dist/analytics-server.d.ts +54 -0
- package/dist/analytics-server.d.ts.map +1 -0
- package/dist/analytics-server.js +78 -0
- package/dist/analytics-server.js.map +1 -0
- package/dist/attribution-logo.d.ts +2 -0
- package/dist/attribution-logo.d.ts.map +1 -0
- package/dist/attribution-logo.js +5 -0
- package/dist/attribution-logo.js.map +1 -0
- package/dist/auth-handler.d.ts +92 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +277 -0
- package/dist/auth-handler.js.map +1 -0
- package/dist/biab-forms.css +484 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1824 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +1681 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +2845 -0
- package/dist/client.js.map +1 -0
- package/dist/collections.d.ts +13 -0
- package/dist/collections.d.ts.map +1 -0
- package/dist/collections.js +13 -0
- package/dist/collections.js.map +1 -0
- package/dist/contracts.d.ts +14609 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +2428 -0
- package/dist/contracts.js.map +1 -0
- package/dist/data-model-schema.d.ts +962 -0
- package/dist/data-model-schema.d.ts.map +1 -0
- package/dist/data-model-schema.js +2295 -0
- package/dist/data-model-schema.js.map +1 -0
- package/dist/distribution.d.ts +38 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +61 -0
- package/dist/distribution.js.map +1 -0
- package/dist/element.d.ts +121 -0
- package/dist/element.d.ts.map +1 -0
- package/dist/element.js +276 -0
- package/dist/element.js.map +1 -0
- package/dist/enums.d.ts +25 -0
- package/dist/enums.d.ts.map +1 -0
- package/dist/enums.js +38 -0
- package/dist/enums.js.map +1 -0
- package/dist/errors.d.ts +195 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +188 -0
- package/dist/errors.js.map +1 -0
- package/dist/forms-core/autocomplete.d.ts +37 -0
- package/dist/forms-core/autocomplete.d.ts.map +1 -0
- package/dist/forms-core/autocomplete.js +82 -0
- package/dist/forms-core/autocomplete.js.map +1 -0
- package/dist/forms-core/business-function.d.ts +61 -0
- package/dist/forms-core/business-function.d.ts.map +1 -0
- package/dist/forms-core/business-function.js +149 -0
- package/dist/forms-core/business-function.js.map +1 -0
- package/dist/forms-core/calculation.d.ts +72 -0
- package/dist/forms-core/calculation.d.ts.map +1 -0
- package/dist/forms-core/calculation.js +103 -0
- package/dist/forms-core/calculation.js.map +1 -0
- package/dist/forms-core/controller.d.ts +249 -0
- package/dist/forms-core/controller.d.ts.map +1 -0
- package/dist/forms-core/controller.js +590 -0
- package/dist/forms-core/controller.js.map +1 -0
- package/dist/forms-core/elements.d.ts +44 -0
- package/dist/forms-core/elements.d.ts.map +1 -0
- package/dist/forms-core/elements.js +68 -0
- package/dist/forms-core/elements.js.map +1 -0
- package/dist/forms-core/email-domains.d.ts +10 -0
- package/dist/forms-core/email-domains.d.ts.map +1 -0
- package/dist/forms-core/email-domains.js +69 -0
- package/dist/forms-core/email-domains.js.map +1 -0
- package/dist/forms-core/file-rules.d.ts +51 -0
- package/dist/forms-core/file-rules.d.ts.map +1 -0
- package/dist/forms-core/file-rules.js +105 -0
- package/dist/forms-core/file-rules.js.map +1 -0
- package/dist/forms-core/formula.d.ts +114 -0
- package/dist/forms-core/formula.d.ts.map +1 -0
- package/dist/forms-core/formula.js +1170 -0
- package/dist/forms-core/formula.js.map +1 -0
- package/dist/forms-core/index.d.ts +24 -0
- package/dist/forms-core/index.d.ts.map +1 -0
- package/dist/forms-core/index.js +28 -0
- package/dist/forms-core/index.js.map +1 -0
- package/dist/forms-core/units.d.ts +49 -0
- package/dist/forms-core/units.d.ts.map +1 -0
- package/dist/forms-core/units.js +255 -0
- package/dist/forms-core/units.js.map +1 -0
- package/dist/forms-core/us-states.d.ts +14 -0
- package/dist/forms-core/us-states.d.ts.map +1 -0
- package/dist/forms-core/us-states.js +66 -0
- package/dist/forms-core/us-states.js.map +1 -0
- package/dist/forms-core/validation.d.ts +266 -0
- package/dist/forms-core/validation.d.ts.map +1 -0
- package/dist/forms-core/validation.js +850 -0
- package/dist/forms-core/validation.js.map +1 -0
- package/dist/forms.d.ts +19 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +29 -0
- package/dist/forms.js.map +1 -0
- package/dist/gallery-types.d.ts +32 -0
- package/dist/gallery-types.d.ts.map +1 -0
- package/dist/gallery-types.js +2 -0
- package/dist/gallery-types.js.map +1 -0
- package/dist/graphql.d.ts +97 -0
- package/dist/graphql.d.ts.map +1 -0
- package/dist/graphql.js +212 -0
- package/dist/graphql.js.map +1 -0
- package/dist/index.cjs +19448 -0
- package/dist/index.d.cts +19 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/marketing-schema.d.ts +216 -0
- package/dist/marketing-schema.d.ts.map +1 -0
- package/dist/marketing-schema.js +200 -0
- package/dist/marketing-schema.js.map +1 -0
- package/dist/mcp.d.ts +42 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +87 -0
- package/dist/mcp.js.map +1 -0
- package/dist/native.d.ts +45 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +43 -0
- package/dist/native.js.map +1 -0
- package/dist/next-revalidate.d.ts +43 -0
- package/dist/next-revalidate.d.ts.map +1 -0
- package/dist/next-revalidate.js +84 -0
- package/dist/next-revalidate.js.map +1 -0
- package/dist/parallel-pages.d.ts +102 -0
- package/dist/parallel-pages.d.ts.map +1 -0
- package/dist/parallel-pages.js +108 -0
- package/dist/parallel-pages.js.map +1 -0
- package/dist/payment-icons.d.ts +66 -0
- package/dist/payment-icons.d.ts.map +1 -0
- package/dist/payment-icons.js +160 -0
- package/dist/payment-icons.js.map +1 -0
- package/dist/proxy.d.ts +67 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +97 -0
- package/dist/proxy.js.map +1 -0
- package/dist/react-analytics.d.ts +39 -0
- package/dist/react-analytics.d.ts.map +1 -0
- package/dist/react-analytics.js +59 -0
- package/dist/react-analytics.js.map +1 -0
- package/dist/react-attribution.d.ts +99 -0
- package/dist/react-attribution.d.ts.map +1 -0
- package/dist/react-attribution.js +52 -0
- package/dist/react-attribution.js.map +1 -0
- package/dist/react-marketing.d.ts +181 -0
- package/dist/react-marketing.d.ts.map +1 -0
- package/dist/react-marketing.js +215 -0
- package/dist/react-marketing.js.map +1 -0
- package/dist/react.d.ts +727 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +2418 -0
- package/dist/react.js.map +1 -0
- package/dist/reserved-paths.d.ts +24 -0
- package/dist/reserved-paths.d.ts.map +1 -0
- package/dist/reserved-paths.js +60 -0
- package/dist/reserved-paths.js.map +1 -0
- package/dist/revalidate-verify.d.ts +30 -0
- package/dist/revalidate-verify.d.ts.map +1 -0
- package/dist/revalidate-verify.js +74 -0
- package/dist/revalidate-verify.js.map +1 -0
- package/dist/sdk.d.ts +678 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +801 -0
- package/dist/sdk.js.map +1 -0
- package/dist/seo.d.ts +218 -0
- package/dist/seo.d.ts.map +1 -0
- package/dist/seo.js +309 -0
- package/dist/seo.js.map +1 -0
- package/dist/socials.d.ts +47 -0
- package/dist/socials.d.ts.map +1 -0
- package/dist/socials.js +172 -0
- package/dist/socials.js.map +1 -0
- package/dist/static.d.ts +14 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +14 -0
- package/dist/static.js.map +1 -0
- package/dist/url-pattern.d.ts +47 -0
- package/dist/url-pattern.d.ts.map +1 -0
- package/dist/url-pattern.js +124 -0
- package/dist/url-pattern.js.map +1 -0
- package/dist/vanilla.d.ts +77 -0
- package/dist/vanilla.d.ts.map +1 -0
- package/dist/vanilla.js +903 -0
- package/dist/vanilla.js.map +1 -0
- package/openapi.json +13779 -0
- package/package.json +204 -0
- package/src/adapters-revalidate.ts +88 -0
- package/src/alpha.ts +89 -0
- package/src/analytics-core.ts +175 -0
- package/src/analytics-server.ts +99 -0
- package/src/angular/biab-form.component.ts +1443 -0
- package/src/angular/index.ts +26 -0
- package/src/attribution-logo.ts +5 -0
- package/src/auth-handler.ts +352 -0
- package/src/cli.ts +2517 -0
- package/src/client.ts +3578 -0
- package/src/collections.ts +101 -0
- package/src/contracts.ts +3390 -0
- package/src/data-model-schema.ts +4042 -0
- package/src/distribution.ts +73 -0
- package/src/element.ts +321 -0
- package/src/enums.ts +65 -0
- package/src/errors.ts +338 -0
- package/src/forms-core/autocomplete.ts +97 -0
- package/src/forms-core/business-function.ts +209 -0
- package/src/forms-core/calculation.ts +167 -0
- package/src/forms-core/controller.ts +922 -0
- package/src/forms-core/elements.ts +86 -0
- package/src/forms-core/email-domains.ts +68 -0
- package/src/forms-core/file-rules.ts +123 -0
- package/src/forms-core/formula.ts +1339 -0
- package/src/forms-core/index.ts +122 -0
- package/src/forms-core/units.ts +308 -0
- package/src/forms-core/us-states.ts +68 -0
- package/src/forms-core/validation.ts +1037 -0
- package/src/forms.ts +63 -0
- package/src/gallery-types.ts +31 -0
- package/src/graphql.ts +295 -0
- package/src/index.ts +505 -0
- package/src/marketing-schema.ts +395 -0
- package/src/mcp.ts +108 -0
- package/src/native.ts +73 -0
- package/src/next-revalidate.ts +124 -0
- package/src/parallel-pages.ts +192 -0
- package/src/payment-icons.ts +205 -0
- package/src/proxy.ts +173 -0
- package/src/qwik.tsx +1234 -0
- package/src/react-analytics.tsx +76 -0
- package/src/react-attribution.tsx +192 -0
- package/src/react-marketing.tsx +338 -0
- package/src/react.tsx +5495 -0
- package/src/reserved-paths.ts +65 -0
- package/src/revalidate-verify.ts +98 -0
- package/src/sdk.ts +1671 -0
- package/src/seo.ts +488 -0
- package/src/socials.ts +230 -0
- package/src/solid.tsx +1466 -0
- package/src/static.ts +21 -0
- package/src/styles/biab-forms.css +484 -0
- package/src/svelte/BiabForm.svelte +1069 -0
- package/src/svelte/context.ts +57 -0
- package/src/svelte/index.ts +79 -0
- package/src/url-pattern.ts +153 -0
- package/src/vanilla.ts +1290 -0
- package/src/vue/BiabForm.vue +1538 -0
- package/src/vue/index.ts +119 -0
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level SDK surface — what the consumer actually writes against.
|
|
3
|
+
*
|
|
4
|
+
* The principle: the consumer authors design + granular data requests,
|
|
5
|
+
* and that's it. Auth, caching, revalidation, tagging, retries — all
|
|
6
|
+
* handled inside this module so the consumer code reads as:
|
|
7
|
+
*
|
|
8
|
+
* const items = await biab.gallery.list({
|
|
9
|
+
* limit: 12,
|
|
10
|
+
* fields: ["src", "title", "category", "blurDataURL"],
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* Auto-applied per request:
|
|
14
|
+
* • `Authorization: Bearer <apiKey>`
|
|
15
|
+
* • `next: { tags: ["biab:<resource>", "biab:site:<orgId-when-known>"],
|
|
16
|
+
* revalidate: <longBackstop> }` — invisible on non-Next
|
|
17
|
+
* runtimes that ignore the `next` key
|
|
18
|
+
* • Field selection forwarded as `?fields=…` so the platform
|
|
19
|
+
* SELECTs only what's needed.
|
|
20
|
+
*
|
|
21
|
+
* The companion `@businessdash/sdk/next/revalidate` route export closes
|
|
22
|
+
* the loop: when BIAB POSTs `content.published`, the route handler
|
|
23
|
+
* verifies the HMAC and calls `revalidateTag(...)` to bust the local
|
|
24
|
+
* Next cache. End-to-end, the consumer wrote ~5 lines.
|
|
25
|
+
*/
|
|
26
|
+
import { BiabDevAddressClient, BiabDevBlogClient, BiabDevCartClient, BiabDevCheckoutClient, BiabDevClient, BiabDevCouponsClient, BiabDevCustomerPortalClient, BiabDevFollowersClient, BiabDevMarketingClient, BiabDevMarketingPagesClient, BiabDevNotificationsClient, BiabDevParallelPagesClient, BiabDevReviewsClient, BiabDevShippingClient, BiabDevStorefrontClient, BiabDevSubscriptionsClient, } from "./client.js";
|
|
27
|
+
import { getEmailDomain, isConsumerEmailDomain, } from "./forms-core/email-domains.js";
|
|
28
|
+
// Apex host on purpose: www.biab.app 307-redirects to the apex and CORS
|
|
29
|
+
// preflight doesn't follow redirects, which breaks browser publishable-token
|
|
30
|
+
// clients. Server-side callers are unaffected, but one default keeps them aligned.
|
|
31
|
+
const DEFAULT_BASE_URL = "https://biab.app/api/package/v1";
|
|
32
|
+
const DEFAULT_REVALIDATE_SECONDS = 60 * 60 * 24; // 24h backstop
|
|
33
|
+
function buildHeaders(state) {
|
|
34
|
+
const headers = {
|
|
35
|
+
Authorization: `Bearer ${state.apiKey}`,
|
|
36
|
+
Accept: "application/json",
|
|
37
|
+
};
|
|
38
|
+
if (state.siteOrigin)
|
|
39
|
+
headers["Origin"] = state.siteOrigin;
|
|
40
|
+
return headers;
|
|
41
|
+
}
|
|
42
|
+
function buildNextHint(tags, seconds) {
|
|
43
|
+
// `next` is a Next.js-only field on RequestInit. Non-Next runtimes
|
|
44
|
+
// (Cloudflare Workers, Deno, raw Node fetch) ignore it cleanly.
|
|
45
|
+
return { tags, revalidate: seconds };
|
|
46
|
+
}
|
|
47
|
+
async function jsonGet(state, pathWithQuery, tags) {
|
|
48
|
+
const url = `${state.baseUrl.replace(/\/$/, "")}/${pathWithQuery.replace(/^\//, "")}`;
|
|
49
|
+
const init = {
|
|
50
|
+
method: "GET",
|
|
51
|
+
headers: buildHeaders(state),
|
|
52
|
+
next: buildNextHint(tags, state.revalidateSeconds),
|
|
53
|
+
};
|
|
54
|
+
const res = await state.fetch(url, init);
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const text = await res.text().catch(() => "");
|
|
57
|
+
throw new Error(`BIAB SDK ${res.status} ${res.statusText} on ${pathWithQuery}: ${text.slice(0, 200)}`);
|
|
58
|
+
}
|
|
59
|
+
return (await res.json());
|
|
60
|
+
}
|
|
61
|
+
function createGalleryResource(state) {
|
|
62
|
+
return {
|
|
63
|
+
async list(params) {
|
|
64
|
+
const query = new URLSearchParams();
|
|
65
|
+
if (params?.limit)
|
|
66
|
+
query.set("limit", String(params.limit));
|
|
67
|
+
if (params?.fields && params.fields.length > 0) {
|
|
68
|
+
query.set("fields", params.fields.join(","));
|
|
69
|
+
}
|
|
70
|
+
const qs = query.toString();
|
|
71
|
+
const path = `sites/${encodeURIComponent(state.siteId)}/gallery${qs ? `?${qs}` : ""}`;
|
|
72
|
+
const tags = ["biab:gallery", `biab:site:${state.siteId}`];
|
|
73
|
+
// The server projects to exactly the requested `fields`, so the row
|
|
74
|
+
// shape is `GalleryItemFor<F>` — a fact only the wire can vouch for.
|
|
75
|
+
// Stating it as the fetch's type argument confines the assumption to
|
|
76
|
+
// this one boundary. The previous `as never` was strictly worse: it
|
|
77
|
+
// asserted the items were assignable to *anything*, so a genuine
|
|
78
|
+
// mismatch between projection and type would never have shown up.
|
|
79
|
+
const result = await jsonGet(state, path, tags);
|
|
80
|
+
return result.items ?? [];
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function createSchedulingResource(state) {
|
|
85
|
+
const sitePath = `sites/${encodeURIComponent(state.siteId)}/scheduling`;
|
|
86
|
+
async function postJson(path, body, tags) {
|
|
87
|
+
const url = `${state.baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
88
|
+
const init = {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: { ...buildHeaders(state), "Content-Type": "application/json" },
|
|
91
|
+
body: JSON.stringify(body),
|
|
92
|
+
next: { tags, revalidate: 0 },
|
|
93
|
+
};
|
|
94
|
+
const res = await state.fetch(url, init);
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const text = await res.text().catch(() => "");
|
|
97
|
+
throw new Error(`BIAB SDK ${res.status} ${res.statusText} on ${path}: ${text.slice(0, 200)}`);
|
|
98
|
+
}
|
|
99
|
+
return (await res.json());
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
async listEventTypes() {
|
|
103
|
+
const tags = ["biab:scheduling", `biab:site:${state.siteId}`];
|
|
104
|
+
const result = await jsonGet(state, `${sitePath}/event-types`, tags);
|
|
105
|
+
return result.items ?? [];
|
|
106
|
+
},
|
|
107
|
+
async getEventType(slug) {
|
|
108
|
+
const tags = [
|
|
109
|
+
"biab:scheduling",
|
|
110
|
+
`biab:scheduling:event:${slug}`,
|
|
111
|
+
`biab:site:${state.siteId}`,
|
|
112
|
+
];
|
|
113
|
+
return await jsonGet(state, `${sitePath}/event-types/${encodeURIComponent(slug)}`, tags);
|
|
114
|
+
},
|
|
115
|
+
async getAvailableSlots(slug, params) {
|
|
116
|
+
const query = new URLSearchParams({
|
|
117
|
+
from: params.from.toISOString(),
|
|
118
|
+
to: params.to.toISOString(),
|
|
119
|
+
});
|
|
120
|
+
const path = `${sitePath}/event-types/${encodeURIComponent(slug)}/slots?${query.toString()}`;
|
|
121
|
+
// Slots are dynamic — no caching, no tags. revalidate: 0 keeps
|
|
122
|
+
// Next from applying the default backstop.
|
|
123
|
+
const url = `${state.baseUrl.replace(/\/$/, "")}/${path}`;
|
|
124
|
+
const init = {
|
|
125
|
+
method: "GET",
|
|
126
|
+
headers: buildHeaders(state),
|
|
127
|
+
next: { revalidate: 0 },
|
|
128
|
+
cache: "no-store",
|
|
129
|
+
};
|
|
130
|
+
const res = await state.fetch(url, init);
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const text = await res.text().catch(() => "");
|
|
133
|
+
throw new Error(`BIAB SDK ${res.status} ${res.statusText} on ${path}: ${text.slice(0, 200)}`);
|
|
134
|
+
}
|
|
135
|
+
const data = (await res.json());
|
|
136
|
+
return data.slots ?? [];
|
|
137
|
+
},
|
|
138
|
+
async confirmBooking(input) {
|
|
139
|
+
return await postJson(`${sitePath}/bookings`, {
|
|
140
|
+
eventTypeSlug: input.eventTypeSlug,
|
|
141
|
+
startAt: input.startAt.toISOString(),
|
|
142
|
+
invitee: input.invitee,
|
|
143
|
+
answers: input.answers,
|
|
144
|
+
notes: input.notes ?? null,
|
|
145
|
+
schedulingLinkId: input.schedulingLinkId,
|
|
146
|
+
holderKey: input.holderKey ?? null,
|
|
147
|
+
}, []);
|
|
148
|
+
},
|
|
149
|
+
async getBookingByToken(token, type = "manage") {
|
|
150
|
+
const path = `${sitePath}/bookings/${encodeURIComponent(token)}?type=${type}`;
|
|
151
|
+
const url = `${state.baseUrl.replace(/\/$/, "")}/${path}`;
|
|
152
|
+
const res = await state.fetch(url, {
|
|
153
|
+
method: "GET",
|
|
154
|
+
headers: buildHeaders(state),
|
|
155
|
+
cache: "no-store",
|
|
156
|
+
});
|
|
157
|
+
if (!res.ok) {
|
|
158
|
+
const text = await res.text().catch(() => "");
|
|
159
|
+
throw new Error(`BIAB SDK ${res.status} ${res.statusText}: ${text.slice(0, 200)}`);
|
|
160
|
+
}
|
|
161
|
+
return (await res.json());
|
|
162
|
+
},
|
|
163
|
+
async rescheduleBooking(input) {
|
|
164
|
+
return await postJson(`${sitePath}/bookings/${encodeURIComponent(input.token)}`, {
|
|
165
|
+
action: "reschedule",
|
|
166
|
+
newStartAt: input.newStartAt.toISOString(),
|
|
167
|
+
reason: input.reason ?? null,
|
|
168
|
+
}, []);
|
|
169
|
+
},
|
|
170
|
+
async cancelBooking(input) {
|
|
171
|
+
return await postJson(`${sitePath}/bookings/${encodeURIComponent(input.token)}`, {
|
|
172
|
+
action: "cancel",
|
|
173
|
+
reason: input.reason ?? null,
|
|
174
|
+
}, []);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Runtime validate a submission against the fetched form schema.
|
|
180
|
+
* Returns `ok: true` only when every required field is present and
|
|
181
|
+
* every supplied value passes its type / min / max / regex /
|
|
182
|
+
* options check. Designed to mirror what BIAB's server enforces so
|
|
183
|
+
* the consumer can show inline errors before round-tripping.
|
|
184
|
+
*
|
|
185
|
+
* For build-time type safety, the future `biab-dev codegen forms`
|
|
186
|
+
* CLI emits a `.d.ts` per form so TypeScript can also enforce
|
|
187
|
+
* shape at compile time. Until that ships, this runtime check is
|
|
188
|
+
* the safety net.
|
|
189
|
+
*/
|
|
190
|
+
/**
|
|
191
|
+
* Narrow check for the shape `biab.forms.uploadFile` returns —
|
|
192
|
+
* mirrors what the BIAB hiring mirror reads (storageId, url, name,
|
|
193
|
+
* mimeType, size). Used by both `validateFormSubmission` and any
|
|
194
|
+
* caller that wants to assert "this value is a successful upload."
|
|
195
|
+
*/
|
|
196
|
+
export function isFormFileValue(value) {
|
|
197
|
+
if (!value || typeof value !== "object")
|
|
198
|
+
return false;
|
|
199
|
+
const v = value;
|
|
200
|
+
return (typeof v.storageId === "string" &&
|
|
201
|
+
typeof v.url === "string" &&
|
|
202
|
+
typeof v.name === "string" &&
|
|
203
|
+
typeof v.mimeType === "string" &&
|
|
204
|
+
typeof v.size === "number");
|
|
205
|
+
}
|
|
206
|
+
export function validateFormSubmission(schema, data) {
|
|
207
|
+
const issues = [];
|
|
208
|
+
// A submission may key values by the field's output `key` (preferred) or its
|
|
209
|
+
// legacy UUID `id`. Accept both everywhere.
|
|
210
|
+
const knownFieldIds = new Set();
|
|
211
|
+
for (const f of schema.fields) {
|
|
212
|
+
knownFieldIds.add(f.id);
|
|
213
|
+
if (f.key)
|
|
214
|
+
knownFieldIds.add(f.key);
|
|
215
|
+
}
|
|
216
|
+
for (const field of schema.fields) {
|
|
217
|
+
// `calculation` fields are computed server-side on submit — never user-validated.
|
|
218
|
+
if (field.type === "calculation")
|
|
219
|
+
continue;
|
|
220
|
+
const value = field.key != null && field.key in data ? data[field.key] : data[field.id];
|
|
221
|
+
const scheduleEmpty = field.type === "schedule" &&
|
|
222
|
+
(() => {
|
|
223
|
+
const s = (value ?? {});
|
|
224
|
+
return !((typeof s.slotStartAt === "string" && s.slotStartAt.length > 0) ||
|
|
225
|
+
s.asap === true ||
|
|
226
|
+
(Array.isArray(s.days) && s.days.length > 0) ||
|
|
227
|
+
(Array.isArray(s.times) && s.times.length > 0));
|
|
228
|
+
})();
|
|
229
|
+
const urlEmpty = field.type === "url" &&
|
|
230
|
+
!(typeof value?.url === "string"
|
|
231
|
+
? value.url
|
|
232
|
+
: "");
|
|
233
|
+
const isEmpty = value == null ||
|
|
234
|
+
value === "" ||
|
|
235
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
236
|
+
scheduleEmpty ||
|
|
237
|
+
urlEmpty;
|
|
238
|
+
// Consent boolean: must be exactly `true` (independent of `required`).
|
|
239
|
+
// `false` is otherwise a valid answer for an optional/required toggle.
|
|
240
|
+
const consentUnmet = field.mustBeTrue === true && value !== true;
|
|
241
|
+
if ((field.required && isEmpty) || consentUnmet) {
|
|
242
|
+
issues.push({
|
|
243
|
+
fieldId: field.id,
|
|
244
|
+
label: field.label,
|
|
245
|
+
reason: "required",
|
|
246
|
+
message: consentUnmet
|
|
247
|
+
? `${field.label} must be checked.`
|
|
248
|
+
: `${field.label} is required.`,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (isEmpty)
|
|
253
|
+
continue;
|
|
254
|
+
switch (field.type) {
|
|
255
|
+
case "email": {
|
|
256
|
+
if (typeof value !== "string" ||
|
|
257
|
+
!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(value)) {
|
|
258
|
+
issues.push({
|
|
259
|
+
fieldId: field.id,
|
|
260
|
+
label: field.label,
|
|
261
|
+
reason: "type",
|
|
262
|
+
message: `${field.label} must be a valid email.`,
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (field.emailConfig
|
|
267
|
+
?.businessOnly &&
|
|
268
|
+
isConsumerEmailDomain(getEmailDomain(value) ?? "")) {
|
|
269
|
+
issues.push({
|
|
270
|
+
fieldId: field.id,
|
|
271
|
+
label: field.label,
|
|
272
|
+
reason: "type",
|
|
273
|
+
message: `${field.label} must be a business email address.`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case "url": {
|
|
279
|
+
// Value object: { url, verified }. Validate the `url` member.
|
|
280
|
+
const u = value?.url;
|
|
281
|
+
if (typeof u !== "string" ||
|
|
282
|
+
!/^(https?:\/\/)?([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}(?::\d{1,5})?(?:[/?#]\S*)?$/i.test(u.trim())) {
|
|
283
|
+
issues.push({
|
|
284
|
+
fieldId: field.id,
|
|
285
|
+
label: field.label,
|
|
286
|
+
reason: "type",
|
|
287
|
+
message: `${field.label} must be a valid URL.`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "phone": {
|
|
293
|
+
// Digit-count pre-check mirroring the server: international fields
|
|
294
|
+
// allow E.164 (7-15), otherwise a 10-digit US number (optionally
|
|
295
|
+
// with a leading "1"). The server is the authoritative check.
|
|
296
|
+
const digits = typeof value === "string" ? value.replace(/\D/g, "") : "";
|
|
297
|
+
const intl = field.phoneConfig?.international === true;
|
|
298
|
+
const national = digits.length === 11 && digits.startsWith("1")
|
|
299
|
+
? digits.slice(1)
|
|
300
|
+
: digits;
|
|
301
|
+
const ok = intl
|
|
302
|
+
? digits.length >= 7 && digits.length <= 15
|
|
303
|
+
: national.length === 10;
|
|
304
|
+
if (!ok) {
|
|
305
|
+
issues.push({
|
|
306
|
+
fieldId: field.id,
|
|
307
|
+
label: field.label,
|
|
308
|
+
reason: "type",
|
|
309
|
+
message: `${field.label} must be a valid phone number.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case "number":
|
|
315
|
+
case "slider":
|
|
316
|
+
if (typeof value !== "number") {
|
|
317
|
+
issues.push({
|
|
318
|
+
fieldId: field.id,
|
|
319
|
+
label: field.label,
|
|
320
|
+
reason: "type",
|
|
321
|
+
message: `${field.label} must be a number.`,
|
|
322
|
+
});
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
if (field.type === "number" &&
|
|
326
|
+
field.allowNegative === false &&
|
|
327
|
+
value < 0) {
|
|
328
|
+
issues.push({
|
|
329
|
+
fieldId: field.id,
|
|
330
|
+
label: field.label,
|
|
331
|
+
reason: "min",
|
|
332
|
+
message: `${field.label} must be 0 or greater.`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (field.validation?.min != null && value < field.validation.min) {
|
|
336
|
+
issues.push({
|
|
337
|
+
fieldId: field.id,
|
|
338
|
+
label: field.label,
|
|
339
|
+
reason: "min",
|
|
340
|
+
message: `${field.label} must be at least ${field.validation.min}.`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
if (field.validation?.max != null && value > field.validation.max) {
|
|
344
|
+
issues.push({
|
|
345
|
+
fieldId: field.id,
|
|
346
|
+
label: field.label,
|
|
347
|
+
reason: "max",
|
|
348
|
+
message: `${field.label} must be at most ${field.validation.max}.`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
break;
|
|
352
|
+
case "select":
|
|
353
|
+
case "radio":
|
|
354
|
+
if (field.options &&
|
|
355
|
+
!field.allowCustom &&
|
|
356
|
+
!field.options.includes(String(value))) {
|
|
357
|
+
issues.push({
|
|
358
|
+
fieldId: field.id,
|
|
359
|
+
label: field.label,
|
|
360
|
+
reason: "options",
|
|
361
|
+
message: `${field.label} must be one of: ${field.options.join(", ")}.`,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
break;
|
|
365
|
+
case "checkbox":
|
|
366
|
+
if (typeof value !== "boolean" && !Array.isArray(value)) {
|
|
367
|
+
issues.push({
|
|
368
|
+
fieldId: field.id,
|
|
369
|
+
label: field.label,
|
|
370
|
+
reason: "type",
|
|
371
|
+
message: `${field.label} must be a boolean or list.`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
break;
|
|
375
|
+
case "choice_cards": {
|
|
376
|
+
const ccLabels = (field.choiceConfig?.options ?? []).map((o) => o.label);
|
|
377
|
+
const ccMulti = field.choiceConfig?.mode === "multi";
|
|
378
|
+
const ccVals = ccMulti
|
|
379
|
+
? Array.isArray(value)
|
|
380
|
+
? value
|
|
381
|
+
: null
|
|
382
|
+
: typeof value === "string"
|
|
383
|
+
? [value]
|
|
384
|
+
: null;
|
|
385
|
+
if (ccVals === null) {
|
|
386
|
+
issues.push({
|
|
387
|
+
fieldId: field.id,
|
|
388
|
+
label: field.label,
|
|
389
|
+
reason: "type",
|
|
390
|
+
message: ccMulti
|
|
391
|
+
? `${field.label} must be a list.`
|
|
392
|
+
: `${field.label} must be a string.`,
|
|
393
|
+
});
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
if (ccLabels.length) {
|
|
397
|
+
for (const v of ccVals) {
|
|
398
|
+
if (typeof v !== "string" || !ccLabels.includes(v)) {
|
|
399
|
+
issues.push({
|
|
400
|
+
fieldId: field.id,
|
|
401
|
+
label: field.label,
|
|
402
|
+
reason: "options",
|
|
403
|
+
message: `${field.label} must be one of: ${ccLabels.join(", ")}.`,
|
|
404
|
+
});
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "file_upload":
|
|
412
|
+
case "file": {
|
|
413
|
+
const items = Array.isArray(value) ? value : [value];
|
|
414
|
+
let badShape = false;
|
|
415
|
+
for (const item of items) {
|
|
416
|
+
if (!isFormFileValue(item)) {
|
|
417
|
+
badShape = true;
|
|
418
|
+
break;
|
|
419
|
+
}
|
|
420
|
+
if (field.fileConfig?.maxSize != null &&
|
|
421
|
+
// `fileConfig.maxSize` is MB (builder slider unit); `item.size`
|
|
422
|
+
// is bytes — convert before comparing.
|
|
423
|
+
item.size > field.fileConfig.maxSize * 1024 * 1024) {
|
|
424
|
+
issues.push({
|
|
425
|
+
fieldId: field.id,
|
|
426
|
+
label: field.label,
|
|
427
|
+
reason: "max",
|
|
428
|
+
message: `${field.label} exceeds the ${field.fileConfig.maxSize} MB limit.`,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (badShape) {
|
|
433
|
+
issues.push({
|
|
434
|
+
fieldId: field.id,
|
|
435
|
+
label: field.label,
|
|
436
|
+
reason: "type",
|
|
437
|
+
message: `${field.label} must be uploaded via biab.forms.uploadFile().`,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
if (field.fileConfig?.maxFiles != null &&
|
|
441
|
+
Array.isArray(value) &&
|
|
442
|
+
value.length > field.fileConfig.maxFiles) {
|
|
443
|
+
issues.push({
|
|
444
|
+
fieldId: field.id,
|
|
445
|
+
label: field.label,
|
|
446
|
+
reason: "max",
|
|
447
|
+
message: `${field.label} accepts at most ${field.fileConfig.maxFiles} files.`,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
case "schedule": {
|
|
453
|
+
// { asap?, days?: string[], times?: string[], notes? } — structural
|
|
454
|
+
// parity with the server (which also enforces length caps). Client
|
|
455
|
+
// pre-check only; the server gate is authoritative.
|
|
456
|
+
if (typeof value !== "object" ||
|
|
457
|
+
value === null ||
|
|
458
|
+
Array.isArray(value)) {
|
|
459
|
+
issues.push({
|
|
460
|
+
fieldId: field.id,
|
|
461
|
+
label: field.label,
|
|
462
|
+
reason: "type",
|
|
463
|
+
message: `${field.label} has an unexpected value.`,
|
|
464
|
+
});
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
const s = value;
|
|
468
|
+
const badType = (s.asap !== undefined && typeof s.asap !== "boolean") ||
|
|
469
|
+
(s.notes !== undefined && typeof s.notes !== "string") ||
|
|
470
|
+
["days", "times"].some((k) => s[k] !== undefined &&
|
|
471
|
+
(!Array.isArray(s[k]) ||
|
|
472
|
+
s[k].some((x) => typeof x !== "string")));
|
|
473
|
+
if (badType) {
|
|
474
|
+
issues.push({
|
|
475
|
+
fieldId: field.id,
|
|
476
|
+
label: field.label,
|
|
477
|
+
reason: "type",
|
|
478
|
+
message: `${field.label} has an unexpected value.`,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
default:
|
|
484
|
+
if (field.regexPattern &&
|
|
485
|
+
typeof value === "string" &&
|
|
486
|
+
!new RegExp(field.regexPattern).test(value)) {
|
|
487
|
+
issues.push({
|
|
488
|
+
fieldId: field.id,
|
|
489
|
+
label: field.label,
|
|
490
|
+
reason: "regex",
|
|
491
|
+
message: `${field.label} format is invalid.`,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const key of Object.keys(data)) {
|
|
497
|
+
if (!knownFieldIds.has(key)) {
|
|
498
|
+
issues.push({
|
|
499
|
+
fieldId: key,
|
|
500
|
+
label: key,
|
|
501
|
+
reason: "unknown_field",
|
|
502
|
+
message: `Unknown field "${key}" — not in the form schema.`,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { ok: issues.length === 0, issues };
|
|
507
|
+
}
|
|
508
|
+
/** Normalize the server's `issues` array into typed `FormValidationIssue`s. */
|
|
509
|
+
function mapSubmitIssues(body) {
|
|
510
|
+
const raw = body?.["issues"];
|
|
511
|
+
if (!Array.isArray(raw))
|
|
512
|
+
return undefined;
|
|
513
|
+
return raw.map((it) => {
|
|
514
|
+
const o = (it ?? {});
|
|
515
|
+
const id = typeof o["id"] === "string"
|
|
516
|
+
? o["id"]
|
|
517
|
+
: typeof o["fieldId"] === "string"
|
|
518
|
+
? o["fieldId"]
|
|
519
|
+
: "";
|
|
520
|
+
const label = typeof o["label"] === "string" ? o["label"] : id || "field";
|
|
521
|
+
const reason = typeof o["reason"] === "string" ? o["reason"] : "type";
|
|
522
|
+
return {
|
|
523
|
+
fieldId: id,
|
|
524
|
+
label,
|
|
525
|
+
reason,
|
|
526
|
+
message: typeof o["message"] === "string" ? o["message"] : `${label}: ${reason}`,
|
|
527
|
+
};
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
/** Map a non-2xx submit response (status + parsed body) to a failure result. */
|
|
531
|
+
function mapSubmitResponse(status, body) {
|
|
532
|
+
const message = (typeof body?.["message"] === "string" && body["message"]) ||
|
|
533
|
+
`Submission failed (${status}).`;
|
|
534
|
+
const code = typeof body?.["error"] === "string" ? body["error"] : undefined;
|
|
535
|
+
const gateReason = typeof body?.["reason"] === "string" ? body["reason"] : undefined;
|
|
536
|
+
const issues = mapSubmitIssues(body);
|
|
537
|
+
let reason;
|
|
538
|
+
if (status === 401)
|
|
539
|
+
reason = "unauthorized";
|
|
540
|
+
else if (status === 403)
|
|
541
|
+
reason = "forbidden";
|
|
542
|
+
else if (status === 402)
|
|
543
|
+
reason =
|
|
544
|
+
gateReason === "payment_required"
|
|
545
|
+
? "payment_required"
|
|
546
|
+
: gateReason === "service_suspended"
|
|
547
|
+
? "service_suspended"
|
|
548
|
+
: "plan_required";
|
|
549
|
+
else if (status === 429)
|
|
550
|
+
reason = "rate_limited";
|
|
551
|
+
else if (status === 404)
|
|
552
|
+
reason = "not_found";
|
|
553
|
+
else if (status === 400 || status === 422)
|
|
554
|
+
reason =
|
|
555
|
+
code === "validation_failed" || issues
|
|
556
|
+
? "validation_failed"
|
|
557
|
+
: "invalid_request";
|
|
558
|
+
else
|
|
559
|
+
reason = "server_error";
|
|
560
|
+
return {
|
|
561
|
+
ok: false,
|
|
562
|
+
status,
|
|
563
|
+
reason,
|
|
564
|
+
message,
|
|
565
|
+
...(issues ? { issues } : {}),
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/** Map a thrown error during the pre-submit schema fetch to a failure result. */
|
|
569
|
+
function mapSubmitThrow(err) {
|
|
570
|
+
if (err instanceof Error) {
|
|
571
|
+
// `jsonGet` embeds the HTTP status in its message — recover it if present.
|
|
572
|
+
const m = err.message.match(/BIAB SDK (\d{3})/);
|
|
573
|
+
if (m)
|
|
574
|
+
return mapSubmitResponse(Number(m[1]), null);
|
|
575
|
+
return {
|
|
576
|
+
ok: false,
|
|
577
|
+
status: 0,
|
|
578
|
+
reason: "network_error",
|
|
579
|
+
message: `Could not load the form schema: ${err.message}`,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
status: 0,
|
|
585
|
+
reason: "network_error",
|
|
586
|
+
message: "Could not load the form schema.",
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
function createFormsResource(state) {
|
|
590
|
+
return {
|
|
591
|
+
async schema(slug) {
|
|
592
|
+
// Non-chatbot form read: a form is reachable here when it is **Live**
|
|
593
|
+
// (`isActive`) — independent of `chatbotAccess`. Embedding a form on
|
|
594
|
+
// your own page no longer requires opting it into the AI chatbot.
|
|
595
|
+
const tags = [
|
|
596
|
+
"biab:forms",
|
|
597
|
+
`biab:forms:${slug}`,
|
|
598
|
+
`biab:site:${state.siteId}`,
|
|
599
|
+
];
|
|
600
|
+
const path = `forms/${encodeURIComponent(slug)}`;
|
|
601
|
+
const result = await jsonGet(state, path, tags);
|
|
602
|
+
return result;
|
|
603
|
+
},
|
|
604
|
+
async submit(slug, data, opts) {
|
|
605
|
+
// 1. Client-side schema check (unless skipped) — return, don't throw,
|
|
606
|
+
// so the consumer handles it the same way as a server rejection.
|
|
607
|
+
if (!opts?.skipValidate) {
|
|
608
|
+
let schema;
|
|
609
|
+
try {
|
|
610
|
+
schema = await this.schema(slug);
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
return mapSubmitThrow(err);
|
|
614
|
+
}
|
|
615
|
+
const v = validateFormSubmission(schema, data);
|
|
616
|
+
if (!v.ok) {
|
|
617
|
+
return {
|
|
618
|
+
ok: false,
|
|
619
|
+
status: 0,
|
|
620
|
+
reason: "client_validation_failed",
|
|
621
|
+
message: `Form validation failed — ${v.issues
|
|
622
|
+
.map((i) => `${i.label}: ${i.message}`)
|
|
623
|
+
.join("; ")}`,
|
|
624
|
+
issues: v.issues,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// 2. Round-trip. Map every transport / HTTP outcome to a result.
|
|
629
|
+
// Live (non-chatbot) submit path — gated on `isActive` only.
|
|
630
|
+
const url = `${state.baseUrl.replace(/\/$/, "")}/forms/${encodeURIComponent(slug)}`;
|
|
631
|
+
const init = {
|
|
632
|
+
method: "POST",
|
|
633
|
+
headers: {
|
|
634
|
+
...buildHeaders(state),
|
|
635
|
+
"Content-Type": "application/json",
|
|
636
|
+
},
|
|
637
|
+
body: JSON.stringify({
|
|
638
|
+
data,
|
|
639
|
+
submitterEmail: opts?.submitterEmail,
|
|
640
|
+
submitterName: opts?.submitterName,
|
|
641
|
+
...(opts?.dryRun ? { dryRun: true } : {}),
|
|
642
|
+
...(opts?.source ? { source: opts.source } : {}),
|
|
643
|
+
...(opts?.referrer ? { referrer: opts.referrer } : {}),
|
|
644
|
+
...(opts?.metadata ? { metadata: opts.metadata } : {}),
|
|
645
|
+
}),
|
|
646
|
+
next: { revalidate: 0 },
|
|
647
|
+
};
|
|
648
|
+
let res;
|
|
649
|
+
try {
|
|
650
|
+
res = await state.fetch(url, init);
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
status: 0,
|
|
656
|
+
reason: "network_error",
|
|
657
|
+
message: err instanceof Error
|
|
658
|
+
? `Could not reach BIAB: ${err.message}`
|
|
659
|
+
: "Could not reach BIAB.",
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const body = (await res.json().catch(() => null));
|
|
663
|
+
if (res.ok) {
|
|
664
|
+
return {
|
|
665
|
+
ok: true,
|
|
666
|
+
status: res.status,
|
|
667
|
+
...(body ?? {}),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return mapSubmitResponse(res.status, body);
|
|
671
|
+
},
|
|
672
|
+
async uploadFile(slug, fieldId, file, opts) {
|
|
673
|
+
const filename = opts?.filename ?? (file instanceof File ? file.name : "upload.bin");
|
|
674
|
+
const contentType = file.type || "application/octet-stream";
|
|
675
|
+
const sizeBytes = file.size;
|
|
676
|
+
// Step 1 — presign. The server validates entitlement
|
|
677
|
+
// (`forms.file_uploads`), Live form access, field type, MIME
|
|
678
|
+
// allow-list, and the per-plan / per-field max-size cap.
|
|
679
|
+
const presignUrl = `${state.baseUrl.replace(/\/$/, "")}/forms/${encodeURIComponent(slug)}/upload-url`;
|
|
680
|
+
const presignRes = await state.fetch(presignUrl, {
|
|
681
|
+
method: "POST",
|
|
682
|
+
headers: {
|
|
683
|
+
...buildHeaders(state),
|
|
684
|
+
"Content-Type": "application/json",
|
|
685
|
+
},
|
|
686
|
+
body: JSON.stringify({
|
|
687
|
+
fieldId,
|
|
688
|
+
filename,
|
|
689
|
+
contentType,
|
|
690
|
+
sizeBytes,
|
|
691
|
+
}),
|
|
692
|
+
});
|
|
693
|
+
if (!presignRes.ok) {
|
|
694
|
+
const text = await presignRes.text().catch(() => "");
|
|
695
|
+
if (presignRes.status === 403 || presignRes.status === 402) {
|
|
696
|
+
throw new Error(`BIAB SDK: file upload requires the "forms.file_uploads" entitlement on this org. ${text.slice(0, 200)}`);
|
|
697
|
+
}
|
|
698
|
+
throw new Error(`BIAB SDK ${presignRes.status} ${presignRes.statusText} on upload-url: ${text.slice(0, 200)}`);
|
|
699
|
+
}
|
|
700
|
+
const presign = (await presignRes.json());
|
|
701
|
+
// Step 2 — PUT bytes directly to R2 (signed URL).
|
|
702
|
+
const putRes = await state.fetch(presign.uploadUrl, {
|
|
703
|
+
method: "PUT",
|
|
704
|
+
headers: { "Content-Type": contentType },
|
|
705
|
+
body: file,
|
|
706
|
+
});
|
|
707
|
+
if (!putRes.ok) {
|
|
708
|
+
const text = await putRes.text().catch(() => "");
|
|
709
|
+
throw new Error(`BIAB SDK upload to R2 failed: ${putRes.status} ${putRes.statusText}: ${text.slice(0, 200)}`);
|
|
710
|
+
}
|
|
711
|
+
// Step 3 — return the value to drop into form data.
|
|
712
|
+
return {
|
|
713
|
+
storageId: presign.storageId,
|
|
714
|
+
url: presign.publicUrl,
|
|
715
|
+
name: filename,
|
|
716
|
+
mimeType: contentType,
|
|
717
|
+
size: sizeBytes,
|
|
718
|
+
};
|
|
719
|
+
},
|
|
720
|
+
async paymentIntent(slug, fieldId) {
|
|
721
|
+
const url = `${state.baseUrl.replace(/\/$/, "")}/forms/${encodeURIComponent(slug)}/payment-intent`;
|
|
722
|
+
const res = await state.fetch(url, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers: {
|
|
725
|
+
...buildHeaders(state),
|
|
726
|
+
"Content-Type": "application/json",
|
|
727
|
+
},
|
|
728
|
+
body: JSON.stringify({ fieldId }),
|
|
729
|
+
});
|
|
730
|
+
if (!res.ok) {
|
|
731
|
+
const text = await res.text().catch(() => "");
|
|
732
|
+
throw new Error(`BIAB SDK ${res.status} ${res.statusText} on payment-intent: ${text.slice(0, 200)}`);
|
|
733
|
+
}
|
|
734
|
+
return (await res.json());
|
|
735
|
+
},
|
|
736
|
+
async scheduleSlots(slug, input) {
|
|
737
|
+
const base = state.baseUrl.replace(/\/$/, "");
|
|
738
|
+
const qs = new URLSearchParams({
|
|
739
|
+
fieldId: input.fieldId,
|
|
740
|
+
from: input.from.toISOString(),
|
|
741
|
+
to: input.to.toISOString(),
|
|
742
|
+
});
|
|
743
|
+
const url = `${base}/forms/${encodeURIComponent(slug)}/schedule-slots?${qs.toString()}`;
|
|
744
|
+
const res = await state.fetch(url, {
|
|
745
|
+
method: "GET",
|
|
746
|
+
headers: buildHeaders(state),
|
|
747
|
+
});
|
|
748
|
+
if (!res.ok) {
|
|
749
|
+
const text = await res.text().catch(() => "");
|
|
750
|
+
throw new Error(`BIAB SDK ${res.status} ${res.statusText} on schedule-slots: ${text.slice(0, 200)}`);
|
|
751
|
+
}
|
|
752
|
+
return (await res.json());
|
|
753
|
+
},
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
export function createBiabClient(opts) {
|
|
757
|
+
if (!opts.apiKey) {
|
|
758
|
+
throw new Error("createBiabClient: apiKey is required.");
|
|
759
|
+
}
|
|
760
|
+
if (!opts.siteId) {
|
|
761
|
+
throw new Error("createBiabClient: siteId is required.");
|
|
762
|
+
}
|
|
763
|
+
const state = {
|
|
764
|
+
apiKey: opts.apiKey,
|
|
765
|
+
siteId: opts.siteId,
|
|
766
|
+
baseUrl: opts.baseUrl ?? DEFAULT_BASE_URL,
|
|
767
|
+
revalidateSeconds: opts.revalidateSeconds ?? DEFAULT_REVALIDATE_SECONDS,
|
|
768
|
+
fetch: opts.fetch ?? globalThis.fetch.bind(globalThis),
|
|
769
|
+
siteOrigin: opts.siteOrigin,
|
|
770
|
+
};
|
|
771
|
+
const transport = new BiabDevClient({
|
|
772
|
+
baseUrl: state.baseUrl,
|
|
773
|
+
apiKey: state.apiKey,
|
|
774
|
+
...(opts.fetch ? { fetch: opts.fetch } : {}),
|
|
775
|
+
...(opts.siteOrigin ? { siteOrigin: opts.siteOrigin } : {}),
|
|
776
|
+
});
|
|
777
|
+
return {
|
|
778
|
+
gallery: createGalleryResource(state),
|
|
779
|
+
scheduling: createSchedulingResource(state),
|
|
780
|
+
forms: createFormsResource(state),
|
|
781
|
+
blog: new BiabDevBlogClient(transport),
|
|
782
|
+
reviews: new BiabDevReviewsClient(transport),
|
|
783
|
+
storefront: new BiabDevStorefrontClient(transport),
|
|
784
|
+
cart: new BiabDevCartClient(transport, state.siteId),
|
|
785
|
+
checkout: new BiabDevCheckoutClient(transport, state.siteId),
|
|
786
|
+
coupons: new BiabDevCouponsClient(transport),
|
|
787
|
+
subscriptions: new BiabDevSubscriptionsClient(transport),
|
|
788
|
+
shipping: new BiabDevShippingClient(transport, state.siteId),
|
|
789
|
+
address: new BiabDevAddressClient(transport),
|
|
790
|
+
// CustomerPortal needs an org id binding; when the consumer's API
|
|
791
|
+
// key already binds an org, pass `null` here (server resolves via
|
|
792
|
+
// the key). For multi-tenant proxies, override after construction.
|
|
793
|
+
customerPortal: new BiabDevCustomerPortalClient(transport, null),
|
|
794
|
+
notifications: new BiabDevNotificationsClient(transport, state.siteId),
|
|
795
|
+
followers: new BiabDevFollowersClient(transport, state.siteId),
|
|
796
|
+
marketing: new BiabDevMarketingClient(transport, state.siteId),
|
|
797
|
+
marketingPages: new BiabDevMarketingPagesClient(transport, state.siteId),
|
|
798
|
+
parallelPages: new BiabDevParallelPagesClient(transport, state.siteId),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
//# sourceMappingURL=sdk.js.map
|