@particle-academy/hubspot-js 0.3.4 → 0.5.0

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
@@ -7,6 +7,67 @@ The four packages share one version, because they are generated from one
7
7
  `provider/` definition and a version that meant something different in each
8
8
  would be a version nobody could reason about.
9
9
 
10
+ ## [0.5.0] — 2026-11-01
11
+
12
+ ### Added
13
+
14
+ - **`crm_event`: HubSpot's CRM webhooks, verified.** HubSpot pushes a BATCH of
15
+ events (deal, contact and company created, property changed, deleted,
16
+ merged, restored, re-associated; which ones is set in the HubSpot app) to
17
+ the app's target URL. Each delivery is verified with **signature v3**
18
+ before anything reads it: base64 HMAC-SHA256 of method + URI + body +
19
+ timestamp under the app's `clientSecret`. The timestamp
20
+ (`X-HubSpot-Request-Timestamp`) is in MILLISECONDS, with a 5-minute window,
21
+ and the URI has only its query percent-decoded. HubSpot's own published
22
+ example verifies in TypeScript, PHP and Python; a changed byte, another
23
+ method, a late delivery and a missing request line are each refused.
24
+ - **The host must pass the request line.** `verifyHubspotDelivery({raw,
25
+ headers, method, url})` (js), `CrmEvent::verifyDelivery($raw, $headers,
26
+ $secret, method: …, url: …)` (php) and `verify_delivery(raw, headers,
27
+ secret, method=…, url=…)` (python) take the URL exactly as it arrived.
28
+ Without them a delivery is refused by name, never verified with a gap.
29
+ - **Routing is DECLARED** (`route: {by: "portalId", path: "[].portalId"}`,
30
+ on the descriptor, as `ROUTE`, and in the registry index). One URL serves
31
+ every portal that installed the app, so route each event by `portalId`
32
+ ONLY after the signature verifies, and only to a connection minted when
33
+ that portal installed. Drop an unmapped portal with a 2xx and a log line,
34
+ and create nothing.
35
+
36
+ ### Changed
37
+
38
+ - **Requires `fancy-connector-core` ≥ 0.12.0**, which verifies a signed
39
+ request line and millisecond timestamps.
40
+
41
+ ## [0.4.0] — 2026-11-01
42
+
43
+ ### Added
44
+
45
+ - **The CRM core, read and write: 17 new actions.** For deals, contacts and
46
+ companies:
47
+ - `*_search`: property `filterGroups` and `sorts` (nested JSON), a text
48
+ `query`, the properties to return, and HubSpot's `after` cursor;
49
+ - `*_list`: page through every record, with `after` in and
50
+ `paging.next.after` out;
51
+ - `*_get`: one record, with `associations` when asked;
52
+ - `deal_create` and `company_create`;
53
+ - `*_update`: a PATCH of the given properties, declared `idempotent`.
54
+
55
+ `contact_create` is unchanged, because saved workflows depend on its
56
+ fields. Alongside them: `owner_list` (owner ids are NOT user ids; both are
57
+ published), `pipeline_list` (every deal pipeline with its stages in order)
58
+ and `property_list` (the property definitions of deals, contacts or
59
+ companies). Record ids are numeric STRINGS in every runtime.
60
+
61
+ ### Changed
62
+
63
+ - **Scopes:** the CRM core's read and write scopes for the three objects,
64
+ plus `crm.objects.owners.read` and `crm.schemas.*.read`. An installation made
65
+ under 0.3.x granted only `crm.objects.contacts.write`, so it **must be
66
+ re-authorised** before any new action works.
67
+ - **Drift now checks every action against HubSpot's own v3 documents:**
68
+ Contacts, Companies, Deals, Owners, Pipelines and Properties. That needs
69
+ the six documents, because HubSpot publishes the CRM one area per document.
70
+
10
71
  ## [0.3.4] — 2026-09-12
11
72
 
12
73
  ### Changed
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # HubSpot
2
2
 
