strapi-plugin-hubspot 0.16.5 → 0.16.7

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/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.16.7 — 2026-09-23
6
+
7
+ ### Fixed
8
+ - Admin TypeScript build of 0.16.6 (test fixture `objectTypeId`). Same
9
+ legitimate-interest / company-field conversion fix.
10
+
11
+ ## 0.16.6 — 2026-09-23
12
+
13
+ ### Fixed
14
+ - Native conversions against HubSpot forms that use **legitimate interest**
15
+ (not explicit consent) and **company fields** (`objectTypeId` `0-2`) were
16
+ refused by the Forms API, so only a CRM note appeared. The submit now sends
17
+ `legalConsentOptions.legitimateInterest` and the company properties the
18
+ marketing form actually declares.
19
+
5
20
  ## 0.16.5 — 2026-09-23
6
21
 
7
22
  ### Fixed
@@ -1923,10 +1923,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
1923
1923
  function toHsFields(props) {
1924
1924
  return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
1925
1925
  }
1926
- function filterToFormFields(fields, allowed) {
1927
- if (!allowed) return fields;
1928
- return fields.filter((field) => allowed.has(field.name));
1929
- }
1930
1926
  function formFieldNames(raw) {
1931
1927
  const names = [];
1932
1928
  for (const group of raw.fieldGroups ?? []) {
@@ -1936,20 +1932,42 @@ function formFieldNames(raw) {
1936
1932
  }
1937
1933
  return names;
1938
1934
  }
1939
- function formRequiredNames(raw) {
1940
- const names = [];
1935
+ const OBJECT_BAG = {
1936
+ "0-1": "contact",
1937
+ "0-2": "company"
1938
+ };
1939
+ function formShapeFields(raw) {
1940
+ const fields = [];
1941
1941
  for (const group of raw.fieldGroups ?? []) {
1942
1942
  for (const field of group.fields ?? []) {
1943
- if (field.name && field.required) names.push(field.name);
1943
+ if (!field.name) continue;
1944
+ fields.push({
1945
+ name: field.name,
1946
+ objectTypeId: field.objectTypeId?.trim() || "0-1",
1947
+ required: Boolean(field.required)
1948
+ });
1944
1949
  }
1945
1950
  }
1946
- return names;
1951
+ return fields;
1952
+ }
1953
+ function fieldsForHubspotForm(bags, shape, extra = {}) {
1954
+ const contact = { ...bags.contact ?? {}, ...extra };
1955
+ if (!shape?.fields.length) {
1956
+ return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
1957
+ }
1958
+ const out = [];
1959
+ for (const field of shape.fields) {
1960
+ const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
1961
+ const value = bags[bagName]?.[field.name] ?? contact[field.name];
1962
+ if (!value) continue;
1963
+ out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
1964
+ }
1965
+ return out;
1947
1966
  }
1948
1967
  function parseFormShape(raw) {
1949
1968
  const legal = raw.legalConsentOptions;
1950
- const hasLegalConsent = Boolean(
1951
- legal && typeof legal === "object" && legal.type && legal.type !== "none"
1952
- );
1969
+ const legalType = typeof legal?.type === "string" ? legal.type : void 0;
1970
+ const hasLegalConsent = Boolean(legalType && legalType !== "none");
1953
1971
  const communications = [];
1954
1972
  for (const box of legal?.communicationsCheckboxes ?? []) {
1955
1973
  const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
@@ -1959,12 +1977,21 @@ function parseFormShape(raw) {
1959
1977
  text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1960
1978
  });
1961
1979
  }
1980
+ const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
1962
1981
  const consentToProcessText = legal?.consentToProcessText?.trim();
1982
+ const privacyText = legal?.privacyText?.trim();
1983
+ const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
1984
+ const fields = formShapeFields(raw);
1963
1985
  return {
1964
- names: new Set(formFieldNames(raw)),
1965
- required: new Set(formRequiredNames(raw)),
1986
+ names: new Set(fields.map((field) => field.name)),
1987
+ required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
1988
+ fields,
1966
1989
  hasLegalConsent,
1967
- captcha: raw.configuration?.captchaEnabled === true,
1990
+ ...legalType ? { legalType } : {},
1991
+ lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
1992
+ subscriptionTypeIds,
1993
+ ...privacyText ? { privacyText } : {},
1994
+ captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
1968
1995
  ...consentToProcessText ? { consentToProcessText } : {},
1969
1996
  communications
1970
1997
  };
@@ -1976,9 +2003,22 @@ function canRetryEmailOnly(required) {
1976
2003
  function buildLegalConsent(shape, given, fallbackText) {
1977
2004
  if (!given) return void 0;
1978
2005
  if (shape && !shape.hasLegalConsent) return void 0;
2006
+ const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
2007
+ if (shape?.legalType === "legitimate_interest") {
2008
+ const subscriptionTypeId = shape.subscriptionTypeIds[0];
2009
+ if (!subscriptionTypeId) return void 0;
2010
+ return {
2011
+ legitimateInterest: {
2012
+ value: true,
2013
+ subscriptionTypeId,
2014
+ legalBasis: shape.lawfulBasis || "LEAD",
2015
+ text: shape.privacyText?.trim() || fallback
2016
+ }
2017
+ };
2018
+ }
1979
2019
  const consent = {
1980
2020
  consentToProcess: true,
1981
- text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
2021
+ text: shape?.consentToProcessText?.trim() || fallback
1982
2022
  };
1983
2023
  if (shape?.communications.length) {
1984
2024
  consent.communications = shape.communications.map((item) => ({
@@ -2500,12 +2540,16 @@ function createFormsService(strapi, _opts = {}) {
2500
2540
  if (tryFormsApi && portalId && formGuid) {
2501
2541
  try {
2502
2542
  const shape = await loadFormShape(apiKey, formGuid);
2503
- const fields = filterToFormFields(
2504
- toHsFields({ ...contactProps, email }),
2505
- shape?.names ?? null
2543
+ const fields = fieldsForHubspotForm(
2544
+ {
2545
+ contact: contactProps,
2546
+ company: stringProps(partition.accepted.company ?? {})
2547
+ },
2548
+ shape,
2549
+ { email }
2506
2550
  );
2507
2551
  if (fields.length && !fields.some((f) => f.name === "email")) {
2508
- fields.unshift({ name: "email", value: email });
2552
+ fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
2509
2553
  }
2510
2554
  sentFieldNames = fields.map((f) => f.name);
2511
2555
  const submitted = await submitMarketingForm({
@@ -2513,7 +2557,7 @@ function createFormsService(strapi, _opts = {}) {
2513
2557
  portalId,
2514
2558
  formGuid,
2515
2559
  region,
2516
- fields: fields.length ? fields : [{ name: "email", value: email }],
2560
+ fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
2517
2561
  context: {
2518
2562
  hutk: sanitizeHutk(meta.hutk),
2519
2563
  pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
@@ -1922,10 +1922,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
1922
1922
  function toHsFields(props) {
1923
1923
  return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
1924
1924
  }
1925
- function filterToFormFields(fields, allowed) {
1926
- if (!allowed) return fields;
1927
- return fields.filter((field) => allowed.has(field.name));
1928
- }
1929
1925
  function formFieldNames(raw) {
1930
1926
  const names = [];
1931
1927
  for (const group of raw.fieldGroups ?? []) {
@@ -1935,20 +1931,42 @@ function formFieldNames(raw) {
1935
1931
  }
1936
1932
  return names;
1937
1933
  }
1938
- function formRequiredNames(raw) {
1939
- const names = [];
1934
+ const OBJECT_BAG = {
1935
+ "0-1": "contact",
1936
+ "0-2": "company"
1937
+ };
1938
+ function formShapeFields(raw) {
1939
+ const fields = [];
1940
1940
  for (const group of raw.fieldGroups ?? []) {
1941
1941
  for (const field of group.fields ?? []) {
1942
- if (field.name && field.required) names.push(field.name);
1942
+ if (!field.name) continue;
1943
+ fields.push({
1944
+ name: field.name,
1945
+ objectTypeId: field.objectTypeId?.trim() || "0-1",
1946
+ required: Boolean(field.required)
1947
+ });
1943
1948
  }
1944
1949
  }
1945
- return names;
1950
+ return fields;
1951
+ }
1952
+ function fieldsForHubspotForm(bags, shape, extra = {}) {
1953
+ const contact = { ...bags.contact ?? {}, ...extra };
1954
+ if (!shape?.fields.length) {
1955
+ return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
1956
+ }
1957
+ const out = [];
1958
+ for (const field of shape.fields) {
1959
+ const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
1960
+ const value = bags[bagName]?.[field.name] ?? contact[field.name];
1961
+ if (!value) continue;
1962
+ out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
1963
+ }
1964
+ return out;
1946
1965
  }
1947
1966
  function parseFormShape(raw) {
1948
1967
  const legal = raw.legalConsentOptions;
1949
- const hasLegalConsent = Boolean(
1950
- legal && typeof legal === "object" && legal.type && legal.type !== "none"
1951
- );
1968
+ const legalType = typeof legal?.type === "string" ? legal.type : void 0;
1969
+ const hasLegalConsent = Boolean(legalType && legalType !== "none");
1952
1970
  const communications = [];
1953
1971
  for (const box of legal?.communicationsCheckboxes ?? []) {
1954
1972
  const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
@@ -1958,12 +1976,21 @@ function parseFormShape(raw) {
1958
1976
  text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1959
1977
  });
1960
1978
  }
1979
+ const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
1961
1980
  const consentToProcessText = legal?.consentToProcessText?.trim();
1981
+ const privacyText = legal?.privacyText?.trim();
1982
+ const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
1983
+ const fields = formShapeFields(raw);
1962
1984
  return {
1963
- names: new Set(formFieldNames(raw)),
1964
- required: new Set(formRequiredNames(raw)),
1985
+ names: new Set(fields.map((field) => field.name)),
1986
+ required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
1987
+ fields,
1965
1988
  hasLegalConsent,
1966
- captcha: raw.configuration?.captchaEnabled === true,
1989
+ ...legalType ? { legalType } : {},
1990
+ lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
1991
+ subscriptionTypeIds,
1992
+ ...privacyText ? { privacyText } : {},
1993
+ captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
1967
1994
  ...consentToProcessText ? { consentToProcessText } : {},
1968
1995
  communications
1969
1996
  };
@@ -1975,9 +2002,22 @@ function canRetryEmailOnly(required) {
1975
2002
  function buildLegalConsent(shape, given, fallbackText) {
1976
2003
  if (!given) return void 0;
1977
2004
  if (shape && !shape.hasLegalConsent) return void 0;
2005
+ const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
2006
+ if (shape?.legalType === "legitimate_interest") {
2007
+ const subscriptionTypeId = shape.subscriptionTypeIds[0];
2008
+ if (!subscriptionTypeId) return void 0;
2009
+ return {
2010
+ legitimateInterest: {
2011
+ value: true,
2012
+ subscriptionTypeId,
2013
+ legalBasis: shape.lawfulBasis || "LEAD",
2014
+ text: shape.privacyText?.trim() || fallback
2015
+ }
2016
+ };
2017
+ }
1978
2018
  const consent = {
1979
2019
  consentToProcess: true,
1980
- text: shape?.consentToProcessText?.trim() || fallbackText?.trim() || DEFAULT_CONSENT_TEXT
2020
+ text: shape?.consentToProcessText?.trim() || fallback
1981
2021
  };
1982
2022
  if (shape?.communications.length) {
1983
2023
  consent.communications = shape.communications.map((item) => ({
@@ -2499,12 +2539,16 @@ function createFormsService(strapi, _opts = {}) {
2499
2539
  if (tryFormsApi && portalId && formGuid) {
2500
2540
  try {
2501
2541
  const shape = await loadFormShape(apiKey, formGuid);
2502
- const fields = filterToFormFields(
2503
- toHsFields({ ...contactProps, email }),
2504
- shape?.names ?? null
2542
+ const fields = fieldsForHubspotForm(
2543
+ {
2544
+ contact: contactProps,
2545
+ company: stringProps(partition.accepted.company ?? {})
2546
+ },
2547
+ shape,
2548
+ { email }
2505
2549
  );
2506
2550
  if (fields.length && !fields.some((f) => f.name === "email")) {
2507
- fields.unshift({ name: "email", value: email });
2551
+ fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
2508
2552
  }
2509
2553
  sentFieldNames = fields.map((f) => f.name);
2510
2554
  const submitted = await submitMarketingForm({
@@ -2512,7 +2556,7 @@ function createFormsService(strapi, _opts = {}) {
2512
2556
  portalId,
2513
2557
  formGuid,
2514
2558
  region,
2515
- fields: fields.length ? fields : [{ name: "email", value: email }],
2559
+ fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
2516
2560
  context: {
2517
2561
  hutk: sanitizeHutk(meta.hutk),
2518
2562
  pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
@@ -23,13 +23,9 @@ export declare function toHsFields(props: Record<string, string>): {
23
23
  name: string;
24
24
  value: string;
25
25
  }[];
26
- export declare function filterToFormFields(fields: {
26
+ export declare function filterToFormFields<T extends {
27
27
  name: string;
28
- value: string;
29
- }[], allowed: Set<string> | null): {
30
- name: string;
31
- value: string;
32
- }[];
28
+ }>(fields: T[], allowed: Set<string> | null): T[];
33
29
  export declare function formFieldNames(raw: {
34
30
  fieldGroups?: {
35
31
  fields?: {
@@ -50,26 +46,56 @@ export interface CommunicationConsent {
50
46
  subscriptionTypeId: number;
51
47
  text: string;
52
48
  }
49
+ export interface FormShapeField {
50
+ name: string;
51
+ objectTypeId: string;
52
+ required: boolean;
53
+ }
53
54
  export interface FormShape {
54
55
  names: Set<string>;
55
56
  required: Set<string>;
57
+ fields: FormShapeField[];
56
58
  hasLegalConsent: boolean;
59
+ legalType?: string;
60
+ lawfulBasis?: "LEAD" | "CUSTOMER";
61
+ subscriptionTypeIds: number[];
62
+ privacyText?: string;
57
63
  captcha: boolean;
58
64
  consentToProcessText?: string;
59
65
  communications: CommunicationConsent[];
60
66
  }
67
+ export declare function formShapeFields(raw: {
68
+ fieldGroups?: {
69
+ fields?: {
70
+ name?: string;
71
+ required?: boolean;
72
+ objectTypeId?: string;
73
+ }[];
74
+ }[];
75
+ }): FormShapeField[];
76
+ /** Values HubSpot declared on the marketing form, with the right objectTypeId. */
77
+ export declare function fieldsForHubspotForm(bags: Record<string, Record<string, string>>, shape: FormShape | null | undefined, extra?: Record<string, string>): {
78
+ name: string;
79
+ value: string;
80
+ objectTypeId: string;
81
+ }[];
61
82
  export declare function parseFormShape(raw: {
62
83
  fieldGroups?: {
63
84
  fields?: {
64
85
  name?: string;
65
86
  required?: boolean;
87
+ objectTypeId?: string;
66
88
  }[];
67
89
  }[];
68
90
  configuration?: {
69
91
  captchaEnabled?: boolean;
92
+ recaptchaEnabled?: boolean;
70
93
  };
71
94
  legalConsentOptions?: {
72
95
  type?: string;
96
+ lawfulBasis?: string;
97
+ privacyText?: string;
98
+ subscriptionTypeIds?: (number | string)[];
73
99
  consentToProcessText?: string;
74
100
  communicationsCheckboxes?: {
75
101
  label?: string;
@@ -81,7 +107,7 @@ export declare function parseFormShape(raw: {
81
107
  }): FormShape;
82
108
  /** Email-only retry is only safe when HubSpot doesn't require other fields. */
83
109
  export declare function canRetryEmailOnly(required: Iterable<string> | undefined): boolean;
84
- export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
110
+ export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "legalType" | "lawfulBasis" | "subscriptionTypeIds" | "privacyText" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
85
111
  export interface FormSubmitContext {
86
112
  hutk?: string;
87
113
  pageUri?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-hubspot",
3
- "version": "0.16.5",
3
+ "version": "0.16.7",
4
4
  "description": "The form builder for HubSpot in Strapi — multi-step forms with conditional fields mapped to real CRM properties, a public submit pipeline with retries, and save-time mapping validation.",
5
5
  "keywords": [
6
6
  "strapi",