@bacano/sdk 1.1.15 → 1.1.18

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/react.js CHANGED
@@ -12,10 +12,10 @@ var WebsiteError = class extends Error {
12
12
  }
13
13
  };
14
14
  var RateLimitError = class extends WebsiteError {
15
- constructor(message, retryAfterSeconds) {
15
+ constructor(message, retryAfterSeconds2) {
16
16
  super(message, "RATE_LIMITED", {
17
17
  status: 429,
18
- retryAfterSeconds
18
+ retryAfterSeconds: retryAfterSeconds2
19
19
  });
20
20
  this.name = "RateLimitError";
21
21
  }
@@ -6445,6 +6445,171 @@ function createContentModule(graphqlUrl, getHeaders, getContext, storageUrl) {
6445
6445
  };
6446
6446
  }
6447
6447
 
6448
+ // libs/sdk/src/modules/forms.ts
6449
+ function retryAfterSeconds(response) {
6450
+ const value = Number(response.headers.get("Retry-After") ?? "1");
6451
+ return Number.isFinite(value) && value >= 0 ? Math.ceil(value) : 1;
6452
+ }
6453
+ function isFormKey(value) {
6454
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
6455
+ }
6456
+ function createFormsModule(apiUrl, websiteSlug, getContext) {
6457
+ const cache = /* @__PURE__ */ new Map();
6458
+ function getServerOrigin() {
6459
+ if (typeof window !== "undefined") return null;
6460
+ const domain = getContext().domain?.trim();
6461
+ if (!domain) return null;
6462
+ try {
6463
+ return new URL(domain.includes("://") ? domain : `https://${domain}`).origin;
6464
+ } catch {
6465
+ return null;
6466
+ }
6467
+ }
6468
+ return {
6469
+ async getDefinition(formKey, options) {
6470
+ const normalizedKey = formKey.trim();
6471
+ if (!isFormKey(normalizedKey)) {
6472
+ throw new WebsiteError(
6473
+ "formKey has an invalid format",
6474
+ "INVALID_FORM_KEY"
6475
+ );
6476
+ }
6477
+ const cached = cache.get(normalizedKey);
6478
+ if (!options?.force && cached && cached.expiresAt > Date.now()) {
6479
+ return cached.value;
6480
+ }
6481
+ const url = new URL(
6482
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/forms/definition`
6483
+ );
6484
+ url.searchParams.set("websiteSlug", websiteSlug);
6485
+ url.searchParams.set("formKey", normalizedKey);
6486
+ let response;
6487
+ try {
6488
+ const serverOrigin = getServerOrigin();
6489
+ const headers = new Headers();
6490
+ if (serverOrigin) headers.set("Origin", serverOrigin);
6491
+ if (cached?.etag) headers.set("If-None-Match", cached.etag);
6492
+ response = await fetch(url, {
6493
+ ...serverOrigin || cached?.etag ? { headers } : {},
6494
+ ...options?.force ? { cache: "no-cache" } : {}
6495
+ });
6496
+ } catch (error) {
6497
+ throw new WebsiteError(
6498
+ `Could not load the website form: ${error instanceof Error ? error.message : String(error)}`,
6499
+ "NETWORK_ERROR"
6500
+ );
6501
+ }
6502
+ if (response.status === 304 && cached) {
6503
+ cache.set(normalizedKey, {
6504
+ ...cached,
6505
+ expiresAt: Date.now() + 3e4
6506
+ });
6507
+ return cached.value;
6508
+ }
6509
+ if (response.status === 404) {
6510
+ cache.set(normalizedKey, {
6511
+ etag: null,
6512
+ expiresAt: Date.now() + 15e3,
6513
+ value: null
6514
+ });
6515
+ return null;
6516
+ }
6517
+ if (response.status === 429) {
6518
+ throw new RateLimitError(
6519
+ "Rate limit exceeded. Please try again later.",
6520
+ retryAfterSeconds(response)
6521
+ );
6522
+ }
6523
+ if (!response.ok) {
6524
+ throw new WebsiteError(
6525
+ `Could not load the website form (status ${response.status})`,
6526
+ response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "FORM_DEFINITION_ERROR",
6527
+ { status: response.status }
6528
+ );
6529
+ }
6530
+ const value = await response.json();
6531
+ if (value.schemaVersion !== 1 || value.key !== normalizedKey || !Array.isArray(value.definition?.fields) || !Array.isArray(value.definition?.sections) || !Array.isArray(value.definition?.steps)) {
6532
+ throw new WebsiteError(
6533
+ "Website form returned an invalid definition",
6534
+ "INVALID_FORM_DEFINITION"
6535
+ );
6536
+ }
6537
+ cache.set(normalizedKey, {
6538
+ etag: response.headers.get("ETag"),
6539
+ expiresAt: Date.now() + 3e4,
6540
+ value
6541
+ });
6542
+ return value;
6543
+ },
6544
+ async submitResponse(input) {
6545
+ const normalizedKey = input.formKey.trim();
6546
+ if (!isFormKey(normalizedKey)) {
6547
+ throw new WebsiteError(
6548
+ "formKey has an invalid format",
6549
+ "INVALID_FORM_KEY"
6550
+ );
6551
+ }
6552
+ const context = getContext();
6553
+ const attribution = getWebsiteMarketingAttribution(context.websiteId);
6554
+ const pageUrl = typeof window !== "undefined" ? window.location.href : void 0;
6555
+ const referrerUrl = typeof document !== "undefined" && document.referrer ? document.referrer : void 0;
6556
+ let response;
6557
+ try {
6558
+ response = await fetch(
6559
+ `${apiUrl.replace(/\/+$/, "")}/api/v1/website/forms/responses`,
6560
+ {
6561
+ method: "POST",
6562
+ headers: { "Content-Type": "application/json" },
6563
+ body: JSON.stringify({
6564
+ websiteSlug,
6565
+ formKey: normalizedKey,
6566
+ formVersion: input.formVersion,
6567
+ definitionHash: input.definitionHash,
6568
+ idempotencyKey: input.idempotencyKey,
6569
+ source: input.source ?? "WEBSITE",
6570
+ values: input.values,
6571
+ pageUrl,
6572
+ referrerUrl,
6573
+ attribution: attribution ? serializeWebsiteMarketingAttribution(attribution) : null,
6574
+ honeypot: input.honeypot ?? ""
6575
+ })
6576
+ }
6577
+ );
6578
+ } catch (error) {
6579
+ throw new WebsiteError(
6580
+ `Could not submit the website form: ${error instanceof Error ? error.message : String(error)}`,
6581
+ "NETWORK_ERROR"
6582
+ );
6583
+ }
6584
+ if (response.status === 429) {
6585
+ throw new RateLimitError(
6586
+ "Rate limit exceeded. Please try again later.",
6587
+ retryAfterSeconds(response)
6588
+ );
6589
+ }
6590
+ const result = await response.json().catch(() => null);
6591
+ if (!response.ok) {
6592
+ throw new WebsiteError(
6593
+ result?.message ?? `Could not submit the website form (status ${response.status})`,
6594
+ result?.code ?? (response.status === 403 ? "ORIGIN_NOT_ALLOWED" : "FORM_RESPONSE_ERROR"),
6595
+ { status: response.status }
6596
+ );
6597
+ }
6598
+ if (typeof result?.accepted !== "boolean" || typeof result.message !== "string") {
6599
+ throw new WebsiteError(
6600
+ "Website form returned an invalid response",
6601
+ "INVALID_RESPONSE"
6602
+ );
6603
+ }
6604
+ return {
6605
+ accepted: result.accepted,
6606
+ responseId: typeof result.responseId === "string" ? result.responseId : null,
6607
+ message: result.message
6608
+ };
6609
+ }
6610
+ };
6611
+ }
6612
+
6448
6613
  // libs/sdk/src/modules/marketing.ts
6449
6614
  function readRetryAfter(response) {
6450
6615
  const value = Number(response.headers.get("Retry-After") ?? "1");
@@ -6971,6 +7136,7 @@ function createBacanoClient(config) {
6971
7136
  config.websiteSlug,
6972
7137
  getContext
6973
7138
  );
7139
+ const formsModule = createFormsModule(apiUrl, config.websiteSlug, getContext);
6974
7140
  return {
6975
7141
  async init() {
6976
7142
  if (context) return;
@@ -7023,6 +7189,10 @@ function createBacanoClient(config) {
7023
7189
  getByTag: contentModule.getByTag.bind(contentModule),
7024
7190
  getForProduct: contentModule.getForProduct.bind(contentModule)
7025
7191
  },
7192
+ forms: {
7193
+ getDefinition: formsModule.getDefinition.bind(formsModule),
7194
+ submitResponse: formsModule.submitResponse.bind(formsModule)
7195
+ },
7026
7196
  cart: {
7027
7197
  getOrCreate: cartModule.getOrCreate.bind(cartModule),
7028
7198
  startCheckout: cartModule.startCheckout.bind(cartModule),
@@ -91,6 +91,11 @@ Figma, desde Bacano Content o desde ambas fuentes.
91
91
  los contratos específicos del SDK.
92
92
  - No usar GraphQL directo, endpoints internos ni lógica comercial duplicada.
93
93
  - No codificar textos editables si pertenecen a Bacano Content.
94
+ - Marcar obligatoriamente cada región visual proveniente de un `ContentEntry`
95
+ con `getContentEntryLocatorAttributes(entry)`. El marcador identifica la
96
+ entrada completa, nunca campos individuales. Conservar esta identidad al
97
+ transformar Content en view models y seguir
98
+ `STOREFRONT_CONTENT_LOCATOR.md`.
94
99
  - No alterar Baby Rex u otra referencia salvo que ese sea el proyecto indicado.
95
100
 
96
101
  Si Content todavía no existe, generar un blueprint de entrega y fixtures
@@ -147,6 +152,9 @@ proyecto.
147
152
  Antes de aprobar producción, comprobar que la fuente editorial efectiva sea
148
153
  Bacano Content. Un fixture, placeholder o fallback temporal accesible en el
149
154
  build de producción es un bloqueo, aunque visualmente coincida con Figma.
155
+ Inspeccionar además el HTML final: toda región alimentada por una entrada debe
156
+ conservar su marcador V1 con la key, nombre y locale correctos. Una región
157
+ editorial sin marcador bloquea la aprobación.
150
158
 
151
159
  Al finalizar, invocar `$bacano-web` si está disponible para la auditoría de
152
160
  producción completa. Esta skill valida fidelidad y correcta integración desde
@@ -7,9 +7,9 @@ negocio.
7
7
 
8
8
  Antes de programar, registrar por ruta:
9
9
 
10
- | Ruta | Frame | Componente | Fuente | Content | Responsive | Preview | Estado |
11
- | --- | --- | --- | --- | --- | --- | --- | --- |
12
- | `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | 2 frames | activa | pendiente |
10
+ | Ruta | Frame | Componente | Fuente | Content | Localizador | Responsive | Preview | Estado |
11
+ | --- | --- | --- | --- | --- | --- | --- | --- | --- |
12
+ | `/` | Home desktop/mobile | `Hero` | Content | `hero_inicio` | entrada V1 | 2 frames | activa | pendiente |
13
13
 
14
14
  Fuentes posibles: Content, catálogo estático, live state, public config,
15
15
  checkout, auth u otra operación documentada del SDK.
@@ -18,6 +18,8 @@ checkout, auth u otra operación documentada del SDK.
18
18
 
19
19
  - Usar componentes y tokens existentes antes de crear otros.
20
20
  - Mapear textos/imágenes editoriales a Content cuando deban ser administrables.
21
+ - Para cada fuente `ContentEntry`, conservar y renderizar el marcador de entrada
22
+ definido en `STOREFRONT_CONTENT_LOCATOR.md`; no bajar a nivel de campo.
21
23
  - Si el equipo de Content y desarrollo trabajan en tiempos distintos, seguir
22
24
  [content-workflow.md](content-workflow.md) y mantener un único contrato de
23
25
  datos entre fixtures temporales y Bacano real.
@@ -35,6 +35,10 @@ reduzca duplicación real.
35
35
  consumidores.
36
36
  - No hacer depender la UI del shape crudo de Content: fixture y mapper real
37
37
  deben converger en el mismo view model.
38
+ - Conservar en ese view model los atributos producidos por
39
+ `getContentEntryLocatorAttributes(entry)`. Toda región visual administrada
40
+ por una entrada debe exponerlos en su contenedor DOM exterior; no se marcan
41
+ campos individuales.
38
42
 
39
43
  ## 2. Elegir el punto de partida
40
44
 
@@ -138,6 +142,10 @@ Bacano mapper ─┘
138
142
  El cambio de fuente debe ocurrir en el loader o repositorio de contenido, nunca
139
143
  mediante condicionales repartidos por los componentes.
140
144
 
145
+ Los fixtures pueden simular el view model, pero no deben inventar marcadores de
146
+ entrada en producción. El marcador definitivo se deriva del `ContentEntry`
147
+ real para que nombre, key y locale no diverjan.
148
+
141
149
  ## 5. Entrega al equipo de Content
142
150
 
143
151
  Entregar una tabla legible con:
@@ -167,7 +175,9 @@ Antes de retirar el fixture:
167
175
  2. Validar campos requeridos, media y contenido largo.
168
176
  3. Comparar la página con Figma usando Content real.
169
177
  4. Verificar preview si la capacidad está activa.
170
- 5. Eliminar el fixture del camino de ejecución, no necesariamente de los tests.
178
+ 5. Inspeccionar el HTML y confirmar los marcadores definidos en
179
+ `STOREFRONT_CONTENT_LOCATOR.md` para cada región editorial.
180
+ 6. Eliminar el fixture del camino de ejecución, no necesariamente de los tests.
171
181
 
172
182
  En producción no se permite fallback silencioso a fixtures. Si una página
173
183
  depende de contenido temporal, reportar `❌ NO LISTO` con las rutas y secciones
@@ -58,6 +58,18 @@ no aceptar una ruta arbitraria solo porque coincide con una cadena. Preview
58
58
  reutiliza componentes y mappers de producción, permanece fuera del sitemap y
59
59
  no habilita funciones transaccionales o marketing.
60
60
 
61
+ ## Trazabilidad editorial
62
+
63
+ Al mapear un `ContentEntry`, conservar los atributos retornados por
64
+ `getContentEntryLocatorAttributes(entry)` hasta el componente visual. Aplicar
65
+ el spread al `section`, `article`, banner o tarjeta exterior que representa la
66
+ entrada. No repetirlo en cada título o párrafo. Si una tarjeta proviene de otra
67
+ entrada referenciada, esa tarjeta lleva su propio marcador anidado.
68
+
69
+ La salida final debe cumplir `STOREFRONT_CONTENT_LOCATOR.md`. Verificar el HTML
70
+ generado; encontrar la llamada al helper en TypeScript no demuestra que el
71
+ marcador haya llegado al DOM.
72
+
61
73
  ## Diagnóstico antes de cambiar código
62
74
 
63
75
  Un `ORIGIN_NOT_ALLOWED` o un `403` al resolver la configuración durante el
@@ -47,10 +47,14 @@ Después leer solo:
47
47
  Si `@bacano/sdk` está disponible, leer sus contratos canónicos aplicables antes
48
48
  de proponer cambios. Como mínimo revisar `STOREFRONT_LEGAL.md`,
49
49
  `STOREFRONT_MARKETING.md`, `STOREFRONT_PERFORMANCE.md` y
50
- `STOREFRONT_PREVIEW.md`; añadir
50
+ `STOREFRONT_PREVIEW.md`; si el sitio renderiza Bacano Content, leer también y
51
+ aplicar obligatoriamente `STOREFRONT_CONTENT_LOCATOR.md`; añadir
51
52
  `AUTHENTICATION.md`, `STOREFRONT_LEADS.md`, `STOREFRONT_CLARITY.md` y
52
53
  otros contratos únicamente cuando esas capacidades existan. Leer
53
54
  `STOREFRONT_CONSTRUCTION.md` cuando el website permita ocultarse temporalmente.
55
+ Si existen formularios administrados desde Bacano, leer obligatoriamente
56
+ `STOREFRONT_FORMS.md` y
57
+ [references/forms.md](references/forms.md) antes de auditarlos o modificarlos.
54
58
 
55
59
  ## 3. Auditar sin alterar el sistema
56
60
 
@@ -67,6 +71,8 @@ ejecutar compras/pagos sin autorización explícita.
67
71
  5. Comparar router, salida del build y sitemap; ninguna ruta pública debe
68
72
  omitirse silenciosamente.
69
73
  6. Inspeccionar HTML generado, metadata, JSON-LD, media y enlaces internos.
74
+ Si existe Content, comparar las entradas renderizadas con los marcadores V1
75
+ presentes en ese HTML; revisar únicamente el JSX no es evidencia suficiente.
70
76
  7. Si existe un dominio o una aplicación ejecutable, auditar sus rutas reales,
71
77
  la consola y un trace de red con nombres y conteos de operaciones GraphQL.
72
78
  8. Probar vistas representativas desktop y mobile.
@@ -29,6 +29,13 @@ función apagada en un bloqueo artificial.
29
29
  - Comprobar que preview, callbacks, cuenta, checkout y respuestas de pago no se
30
30
  indexen cuando corresponda.
31
31
  - Validar estados vacío, loading, error y ausencia de Content opcional.
32
+ - `P0`: toda región visual cuya fuente sea un `ContentEntry` expone en el HTML
33
+ final el marcador de `STOREFRONT_CONTENT_LOCATOR.md`, con la key, nombre y
34
+ locale de la entrada fuente. El control es por entrada completa, no por campo.
35
+ - Comparar inventario de entradas y regiones. Permitir marcadores repetidos si
36
+ una entrada aparece en varios lugares y marcadores anidados cuando una tarjeta
37
+ proviene de otra entrada referenciada. No exigirlos a código/i18n, catálogo,
38
+ `publicConfig` ni documentos legales.
32
39
 
33
40
  ## SEO y compartición
34
41
 
@@ -0,0 +1,67 @@
1
+ # Formularios públicos de Bacano
2
+
3
+ Aplicar este contrato cuando el sitio muestre al menos un formulario administrado
4
+ desde Bacano. Leer también `STOREFRONT_FORMS.md` completo desde el paquete SDK.
5
+
6
+ ## Implementación oficial
7
+
8
+ - Clasificar formularios como `ACTIVA`, `INACTIVA` o `BLOQUEADA` en la matriz de
9
+ capacidades.
10
+ - Usar `BacanoForm` desde `@bacano/sdk/react/forms` o un renderer propio basado
11
+ exclusivamente en `useBacanoForm()`.
12
+ - Reutilizar el único `BacanoProvider` y cliente global del sitio. No crear otro
13
+ cliente, consultar GraphQL ni llamar endpoints internos.
14
+ - No duplicar keys, validaciones, opciones o estructura en un esquema local.
15
+ - En Static First, obtener la definición durante build/servidor y entregarla
16
+ como `initialForm`. La revalidación runtime no debe borrar el HTML inicial.
17
+ - Renderizar campos directos, secciones visuales y pasos según su semántica. Una
18
+ sección no es un paso y no debe inventar navegación.
19
+ - Las listas de Content llegan resueltas por Bacano; el storefront no reconstruye
20
+ opciones ni consulta Content por separado.
21
+
22
+ ## Envío y seguridad
23
+
24
+ - Enviar mediante `client.forms.submitResponse()` o el envío interno de
25
+ `BacanoForm`.
26
+ - `honeypot` es un campo trampa, no un nonce, timestamp, UUID ni dato del
27
+ visitante. `BacanoForm` lo administra. En un renderer manual, omitirlo salvo
28
+ que exista un input trampa realmente oculto; para una persona siempre debe
29
+ permanecer vacío y el autocompletado no debe llenarlo.
30
+ - `accepted: true` con `responseId: null` es una aceptación silenciosa antispam:
31
+ no demuestra que la respuesta fue guardada. La verificación funcional exige
32
+ un UUID y la respuesta visible una sola vez en Bacano.
33
+ - Los datos de contacto deben conservar su semántica: usar campos `email` y
34
+ `phone`. Un campo `text` llamado correo o teléfono se guarda como texto libre
35
+ y no alimenta los datos de contacto normalizados para CRM o automatizaciones.
36
+ - Conservar una `idempotencyKey` durante reintentos del mismo intento lógico.
37
+ - Enviar la versión y `definitionHash` exactos de la definición renderizada.
38
+ - Respetar estados `ACTIVE` y `PAUSED`; no renderizar borradores o archivados.
39
+ - No escribir respuestas o PII en consola, analítica, URLs o logs del storefront.
40
+ - En `CAPTURE_LEAD`, Bacano conserva primero la respuesta y resuelve la identidad
41
+ internamente. No afirmar al visitante que se creó un lead o que ya era cliente:
42
+ la respuesta pública es genérica y no expone ese resultado.
43
+ - No afirmar que se envió un correo, se entregó un cupón o se abrió una solicitud
44
+ si esa acción aún no está implementada y verificada en Bacano.
45
+ - Una brecha del SDK se reporta; nunca se resuelve con GraphQL directo ni una API
46
+ paralela del storefront.
47
+
48
+ ## Verificaciones obligatorias
49
+
50
+ - Formulario activo: estructura, labels, required, ayudas, opciones y envío.
51
+ - Formulario pausado e inexistente: mensaje seguro y ningún envío.
52
+ - Versión desactualizada: refresh oficial y conservación controlada de datos.
53
+ - Doble clic y reintento: una sola respuesta por intento idempotente.
54
+ - Error de red: estado recuperable sin promesa falsa de recepción.
55
+ - Persistencia: la respuesta aparece una sola vez en Bacano con versión, origen y
56
+ snapshot correctos.
57
+ - Autocompletado: completar datos con el navegador o un gestor de contraseñas no
58
+ llena el honeypot ni produce `responseId: null`.
59
+ - Semántica: correo y teléfono usan `email` y `phone`; no se infieren por label o
60
+ key.
61
+ - Accesibilidad: label asociado, foco, teclado, mensajes de error y progreso de
62
+ pasos comprensibles.
63
+ - Static First: el HTML exportado conserva el formulario cuando corresponde.
64
+
65
+ Reportar evidencia con la key pública, ruta, archivo del renderer, requests
66
+ observados y prueba de persistencia. Nunca incluir valores personales reales en
67
+ el informe.
@@ -12,6 +12,9 @@ Aplicar además de los controles comunes.
12
12
 
13
13
  ## Formularios y contacto
14
14
 
15
+ - Los formularios administrados desde Bacano cumplen
16
+ [forms.md](forms.md), usan el renderer o hook oficial y conservan su definición
17
+ estática cuando corresponde.
15
18
  - Distinguir formularios informativos/PQRS de capturas comerciales.
16
19
  - Newsletter, cupón o interés comercial usan
17
20
  `client.marketing.captureLead()` y el texto vigente de autorización.
package/types.d.ts CHANGED
@@ -149,6 +149,93 @@ export interface WebsiteCampaignCaptureResult {
149
149
  accepted: boolean;
150
150
  message: string;
151
151
  }
152
+ export type WebsiteFormAction = "STORE_RESPONSE" | "CAPTURE_LEAD";
153
+ export type WebsiteFormStatus = "ACTIVE" | "PAUSED";
154
+ export type WebsiteFormFieldType = "text" | "textarea" | "email" | "phone" | "number" | "date" | "select" | "radio" | "multi_select" | "checkbox";
155
+ export interface WebsiteFormOption {
156
+ value: string;
157
+ label: string;
158
+ }
159
+ export type WebsiteFormOptionSource = {
160
+ kind: "INLINE";
161
+ options: WebsiteFormOption[];
162
+ } | {
163
+ kind: "CONTENT_LIST";
164
+ listKey: string;
165
+ };
166
+ export interface WebsiteFormField {
167
+ key: string;
168
+ label: string;
169
+ type: WebsiteFormFieldType;
170
+ required: boolean;
171
+ placeholder?: string | null;
172
+ helpText?: string | null;
173
+ optionSource?: WebsiteFormOptionSource | null;
174
+ /** Resolved public options. Content List internals never need rebuilding by a storefront. */
175
+ options?: WebsiteFormOption[];
176
+ validation: {
177
+ min?: number | null;
178
+ max?: number | null;
179
+ minLength?: number | null;
180
+ maxLength?: number | null;
181
+ };
182
+ }
183
+ export interface WebsiteFormSection {
184
+ key: string;
185
+ title: string;
186
+ description?: string | null;
187
+ fields: WebsiteFormField[];
188
+ }
189
+ export interface WebsiteFormStep {
190
+ key: string;
191
+ title: string;
192
+ description?: string | null;
193
+ fields: WebsiteFormField[];
194
+ sections: WebsiteFormSection[];
195
+ }
196
+ export interface WebsiteFormDefinition {
197
+ schemaVersion: 1;
198
+ submitLabel: string;
199
+ successMessage: string;
200
+ unavailableMessage: string;
201
+ fields: WebsiteFormField[];
202
+ sections: WebsiteFormSection[];
203
+ steps: WebsiteFormStep[];
204
+ }
205
+ export interface WebsitePublishedForm {
206
+ schemaVersion: 1;
207
+ id: string;
208
+ key: string;
209
+ name: string;
210
+ description: string;
211
+ action: WebsiteFormAction;
212
+ status: WebsiteFormStatus;
213
+ version: number;
214
+ publishedAt: string;
215
+ definitionHash: string;
216
+ definition: WebsiteFormDefinition;
217
+ }
218
+ export type WebsiteFormResponseSource = "WEBSITE" | "QR";
219
+ export type WebsiteFormResponseValue = string | string[] | boolean;
220
+ export interface WebsiteFormResponseInput {
221
+ formKey: string;
222
+ formVersion: number;
223
+ definitionHash: string;
224
+ idempotencyKey: string;
225
+ values: Record<string, WebsiteFormResponseValue>;
226
+ source?: WebsiteFormResponseSource;
227
+ /**
228
+ * Bot-trap field, not a token or nonce. Omit it unless a real hidden trap is
229
+ * rendered; legitimate visitors must always submit an empty value.
230
+ */
231
+ honeypot?: string;
232
+ }
233
+ export interface WebsiteFormResponseResult {
234
+ accepted: boolean;
235
+ /** A null value is not proof of persistence (for example, an anti-bot response). */
236
+ responseId: string | null;
237
+ message: string;
238
+ }
152
239
  export type ResolvedWebsitePublicConfig = WebsitePublicConfig & {
153
240
  branding: WebsiteBrandingPublicConfig;
154
241
  legal: WebsiteLegalPublicConfig;