3
+ [![Fancified](art/fancified.svg)](https://particle.academy)
4
+
3
5
  HubSpot for [fancy-flow][flow] — as **four imported, versioned packages**, one
4
6
  per runtime. Not vendored source: a copy cannot be upgraded, and third-party APIs
5
7
  change.
@@ -50,7 +52,7 @@ HubSpot uses OAuth2 (authorization_code). The package DECLARES the exchange; the
50
52
 
51
53
  - **Authorize URL** — https://app.hubspot.com/oauth/authorize
52
54
  - **Token URL** — https://api.hubapi.com/oauth/v1/token
53
- - **Scopes** — `crm.objects.contacts.write`
55
+ - **Scopes** — `crm.objects.contacts.read`, `crm.objects.contacts.write`, `crm.objects.companies.read`, `crm.objects.companies.write`, `crm.objects.deals.read`, `crm.objects.deals.write`, `crm.objects.owners.read`, `crm.schemas.contacts.read`, `crm.schemas.companies.read`, `crm.schemas.deals.read`
54
56
  - **Access token lifetime** — 1800 seconds (30 minutes). A host that never refreshes works all afternoon and is broken by morning.
55
57
 
56
58
  The refresh tokens do **not** rotate: the same one is reusable, so a refresh may safely be retried and may run concurrently. Stated rather than assumed, because the opposite — a provider that spends the token and revokes the grant on a replay — looks identical until it happens.
@@ -65,6 +67,67 @@ HubSpot has a test estate on the same host, reached with credentials from a SEPA
65
67
 
66
68
  ### Actions
67
69
 
70
+ #### `company_create` — Create company
71
+
72
+ Create a company in HubSpot.
73
+
74
+ `POST /crm/v3/objects/companies` · **unsafe to replay** — a retried durable run does it TWICE
75
+
76
+ | Input | Required | What it is |
77
+ |---|---|---|
78
+ | `properties` | yes | HubSpot property name → value for the new company. Values are sent as strings, which is how HubSpot stores every property. |
79
+
80
+ #### `company_get` — Get company
81
+
82
+ Read one company, with its associations when asked.
83
+
84
+ `GET /crm/v3/objects/companies/{companyId}` · reads only — safe to replay
85
+
86
+ | Input | Required | What it is |
87
+ |---|---|---|
88
+ | `companyId` | yes | The company's HubSpot id (a numeric string). |
89
+ | `properties` | no | Comma-separated property names to return. HubSpot returns a small default set otherwise. |
90
+ | `associations` | no | Comma-separated object types whose associated ids to return. |
91
+
92
+ #### `company_list` — List companies
93
+
94
+ Page through every company in the portal.
95
+
96
+ `GET /crm/v3/objects/companies` · reads only — safe to replay
97
+
98
+ | Input | Required | What it is |
99
+ |---|---|---|
100
+ | `limit` | no | How many records per page, 1 to 100. HubSpot's own default is 10. |
101
+ | `after` | no | The cursor from the previous page's paging.next.after. Leave empty for the first page. |
102
+ | `properties` | no | Comma-separated property names to return. HubSpot returns a small default set otherwise. |
103
+ | `associations` | no | Comma-separated object types whose associated ids to return. |
104
+
105
+ #### `company_search` — Search companies
106
+
107
+ Find companies by property filters, sorts or a text query.
108
+
109
+ `POST /crm/v3/objects/companies/search` · reads only — safe to replay
110
+
111
+ | Input | Required | What it is |
112
+ |---|---|---|
113
+ | `filterGroups` | no | HubSpot's filterGroups: groups are ORed, filters inside a group are ANDed. Example: [{"filters":[{"propertyName":"amount","operator":"GT","value":"1000"}]}]. |
114
+ | `sorts` | no | Example: [{"propertyName":"createdate","direction":"DESCENDING"}]. |
115
+ | `query` | no | Searches the object's default text properties. |
116
+ | `properties` | no | Comma-separated property names to return. |
117
+ | `limit` | no | 1 to 200. |
118
+ | `after` | no | The cursor from the previous page's paging.next.after. |
119
+
120
+ #### `company_update` — Update company
121
+
122
+ Change properties on an existing company.
123
+
124
+ `PATCH /crm/v3/objects/companies/{companyId}` · idempotent — safe to replay
125
+
126
+ | Input | Required | What it is |
127
+ |---|---|---|
128
+ | `companyId` | yes | The company's HubSpot id (a numeric string). |
129
+ | `properties` | yes | HubSpot property name → value for the company. Values are sent as strings, which is how HubSpot stores every property. |
130
+
68
131
  #### `contact_create` — Create contact
69
132
 
70
133
  Create a contact in HubSpot.
@@ -80,6 +143,160 @@ Create a contact in HubSpot.
80
143
  | `phone` | no | Phone |
81
144
  | `lifecycleStage` | no | HubSpot's own stage names. A value it does not recognise is refused at write time, not at setup. |
82
145
 
146
+ #### `contact_get` — Get contact
147
+
148
+ Read one contact, with its associations when asked.
149
+
150
+ `GET /crm/v3/objects/contacts/{contactId}` · reads only — safe to replay
151
+
152
+ | Input | Required | What it is |
153
+ |---|---|---|
154
+ | `contactId` | yes | The contact's HubSpot id (a numeric string). |
155
+ | `properties` | no | Comma-separated property names to return. HubSpot returns a small default set otherwise. |
156
+ | `associations` | no | Comma-separated object types whose associated ids to return. |
157
+
158
+ #### `contact_list` — List contacts
159
+
160
+ Page through every contact in the portal.
161
+
162
+ `GET /crm/v3/objects/contacts` · reads only — safe to replay
163
+
164
+ | Input | Required | What it is |
165
+ |---|---|---|
166
+ | `limit` | no | How many records per page, 1 to 100. HubSpot's own default is 10. |
167
+ | `after` | no | The cursor from the previous page's paging.next.after. Leave empty for the first page. |
168
+ | `properties` | no | Comma-separated property names to return. HubSpot returns a small default set otherwise. |
169
+ | `associations` | no | Comma-separated object types whose associated ids to return. |
170
+
171
+ #### `contact_search` — Search contacts
172
+
173
+ Find contacts by property filters, sorts or a text query.
174
+
175
+ `POST /crm/v3/objects/contacts/search` · reads only — safe to replay
176
+
177
+ | Input | Required | What it is |
178
+ |---|---|---|
179
+ | `filterGroups` | no | HubSpot's filterGroups: groups are ORed, filters inside a group are ANDed. Example: [{"filters":[{"propertyName":"amount","operator":"GT","value":"1000"}]}]. |
180
+ | `sorts` | no | Example: [{"propertyName":"createdate","direction":"DESCENDING"}]. |
181
+ | `query` | no | Searches the object's default text properties. |
182
+ | `properties` | no | Comma-separated property names to return. |
183
+ | `limit` | no | 1 to 200. |
184
+ | `after` | no | The cursor from the previous page's paging.next.after. |
185
+
186
+ #### `contact_update` — Update contact
187
+
188
+ Change properties on an existing contact.
189
+
190
+ `PATCH /crm/v3/objects/contacts/{contactId}` · idempotent — safe to replay
191
+
192
+ | Input | Required | What it is |
193
+ |---|---|---|
194
+ | `contactId` | yes | The contact's HubSpot id (a numeric string). |
195
+ | `properties` | yes | HubSpot property name → value for the contact. Values are sent as strings, which is how HubSpot stores every property. |
196
+
197
+ #### `deal_create` — Create deal
198
+
199
+ Create a deal in HubSpot.
200
+
201
+ `POST /crm/v3/objects/deals` · **unsafe to replay** — a retried durable run does it TWICE
202
+
203
+ | Input | Required | What it is |
204
+ |---|---|---|
205
+ | `properties` | yes | HubSpot property name → value for the new deal. Values are sent as strings, which is how HubSpot stores every property. |
206
+
207
+ #### `deal_get` — Get deal
208
+
209
+ Read one deal, with its associations when asked.
210
+
211
+ `GET /crm/v3/objects/deals/{dealId}` · reads only — safe to replay
212
+
213
+ | Input | Required | What it is |
214
+ |---|---|---|
215
+ | `dealId` | yes | The deal's HubSpot id (a numeric string). |
216
+ | `properties` | no | Comma-separated property names to return. HubSpot returns a small default set otherwise. |
217
+ | `associations` | no | Comma-separated object types whose associated ids to return. |
218
+
219
+ #### `deal_list` — List deals
220
+
221
+ Page through every deal in the portal.
222
+
223
+ `GET /crm/v3/objects/deals` · reads only — safe to replay
224
+
225
+ | Input | Required | What it is |
226
+ |---|---|---|
227
+ | `limit` | no | How many records per page, 1 to 100. HubSpot's own default is 10. |
228
+ | `after` | no | The cursor from the previous page's paging.next.after. Leave empty for the first page. |
229
+ | `properties` | no | Comma-separated property names to return. HubSpot returns a small default set otherwise. |
230
+ | `associations` | no | Comma-separated object types whose associated ids to return. |
231
+
232
+ #### `deal_search` — Search deals
233
+
234
+ Find deals by property filters, sorts or a text query.
235
+
236
+ `POST /crm/v3/objects/deals/search` · reads only — safe to replay
237
+
238
+ | Input | Required | What it is |
239
+ |---|---|---|
240
+ | `filterGroups` | no | HubSpot's filterGroups: groups are ORed, filters inside a group are ANDed. Example: [{"filters":[{"propertyName":"amount","operator":"GT","value":"1000"}]}]. |
241
+ | `sorts` | no | Example: [{"propertyName":"createdate","direction":"DESCENDING"}]. |
242
+ | `query` | no | Searches the object's default text properties. |
243
+ | `properties` | no | Comma-separated property names to return. |
244
+ | `limit` | no | 1 to 200. |
245
+ | `after` | no | The cursor from the previous page's paging.next.after. |
246
+
247
+ #### `deal_update` — Update deal
248
+
249
+ Change properties on an existing deal.
250
+
251
+ `PATCH /crm/v3/objects/deals/{dealId}` · idempotent — safe to replay
252
+
253
+ | Input | Required | What it is |
254
+ |---|---|---|
255
+ | `dealId` | yes | The deal's HubSpot id (a numeric string). |
256
+ | `properties` | yes | HubSpot property name → value for the deal. Values are sent as strings, which is how HubSpot stores every property. |
257
+
258
+ #### `owner_list` — List owners
259
+
260
+ Page through the portal's owners: the users records can be assigned to.
261
+
262
+ `GET /crm/v3/owners` · reads only — safe to replay
263
+
264
+ | Input | Required | What it is |
265
+ |---|---|---|
266
+ | `email` | no | Only the owner with this email. |
267
+ | `limit` | no | How many records per page, 1 to 100. HubSpot's own default is 10. |
268
+ | `after` | no | The cursor from the previous page's paging.next.after. Leave empty for the first page. |
269
+
270
+ #### `pipeline_list` — List deal pipelines
271
+
272
+ Every deal pipeline in the portal, with its stages in order.
273
+
274
+ `GET /crm/v3/pipelines/deals` · reads only — safe to replay
275
+
276
+ Takes no input.
277
+
278
+ #### `property_list` — List property definitions
279
+
280
+ Every property defined on deals, contacts or companies: name, label, type and the options of an enumeration.
281
+
282
+ `GET /crm/v3/properties/{objectType}` · reads only — safe to replay
283
+
284
+ | Input | Required | What it is |
285
+ |---|---|---|
286
+ | `objectType` | yes | Object |
287
+
288
+ ### Triggers
289
+
290
+ #### `crm_event` — HubSpot CRM event
291
+
292
+ Start when HubSpot reports deals, contacts or companies created, changed, deleted, merged, restored or re-associated.
293
+
294
+ Delivered by webhook, and the signature is verified before anything runs.
295
+
296
+ **You have to set this up with the provider first:**
297
+
298
+ In the HubSpot app's webhook settings, set the target URL to the route your host mounts for this trigger and subscribe to the deal, contact and company events you want. Deliveries are signed with the app's client secret, already on the connection as `clientSecret`. ONE URL serves every portal that installed the app, so route each event by its portalId, and only after the signature has verified: map portalId to the connection created when that portal installed, and drop an unmapped portalId with a 2xx and a log line, creating nothing.
299
+
83
300
  ## Run it before you have credentials
84
301
 
85
302
  Every operation ships a **faker**, whether or not HubSpot has a sandbox. Set a
@@ -0,0 +1,26 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="128" height="28" role="img" aria-label="Fancified — built with Fancy UI">
2
+ <title>Fancified · built with Fancy UI</title>
3
+ <defs>
4
+ <linearGradient id="fcdBg" x1="0" y1="0" x2="1" y2="1">
5
+ <stop offset="0" stop-color="#6d28d9"/>
6
+ <stop offset=".5" stop-color="#9333ea"/>
7
+ <stop offset="1" stop-color="#d946ef"/>
8
+ </linearGradient>
9
+ <linearGradient id="fcdGloss" x1="0" y1="0" x2="0" y2="1">
10
+ <stop offset="0" stop-color="#ffffff" stop-opacity=".30"/>
11
+ <stop offset=".55" stop-color="#ffffff" stop-opacity="0"/>
12
+ </linearGradient>
13
+ <clipPath id="fcdClip"><rect width="128" height="28" rx="7"/></clipPath>
14
+ </defs>
15
+ <g clip-path="url(#fcdClip)">
16
+ <rect width="128" height="28" fill="url(#fcdBg)"/>
17
+ <rect width="34" height="28" fill="#3b0764" fill-opacity=".5"/>
18
+ <rect width="128" height="13" fill="url(#fcdGloss)"/>
19
+ <g fill="#fff">
20
+ <path transform="translate(17 14)" d="M0 -7 C.6 -2.2 2.2 -.6 7 0 C2.2 .6 .6 2.2 0 7 C-.6 2.2 -2.2 .6 -7 0 C-2.2 -.6 -.6 -2.2 0 -7 Z"/>
21
+ <path transform="translate(25.5 7.5) scale(.34)" fill-opacity=".85" d="M0 -7 C.6 -2.2 2.2 -.6 7 0 C2.2 .6 .6 2.2 0 7 C-.6 2.2 -2.2 .6 -7 0 C-2.2 -.6 -.6 -2.2 0 -7 Z"/>
22
+ </g>
23
+ <text x="81" y="18.5" fill="#ffffff" text-anchor="middle" font-family="'Segoe UI',Verdana,'DejaVu Sans',sans-serif" font-size="12" font-weight="700" letter-spacing=".3">Fancified</text>
24
+ </g>
25
+ <rect x=".5" y=".5" width="127" height="27" rx="6.5" fill="none" stroke="#ffffff" stroke-opacity=".22"/>
26
+ </svg>