@rawdash/connector-hubspot 0.16.0 → 0.17.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/dist/index.d.ts CHANGED
@@ -28,6 +28,122 @@ type HubSpotCredentials = typeof hubspotCredentials;
28
28
  declare const PHASE_ORDER: readonly ["contacts", "companies", "deals", "deal_events", "email_campaigns", "email_stats"];
29
29
  type HubSpotPhase = (typeof PHASE_ORDER)[number];
30
30
  type HubSpotResource = HubSpotPhase;
31
+ declare const hubspotResources: {
32
+ readonly hubspot_contact: {
33
+ readonly shape: "entity";
34
+ readonly description: "CRM contacts with email, lifecycle stage, lead status, owner, and creation time.";
35
+ readonly endpoint: "POST /crm/v3/objects/contacts/search";
36
+ readonly responses: {
37
+ readonly contacts: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
38
+ };
39
+ };
40
+ readonly hubspot_company: {
41
+ readonly shape: "entity";
42
+ readonly description: "CRM companies with name, domain, industry, lifecycle stage, and creation time.";
43
+ readonly endpoint: "POST /crm/v3/objects/companies/search";
44
+ readonly responses: {
45
+ readonly companies: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
46
+ };
47
+ };
48
+ readonly hubspot_deal: {
49
+ readonly shape: "entity";
50
+ readonly description: "CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.";
51
+ readonly endpoint: "POST /crm/v3/objects/deals/search";
52
+ readonly responses: {
53
+ readonly deals: z.ZodArray<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
54
+ };
55
+ };
56
+ readonly hubspot_deal_stage_change: {
57
+ readonly shape: "event";
58
+ readonly description: "Deal stage-change events derived from deal property history, one event per stage transition.";
59
+ readonly endpoint: "GET /crm/v3/objects/deals?propertiesWithHistory=dealstage";
60
+ readonly responses: {
61
+ readonly deal_events: z.ZodArray<z.ZodObject<{
62
+ id: z.ZodString;
63
+ propertiesWithHistory: z.ZodOptional<z.ZodObject<{
64
+ dealstage: z.ZodOptional<z.ZodArray<z.ZodObject<{
65
+ value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
66
+ timestamp: z.ZodOptional<z.ZodNullable<z.ZodString>>;
67
+ sourceType: z.ZodOptional<z.ZodNullable<z.ZodString>>;
68
+ }, z.core.$strip>>>;
69
+ }, z.core.$strip>>;
70
+ }, z.core.$strip>>;
71
+ };
72
+ };
73
+ readonly hubspot_email_campaign: {
74
+ readonly shape: "entity";
75
+ readonly description: "Marketing email campaigns with name, subject, sender, type, send date, and recipient count.";
76
+ readonly endpoint: "GET /email/public/v1/campaigns";
77
+ readonly responses: {
78
+ readonly email_campaigns: z.ZodArray<z.ZodObject<{
79
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
80
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
81
+ subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
82
+ fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
83
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
84
+ lastProcessingFinishedAt: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
85
+ numIncluded: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
86
+ counters: z.ZodOptional<z.ZodObject<{
87
+ sent: z.ZodOptional<z.ZodNumber>;
88
+ delivered: z.ZodOptional<z.ZodNumber>;
89
+ open: z.ZodOptional<z.ZodNumber>;
90
+ click: z.ZodOptional<z.ZodNumber>;
91
+ bounce: z.ZodOptional<z.ZodNumber>;
92
+ unsubscribed: z.ZodOptional<z.ZodNumber>;
93
+ }, z.core.$strip>>;
94
+ }, z.core.$strip>>;
95
+ };
96
+ };
97
+ readonly hubspot_email_stats: {
98
+ readonly shape: "metric";
99
+ readonly description: "Per-campaign marketing email engagement stats (sent, delivered, opened, clicked, bounced, unsubscribed) timestamped at the campaign send time.";
100
+ readonly endpoint: "GET /email/public/v1/campaigns/{id}";
101
+ readonly unit: "emails";
102
+ readonly notes: "One sample per campaign; value is the sent count, and every counter (delivered, opened, clicked, bounced, unsubscribed) is also exposed in attributes.";
103
+ readonly dimensions: [{
104
+ readonly name: "campaignId";
105
+ readonly description: "HubSpot campaign id.";
106
+ }, {
107
+ readonly name: "campaignName";
108
+ readonly description: "Campaign name.";
109
+ }, {
110
+ readonly name: "delivered";
111
+ readonly description: "Number of emails delivered.";
112
+ }, {
113
+ readonly name: "opened";
114
+ readonly description: "Number of emails opened.";
115
+ }, {
116
+ readonly name: "clicked";
117
+ readonly description: "Number of emails clicked.";
118
+ }, {
119
+ readonly name: "bounced";
120
+ readonly description: "Number of emails bounced.";
121
+ }, {
122
+ readonly name: "unsubscribed";
123
+ readonly description: "Number of recipients who unsubscribed.";
124
+ }];
125
+ readonly responses: {
126
+ readonly email_stats: z.ZodArray<z.ZodObject<{
127
+ id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
128
+ name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
129
+ subject: z.ZodOptional<z.ZodNullable<z.ZodString>>;
130
+ fromName: z.ZodOptional<z.ZodNullable<z.ZodString>>;
131
+ type: z.ZodOptional<z.ZodNullable<z.ZodString>>;
132
+ lastProcessingFinishedAt: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>>;
133
+ numIncluded: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
134
+ counters: z.ZodOptional<z.ZodObject<{
135
+ sent: z.ZodOptional<z.ZodNumber>;
136
+ delivered: z.ZodOptional<z.ZodNumber>;
137
+ open: z.ZodOptional<z.ZodNumber>;
138
+ click: z.ZodOptional<z.ZodNumber>;
139
+ bounce: z.ZodOptional<z.ZodNumber>;
140
+ unsubscribed: z.ZodOptional<z.ZodNumber>;
141
+ }, z.core.$strip>>;
142
+ }, z.core.$strip>>;
143
+ };
144
+ };
145
+ };
146
+ declare const id = "hubspot";
31
147
  declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCredentials> {
32
148
  static readonly id = "hubspot";
33
149
  static readonly resources: {
@@ -224,4 +340,4 @@ declare class HubSpotConnector extends BaseConnector<HubSpotSettings, HubSpotCre
224
340
  sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
225
341
  }
226
342
 
227
- export { HubSpotConnector, type HubSpotResource, type HubSpotSettings, configFields, HubSpotConnector as default, doc };
343
+ export { HubSpotConnector, type HubSpotResource, type HubSpotSettings, configFields, HubSpotConnector as default, doc, id, hubspotResources as resources };
package/dist/index.js CHANGED
@@ -278,8 +278,9 @@ var hubspotResources = defineResources({
278
278
  responses: { email_stats: campaignsSchema }
279
279
  }
280
280
  });
281
+ var id = "hubspot";
281
282
  var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
282
- static id = "hubspot";
283
+ static id = id;
283
284
  static resources = hubspotResources;
284
285
  static schemas = schemasFromResources(hubspotResources);
285
286
  static create(input, ctx) {
@@ -290,7 +291,7 @@ var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
290
291
  ctx
291
292
  );
292
293
  }
293
- id = "hubspot";
294
+ id = id;
294
295
  credentials = hubspotCredentials;
295
296
  buildHeaders() {
296
297
  return {
@@ -444,9 +445,9 @@ var HubSpotConnector = class _HubSpotConnector extends BaseConnector {
444
445
  // -------------------------------------------------------------------------
445
446
  // Marketing email campaigns + stats (legacy email campaigns API)
446
447
  // -------------------------------------------------------------------------
447
- async fetchCampaignDetail(id, resource, signal) {
448
+ async fetchCampaignDetail(id2, resource, signal) {
448
449
  const res = await this.apiGet(
449
- `${BASE_URL}/email/public/v1/campaigns/${id}`,
450
+ `${BASE_URL}/email/public/v1/campaigns/${id2}`,
450
451
  resource,
451
452
  signal
452
453
  );
@@ -594,6 +595,8 @@ export {
594
595
  HubSpotConnector,
595
596
  configFields,
596
597
  index_default as default,
597
- doc
598
+ doc,
599
+ id,
600
+ hubspotResources as resources
598
601
  };
599
602
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/hubspot.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n accessToken: z.object({ $secret: z.string() }).meta({\n label: 'Private App access token',\n description:\n 'HubSpot private app access token with read scopes for contacts, companies, deals, and marketing email. Create one at Settings → Integrations → Private Apps.',\n placeholder: 'pat-na1-...',\n secret: true,\n }),\n resources: z\n .array(\n z.enum([\n 'contacts',\n 'companies',\n 'deals',\n 'deal_events',\n 'email_campaigns',\n 'email_stats',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which HubSpot resources to sync. Omit to sync all resources. The access token only needs read scopes for the resources listed here.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'HubSpot',\n category: 'sales',\n brandColor: '#FF7A59',\n tagline:\n 'Sync CRM contacts, companies, and deals plus deal stage-change events and marketing email campaign stats from HubSpot.',\n vendor: {\n name: 'HubSpot',\n apiDocs: 'https://developers.hubspot.com/docs/api/overview',\n website: 'https://www.hubspot.com',\n },\n auth: {\n summary:\n 'A HubSpot private app access token with read scopes for the resources you sync (contacts, companies, deals, and marketing email).',\n setup: [\n 'In HubSpot, go to Settings → Integrations → Private Apps and create a private app.',\n 'Grant read scopes for the resources you intend to sync (CRM contacts, companies, deals, and marketing email).',\n 'Copy the generated access token (starts with `pat-`).',\n 'Store it as a secret and reference it from the connector config as `accessToken: secret(\"HUBSPOT_ACCESS_TOKEN\")`.',\n ],\n },\n rateLimit:\n 'HubSpot allows 100 requests / 10s; the Search API caps results at 10,000 per query.',\n limitations: [\n 'Deal stage-change events are rewritten on every sync because the deal list endpoint has no incremental `since` filter.',\n 'Marketing email campaign data comes from the legacy email campaigns API and is only available for marketing emails.',\n 'Very large CRM portfolios may not backfill in full because the Search API caps at 10,000 results per query.',\n ],\n});\n\nexport interface HubSpotSettings {\n resources?: readonly HubSpotResource[];\n}\n\n// ---------------------------------------------------------------------------\n// HubSpot API types\n// ---------------------------------------------------------------------------\n\ntype HubSpotProperties = Record<string, string | null | undefined>;\n\ninterface CrmRecord {\n id: string;\n properties: HubSpotProperties;\n createdAt: string;\n updatedAt: string;\n archived?: boolean;\n}\n\ninterface CrmSearchResponse {\n total?: number;\n results: CrmRecord[];\n paging?: { next?: { after?: string } };\n}\n\ninterface CrmListResponse {\n results: DealHistoryRecord[];\n paging?: { next?: { after?: string } };\n}\n\ninterface DealHistoryEntry {\n value?: string | null;\n timestamp?: string | null;\n sourceType?: string | null;\n}\n\ninterface DealHistoryRecord {\n id: string;\n propertiesWithHistory?: {\n dealstage?: DealHistoryEntry[];\n };\n}\n\ninterface CampaignListResponse {\n campaigns: Array<{ id: number | string }>;\n hasMore?: boolean;\n offset?: number | string;\n}\n\ninterface CampaignCounters {\n sent?: number;\n delivered?: number;\n open?: number;\n click?: number;\n bounce?: number;\n unsubscribed?: number;\n}\n\ninterface CampaignDetail {\n id: number | string;\n name?: string | null;\n subject?: string | null;\n fromName?: string | null;\n type?: string | null;\n lastProcessingFinishedAt?: number | string | null;\n numIncluded?: number | null;\n counters?: CampaignCounters;\n}\n\n// ---------------------------------------------------------------------------\n// Credentials\n// ---------------------------------------------------------------------------\n\nconst hubspotCredentials = {\n accessToken: {\n description: 'HubSpot private app access token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype HubSpotCredentials = typeof hubspotCredentials;\n\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\n// ---------------------------------------------------------------------------\n\nconst PHASE_ORDER = [\n 'contacts',\n 'companies',\n 'deals',\n 'deal_events',\n 'email_campaigns',\n 'email_stats',\n] as const;\n\ntype HubSpotPhase = (typeof PHASE_ORDER)[number];\n\nexport type HubSpotResource = HubSpotPhase;\n\nconst isHubSpotSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst BASE_URL = 'https://api.hubapi.com';\nconst SEARCH_LIMIT = 100;\nconst LIST_LIMIT = 100;\n\ntype CrmObjectPhase = 'contacts' | 'companies' | 'deals';\n\n// CRM object name + property names requested per search phase.\nconst SEARCH_PROPERTIES: Record<CrmObjectPhase, readonly string[]> = {\n contacts: [\n 'email',\n 'lifecyclestage',\n 'hs_lead_status',\n 'createdate',\n 'lastmodifieddate',\n 'hubspot_owner_id',\n ],\n companies: [\n 'name',\n 'domain',\n 'industry',\n 'createdate',\n 'lifecyclestage',\n 'hs_lastmodifieddate',\n ],\n deals: [\n 'dealname',\n 'dealstage',\n 'pipeline',\n 'amount',\n 'closedate',\n 'hubspot_owner_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ],\n};\n\n// Property each CRM object stamps with its last-modified time, used both for\n// the incremental `since` filter and the entity `updated_at` fallback.\nconst MODIFIED_PROPERTY: Record<CrmObjectPhase, string> = {\n contacts: 'lastmodifieddate',\n companies: 'hs_lastmodifieddate',\n deals: 'hs_lastmodifieddate',\n};\n\nconst ENTITY_TYPE_BY_PHASE: Partial<Record<HubSpotPhase, string>> = {\n contacts: 'hubspot_contact',\n companies: 'hubspot_company',\n deals: 'hubspot_deal',\n email_campaigns: 'hubspot_email_campaign',\n};\n\nconst DEAL_STAGE_EVENT = 'hubspot_deal_stage_change';\nconst EMAIL_STATS_METRIC = 'hubspot_email_stats';\n\n// ---------------------------------------------------------------------------\n// Value helpers\n// ---------------------------------------------------------------------------\n\nfunction finiteNumberOrNull(value: string | null | undefined): number | null {\n if (value === null || value === undefined || value.trim() === '') {\n return null;\n }\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction counterValue(value: number | undefined): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\n// ---------------------------------------------------------------------------\n\nconst idString = z.string().min(1);\n\nconst contactProperties = z.object({\n email: z.string().nullish(),\n lifecyclestage: z.string().nullish(),\n hs_lead_status: z.string().nullish(),\n createdate: z.string().nullish(),\n lastmodifieddate: z.string().nullish(),\n hubspot_owner_id: z.string().nullish(),\n});\n\nconst companyProperties = z.object({\n name: z.string().nullish(),\n domain: z.string().nullish(),\n industry: z.string().nullish(),\n createdate: z.string().nullish(),\n lifecyclestage: z.string().nullish(),\n hs_lastmodifieddate: z.string().nullish(),\n});\n\nconst dealProperties = z.object({\n dealname: z.string().nullish(),\n dealstage: z.string().nullish(),\n pipeline: z.string().nullish(),\n amount: z.string().nullish(),\n closedate: z.string().nullish(),\n hubspot_owner_id: z.string().nullish(),\n createdate: z.string().nullish(),\n hs_lastmodifieddate: z.string().nullish(),\n});\n\nfunction crmRecordSchema(props: z.ZodType): z.ZodType {\n return z.object({\n id: idString,\n properties: props,\n createdAt: z.string(),\n updatedAt: z.string(),\n archived: z.boolean().optional(),\n });\n}\n\nconst dealHistoryRecordSchema = z.object({\n id: idString,\n propertiesWithHistory: z\n .object({\n dealstage: z\n .array(\n z.object({\n value: z.string().nullish(),\n timestamp: z.string().nullish(),\n sourceType: z.string().nullish(),\n }),\n )\n .optional(),\n })\n .optional(),\n});\n\nconst campaignDetailSchema = z.object({\n id: z.union([z.string(), z.number()]),\n name: z.string().nullish(),\n subject: z.string().nullish(),\n fromName: z.string().nullish(),\n type: z.string().nullish(),\n lastProcessingFinishedAt: z.union([z.string(), z.number()]).nullish(),\n numIncluded: z.number().nullish(),\n counters: z\n .object({\n sent: z.number().optional(),\n delivered: z.number().optional(),\n open: z.number().optional(),\n click: z.number().optional(),\n bounce: z.number().optional(),\n unsubscribed: z.number().optional(),\n })\n .optional(),\n});\n\nconst contactsSchema = z.array(crmRecordSchema(contactProperties));\nconst companiesSchema = z.array(crmRecordSchema(companyProperties));\nconst dealsSchema = z.array(crmRecordSchema(dealProperties));\nconst dealEventsSchema = z.array(dealHistoryRecordSchema);\nconst campaignsSchema = z.array(campaignDetailSchema);\n\nconst hubspotResources = defineResources({\n hubspot_contact: {\n shape: 'entity',\n description:\n 'CRM contacts with email, lifecycle stage, lead status, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/contacts/search',\n responses: { contacts: contactsSchema },\n },\n hubspot_company: {\n shape: 'entity',\n description:\n 'CRM companies with name, domain, industry, lifecycle stage, and creation time.',\n endpoint: 'POST /crm/v3/objects/companies/search',\n responses: { companies: companiesSchema },\n },\n hubspot_deal: {\n shape: 'entity',\n description:\n 'CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/deals/search',\n responses: { deals: dealsSchema },\n },\n hubspot_deal_stage_change: {\n shape: 'event',\n description:\n 'Deal stage-change events derived from deal property history, one event per stage transition.',\n endpoint: 'GET /crm/v3/objects/deals?propertiesWithHistory=dealstage',\n responses: { deal_events: dealEventsSchema },\n },\n hubspot_email_campaign: {\n shape: 'entity',\n description:\n 'Marketing email campaigns with name, subject, sender, type, send date, and recipient count.',\n endpoint: 'GET /email/public/v1/campaigns',\n responses: { email_campaigns: campaignsSchema },\n },\n hubspot_email_stats: {\n shape: 'metric',\n description:\n 'Per-campaign marketing email engagement stats (sent, delivered, opened, clicked, bounced, unsubscribed) timestamped at the campaign send time.',\n endpoint: 'GET /email/public/v1/campaigns/{id}',\n unit: 'emails',\n notes:\n 'One sample per campaign; value is the sent count, and every counter (delivered, opened, clicked, bounced, unsubscribed) is also exposed in attributes.',\n dimensions: [\n { name: 'campaignId', description: 'HubSpot campaign id.' },\n { name: 'campaignName', description: 'Campaign name.' },\n { name: 'delivered', description: 'Number of emails delivered.' },\n { name: 'opened', description: 'Number of emails opened.' },\n { name: 'clicked', description: 'Number of emails clicked.' },\n { name: 'bounced', description: 'Number of emails bounced.' },\n {\n name: 'unsubscribed',\n description: 'Number of recipients who unsubscribed.',\n },\n ],\n responses: { email_stats: campaignsSchema },\n },\n});\n\n// ---------------------------------------------------------------------------\n// HubSpotConnector\n// ---------------------------------------------------------------------------\n\nexport class HubSpotConnector extends BaseConnector<\n HubSpotSettings,\n HubSpotCredentials\n> {\n static readonly id = 'hubspot';\n\n static readonly resources = hubspotResources;\n\n static readonly schemas = schemasFromResources(hubspotResources);\n\n static create(input: unknown, ctx?: ConnectorContext): HubSpotConnector {\n const parsed = configFields.parse(input);\n return new HubSpotConnector(\n { resources: parsed.resources },\n { accessToken: parsed.accessToken },\n ctx,\n );\n }\n\n readonly id = 'hubspot';\n override readonly credentials = hubspotCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent('hubspot'),\n };\n }\n\n private apiGet<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n });\n }\n\n private apiPost<T>(\n url: string,\n resource: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.post<T>(url, {\n resource,\n headers: this.buildHeaders(),\n body: JSON.stringify(body),\n signal,\n });\n }\n\n // -------------------------------------------------------------------------\n // CRM search phases (contacts / companies / deals)\n // -------------------------------------------------------------------------\n\n private buildSearchBody(\n phase: CrmObjectPhase,\n after: string | null,\n options: SyncOptions,\n ): Record<string, unknown> {\n const modifiedProperty = MODIFIED_PROPERTY[phase];\n const filterGroups: unknown[] = [];\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n filterGroups.push({\n filters: [\n {\n propertyName: modifiedProperty,\n operator: 'GTE',\n value: String(sinceMs),\n },\n ],\n });\n }\n }\n return {\n filterGroups,\n // Ascending modified-time keeps pagination stable while the `since`\n // filter trims the set upstream, so an incremental sync never scans\n // the whole object.\n sorts: [{ propertyName: modifiedProperty, direction: 'ASCENDING' }],\n properties: SEARCH_PROPERTIES[phase],\n limit: SEARCH_LIMIT,\n ...(after ? { after } : {}),\n };\n }\n\n private async fetchSearchPage(\n phase: CrmObjectPhase,\n after: string | null,\n options: SyncOptions,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const body = this.buildSearchBody(phase, after, options);\n const res = await this.apiPost<CrmSearchResponse>(\n `${BASE_URL}/crm/v3/objects/${phase}/search`,\n phase,\n body,\n signal,\n );\n return {\n items: res.body.results,\n next: res.body.paging?.next?.after ?? null,\n };\n }\n\n private async writeSearchPhase(\n storage: StorageHandle,\n phase: CrmObjectPhase,\n items: CrmRecord[],\n ): Promise<void> {\n for (const record of items) {\n const props = record.properties;\n const modifiedProperty = MODIFIED_PROPERTY[phase];\n const updatedAt =\n parseEpoch(record.updatedAt, 'iso') ??\n parseEpoch(props[modifiedProperty], 'ms') ??\n 0;\n\n let attributes: Record<string, string | number | null>;\n if (phase === 'contacts') {\n attributes = {\n email: props.email ?? null,\n lifecycleStage: props.lifecyclestage ?? null,\n leadStatus: props.hs_lead_status ?? null,\n ownerId: props.hubspot_owner_id ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n } else if (phase === 'companies') {\n attributes = {\n name: props.name ?? null,\n domain: props.domain ?? null,\n industry: props.industry ?? null,\n lifecycleStage: props.lifecyclestage ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n } else {\n attributes = {\n dealName: props.dealname ?? null,\n dealStage: props.dealstage ?? null,\n pipeline: props.pipeline ?? null,\n amount: finiteNumberOrNull(props.amount),\n closeDate: parseEpoch(props.closedate, 'ms'),\n ownerId: props.hubspot_owner_id ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n }\n\n await storage.entity({\n type: ENTITY_TYPE_BY_PHASE[phase]!,\n id: record.id,\n attributes,\n updated_at: updatedAt,\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Deal stage-change events (deal property history)\n // -------------------------------------------------------------------------\n\n private async fetchDealHistoryPage(\n after: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const url = new URL(`${BASE_URL}/crm/v3/objects/deals`);\n url.searchParams.set('limit', String(LIST_LIMIT));\n url.searchParams.set('properties', 'dealstage');\n url.searchParams.set('propertiesWithHistory', 'dealstage');\n if (after) {\n url.searchParams.set('after', after);\n }\n const res = await this.apiGet<CrmListResponse>(\n url.toString(),\n 'deal_events',\n signal,\n );\n return {\n items: res.body.results,\n next: res.body.paging?.next?.after ?? null,\n };\n }\n\n private async writeDealEvents(\n storage: StorageHandle,\n items: DealHistoryRecord[],\n _options: SyncOptions,\n ): Promise<void> {\n for (const record of items) {\n const history = record.propertiesWithHistory?.dealstage ?? [];\n for (const entry of history) {\n const ts = parseEpoch(entry.timestamp, 'iso');\n if (ts === null) {\n continue;\n }\n await storage.event({\n name: DEAL_STAGE_EVENT,\n start_ts: ts,\n end_ts: null,\n attributes: {\n dealId: record.id,\n stage: entry.value ?? null,\n sourceType: entry.sourceType ?? null,\n },\n });\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Marketing email campaigns + stats (legacy email campaigns API)\n // -------------------------------------------------------------------------\n\n private async fetchCampaignDetail(\n id: number | string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<CampaignDetail> {\n const res = await this.apiGet<CampaignDetail>(\n `${BASE_URL}/email/public/v1/campaigns/${id}`,\n resource,\n signal,\n );\n return res.body;\n }\n\n private async fetchCampaignsPage(\n phase: 'email_campaigns' | 'email_stats',\n after: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const url = new URL(`${BASE_URL}/email/public/v1/campaigns`);\n url.searchParams.set('limit', String(LIST_LIMIT));\n if (after) {\n url.searchParams.set('offset', after);\n }\n const listRes = await this.apiGet<CampaignListResponse>(\n url.toString(),\n `${phase}_list`,\n signal,\n );\n const { campaigns, hasMore, offset } = listRes.body;\n\n const details: CampaignDetail[] = [];\n for (const campaign of campaigns) {\n details.push(await this.fetchCampaignDetail(campaign.id, phase, signal));\n }\n\n const next =\n hasMore && offset !== undefined && offset !== null\n ? String(offset)\n : null;\n return { items: details, next };\n }\n\n private async writeEmailCampaigns(\n storage: StorageHandle,\n items: CampaignDetail[],\n ): Promise<void> {\n for (const detail of items) {\n const sentDate = parseEpoch(detail.lastProcessingFinishedAt, 'ms');\n await storage.entity({\n type: ENTITY_TYPE_BY_PHASE.email_campaigns!,\n id: String(detail.id),\n attributes: {\n name: detail.name ?? null,\n subject: detail.subject ?? null,\n fromName: detail.fromName ?? null,\n type: detail.type ?? null,\n sentDate,\n numIncluded: detail.numIncluded ?? null,\n },\n updated_at: sentDate ?? 0,\n });\n }\n }\n\n private async writeEmailStats(\n storage: StorageHandle,\n items: CampaignDetail[],\n ): Promise<void> {\n for (const detail of items) {\n const ts = parseEpoch(detail.lastProcessingFinishedAt, 'ms');\n if (ts === null) {\n continue;\n }\n const counters = detail.counters ?? {};\n const sent = counterValue(counters.sent);\n await storage.metric({\n name: EMAIL_STATS_METRIC,\n ts,\n value: sent,\n attributes: {\n campaignId: String(detail.id),\n campaignName: detail.name ?? null,\n sent,\n delivered: counterValue(counters.delivered),\n opened: counterValue(counters.open),\n clicked: counterValue(counters.click),\n bounced: counterValue(counters.bounce),\n unsubscribed: counterValue(counters.unsubscribed),\n },\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Scope clearing (idempotency)\n // -------------------------------------------------------------------------\n\n private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: HubSpotPhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'deal_events') {\n // Events never upsert and the list endpoint has no `since` filter, so\n // every sync rewrites the requested window — clear in both modes.\n await storage.events([], { names: [DEAL_STAGE_EVENT] });\n return;\n }\n if (phase === 'email_stats') {\n await storage.metrics([], { names: [EMAIL_STATS_METRIC] });\n return;\n }\n // Entity phases upsert by id, so only a full backfill needs to drop stale\n // rows; incremental ticks just overwrite the records they touch.\n if (!isFull) {\n return;\n }\n const entityType = ENTITY_TYPE_BY_PHASE[phase];\n if (entityType) {\n await storage.entities([], { types: [entityType] });\n }\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: HubSpotPhase,\n items: unknown[],\n options: SyncOptions,\n ): Promise<void> {\n switch (phase) {\n case 'contacts':\n case 'companies':\n case 'deals':\n await this.writeSearchPhase(storage, phase, items as CrmRecord[]);\n return;\n case 'deal_events':\n await this.writeDealEvents(\n storage,\n items as DealHistoryRecord[],\n options,\n );\n return;\n case 'email_campaigns':\n await this.writeEmailCampaigns(storage, items as CampaignDetail[]);\n return;\n case 'email_stats':\n await this.writeEmailStats(storage, items as CampaignDetail[]);\n return;\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isHubSpotSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<HubSpotResource, HubSpotPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<HubSpotPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (phase, page, sig) => {\n if (\n phase === 'contacts' ||\n phase === 'companies' ||\n phase === 'deals'\n ) {\n return this.fetchSearchPage(phase, page, options, sig);\n }\n if (phase === 'deal_events') {\n return this.fetchDealHistoryPage(page, sig);\n }\n return this.fetchCampaignsPage(phase, page, sig);\n },\n writeBatch: async (phase, items, page) => {\n if (page === null) {\n await this.clearScopeOnFirstPage(storage, phase, isFull);\n }\n await this.writePhase(storage, phase, items, options);\n },\n });\n }\n}\n","import { HubSpotConnector } from './hubspot';\n\nexport { configFields, doc, HubSpotConnector } from './hubspot';\nexport type { HubSpotSettings, HubSpotResource } from './hubspot';\nexport default HubSpotConnector;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../connector-shared/src/errors.ts","../../../connector-shared/src/retry.ts","../../../connector-shared/src/version.ts","../../../connector-shared/src/request.ts","../../../connector-shared/src/rate-limit.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/hubspot.ts","../src/index.ts"],"sourcesContent":["import type { HttpResponse } from './types';\n\nexport type HttpErrorKind =\n | 'transient'\n | 'rate_limit'\n | 'auth'\n | 'upstream_bug'\n | 'client_bug';\n\nexport abstract class HttpClientError extends Error {\n abstract readonly kind: HttpErrorKind;\n readonly response?: HttpResponse;\n\n constructor(message: string, response?: HttpResponse) {\n super(message);\n this.name = new.target.name;\n this.response = response;\n }\n}\n\nexport class TransientError extends HttpClientError {\n readonly kind = 'transient' as const;\n}\n\nexport class RateLimitError extends HttpClientError {\n readonly kind = 'rate_limit' as const;\n readonly retryAfter?: Date;\n\n constructor(message: string, response?: HttpResponse, retryAfter?: Date) {\n super(message, response);\n this.retryAfter = retryAfter;\n }\n}\n\nexport class AuthError extends HttpClientError {\n readonly kind = 'auth' as const;\n}\n\nexport class UpstreamBugError extends HttpClientError {\n readonly kind = 'upstream_bug' as const;\n}\n\nexport class ClientBugError extends HttpClientError {\n readonly kind = 'client_bug' as const;\n}\n\nexport function classifyStatus(status: number): HttpErrorKind {\n if (status === 429) {\n return 'rate_limit';\n }\n if (status === 401 || status === 403) {\n return 'auth';\n }\n if (status === 408) {\n return 'transient';\n }\n if (status >= 500) {\n return 'upstream_bug';\n }\n if (status >= 400) {\n return 'client_bug';\n }\n return 'client_bug';\n}\n\nexport function errorForStatus(\n message: string,\n response: HttpResponse,\n retryAfter?: Date,\n): HttpClientError {\n const kind = classifyStatus(response.status);\n switch (kind) {\n case 'rate_limit':\n return new RateLimitError(message, response, retryAfter);\n case 'auth':\n return new AuthError(message, response);\n case 'transient':\n return new TransientError(message, response);\n case 'upstream_bug':\n return new UpstreamBugError(message, response);\n case 'client_bug':\n return new ClientBugError(message, response);\n }\n}\n","import { HttpClientError, RateLimitError, TransientError } from './errors';\n\nexport interface RetryPolicy {\n maxAttempts?: number;\n initialDelayMs?: number;\n maxDelayMs?: number;\n retryOn?: (status: number | null, err?: Error) => boolean;\n}\n\nexport const defaultRetryOn = (status: number | null, err?: Error): boolean => {\n if (err instanceof RateLimitError) {\n return true;\n }\n if (err instanceof TransientError) {\n return true;\n }\n if (status === null) {\n return err instanceof Error && !(err instanceof HttpClientError);\n }\n if (status === 408 || status === 429) {\n return true;\n }\n if (status >= 500) {\n return true;\n }\n return false;\n};\n\nexport function backoffDelayMs(\n attempt: number,\n policy: Required<Pick<RetryPolicy, 'initialDelayMs' | 'maxDelayMs'>>,\n): number {\n const base = policy.initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, policy.maxDelayMs);\n}\n\nexport function parseRetryAfter(\n headerValue: string | null,\n now: Date = new Date(),\n): Date | undefined {\n if (!headerValue) {\n return undefined;\n }\n const trimmed = headerValue.trim();\n if (/^\\d+$/.test(trimmed)) {\n return new Date(now.getTime() + Number(trimmed) * 1000);\n }\n const parsed = Date.parse(trimmed);\n if (Number.isNaN(parsed)) {\n return undefined;\n }\n return new Date(parsed);\n}\n\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) {\n return Promise.reject(signal.reason ?? new Error('Aborted'));\n }\n return new Promise<void>((resolve, reject) => {\n const onAbort = () => {\n clearTimeout(timer);\n reject(signal!.reason ?? new Error('Aborted'));\n };\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n","export const HTTP_CLIENT_VERSION = '0.0.0';\n\nexport const DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n\nexport function connectorUserAgent(connectorId: string): string {\n return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;\n}\n","import {\n AuthError,\n ClientBugError,\n HttpClientError,\n RateLimitError,\n TransientError,\n UpstreamBugError,\n errorForStatus,\n} from './errors';\nimport { defaultRetryOn, parseRetryAfter, sleep } from './retry';\nimport type { FetchLike, HttpMethod, HttpRequest, HttpResponse } from './types';\nimport { DEFAULT_USER_AGENT } from './version';\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_ATTEMPTS = 3;\nconst DEFAULT_INITIAL_DELAY_MS = 1000;\nconst DEFAULT_MAX_DELAY_MS = 60_000;\nconst OBSERVER_TIMEOUT_MS = 250;\n\nexport interface RequestObservation {\n url: string;\n method: HttpMethod;\n status: number;\n resource: string;\n requestId: string;\n body: unknown;\n}\n\nexport type RequestObserver = (\n event: RequestObservation,\n) => void | Promise<void>;\n\nexport interface RequestOptions {\n fetch?: FetchLike;\n observer?: RequestObserver;\n resource: string;\n requestId?: string;\n}\n\nasync function notifyObserver(\n observer: RequestObserver,\n event: RequestObservation,\n): Promise<void> {\n let result: void | Promise<void>;\n try {\n result = observer(event);\n } catch (err) {\n console.warn('[connector-shared] request observer threw:', err);\n return;\n }\n if (!(result instanceof Promise)) {\n return;\n }\n const guarded = result.catch((err) => {\n console.warn('[connector-shared] request observer rejected:', err);\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>((resolve) => {\n timer = setTimeout(resolve, OBSERVER_TIMEOUT_MS);\n });\n try {\n await Promise.race([guarded, timeout]);\n } finally {\n if (timer) {\n clearTimeout(timer);\n }\n }\n}\n\nfunction newRequestId(): string {\n const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (c?.randomUUID) {\n return c.randomUUID();\n }\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\nfunction mergeHeaders(\n defaults: Record<string, string>,\n overrides: Record<string, string> | undefined,\n): Record<string, string> {\n const merged: Record<string, string> = {};\n for (const [k, v] of Object.entries(defaults)) {\n merged[k.toLowerCase()] = v;\n }\n if (overrides) {\n for (const [k, v] of Object.entries(overrides)) {\n merged[k.toLowerCase()] = v;\n }\n }\n return merged;\n}\n\nfunction linkTimeoutSignal(\n parent: AbortSignal | undefined,\n timeoutMs: number,\n): { signal: AbortSignal; cancel: () => void } {\n const controller = new AbortController();\n const onParentAbort = () => {\n controller.abort(parent?.reason);\n };\n if (parent) {\n if (parent.aborted) {\n controller.abort(parent.reason);\n } else {\n parent.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n const timer = setTimeout(() => {\n controller.abort(new Error(`Request timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (parent) {\n parent.removeEventListener('abort', onParentAbort);\n }\n },\n };\n}\n\nasync function readBody(res: Response, parseJson: boolean): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n const contentType = res.headers.get('content-type') ?? '';\n if (parseJson && contentType.includes('application/json')) {\n const text = await res.text();\n if (text.length === 0) {\n return null;\n }\n return JSON.parse(text);\n }\n return res.text();\n}\n\nexport async function request<T = unknown>(\n req: HttpRequest,\n options: RequestOptions,\n): Promise<HttpResponse<T>> {\n const fetchImpl: FetchLike = options.fetch ?? (globalThis.fetch as FetchLike);\n const retry = req.retry ?? {};\n const maxAttempts = retry.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;\n const initialDelayMs = retry.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;\n const maxDelayMs = retry.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;\n const retryOn = retry.retryOn ?? defaultRetryOn;\n const timeoutMs = req.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const parseJson = req.parseJson ?? true;\n\n const headers = mergeHeaders(\n {\n 'User-Agent': DEFAULT_USER_AGENT,\n Accept: 'application/json',\n },\n req.headers,\n );\n\n let lastErr: Error | undefined;\n\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n req.signal?.throwIfAborted();\n\n const { signal, cancel } = linkTimeoutSignal(req.signal, timeoutMs);\n let res: Response;\n try {\n res = await fetchImpl(req.url, {\n method: req.method ?? 'GET',\n headers,\n body: req.body as RequestInit['body'],\n signal,\n });\n } catch (err) {\n cancel();\n if (req.signal?.aborted) {\n throw req.signal.reason ?? err;\n }\n const error = err instanceof Error ? err : new Error(String(err));\n lastErr = error;\n if (attempt < maxAttempts - 1 && retryOn(null, error)) {\n const delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n await sleep(delay, req.signal);\n continue;\n }\n throw new TransientError(error.message);\n }\n cancel();\n\n const body = await readBody(res, parseJson);\n const httpResponse: HttpResponse<T> = {\n status: res.status,\n headers: res.headers,\n body: body as T,\n };\n if (req.rateLimit) {\n const state = req.rateLimit.parse(res.headers);\n if (state) {\n httpResponse.rateLimitState = state;\n }\n }\n\n if (options.observer) {\n await notifyObserver(options.observer, {\n url: req.url,\n method: req.method ?? 'GET',\n status: res.status,\n resource: options.resource,\n requestId: options.requestId ?? newRequestId(),\n body,\n });\n }\n\n if (res.ok) {\n return httpResponse;\n }\n\n const retryAfter = parseRetryAfter(res.headers.get('retry-after'));\n const message = `HTTP ${res.status} ${res.statusText} for ${req.method ?? 'GET'} ${req.url}`;\n const err = errorForStatus(message, httpResponse, retryAfter);\n\n if (\n attempt < maxAttempts - 1 &&\n retryOn(res.status, err) &&\n !(err instanceof AuthError) &&\n !(err instanceof ClientBugError)\n ) {\n lastErr = err;\n let delay = computeDelay(attempt, initialDelayMs, maxDelayMs);\n if (err instanceof RateLimitError && retryAfter) {\n const wait = retryAfter.getTime() - Date.now();\n if (wait > 0) {\n delay = Math.min(wait, maxDelayMs);\n }\n }\n await sleep(delay, req.signal);\n continue;\n }\n\n throw err;\n }\n\n throw lastErr ?? new UpstreamBugError('Exhausted retry attempts');\n}\n\nfunction computeDelay(\n attempt: number,\n initialDelayMs: number,\n maxDelayMs: number,\n): number {\n const base = initialDelayMs * 2 ** attempt;\n const jitter = base * 0.25 * Math.random();\n return Math.min(base + jitter, maxDelayMs);\n}\n\nexport { HttpClientError };\n","export interface RateLimitState {\n remaining: number;\n resetAt: Date;\n}\n\nexport interface RateLimitPolicy {\n parse(headers: Headers): RateLimitState | null;\n}\n\nexport interface StandardRateLimitPolicyConfig {\n remainingHeader: string;\n resetHeader: string;\n resetUnit: 's' | 'ms';\n resetFallbackMs?: number;\n}\n\nexport function standardRateLimitPolicy(\n config: StandardRateLimitPolicyConfig,\n): RateLimitPolicy {\n const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;\n const multiplier = resetUnit === 's' ? 1000 : 1;\n return {\n parse(h) {\n const remainingRaw = h.get(remainingHeader);\n if (remainingRaw === null || remainingRaw.trim() === '') {\n return null;\n }\n const remaining = Number(remainingRaw);\n if (!Number.isFinite(remaining)) {\n return null;\n }\n const resetRaw = h.get(resetHeader);\n if (resetRaw === null) {\n if (resetFallbackMs === undefined) {\n return null;\n }\n return {\n remaining,\n resetAt: new Date(Date.now() + resetFallbackMs),\n };\n }\n if (resetRaw.trim() === '') {\n return null;\n }\n const reset = Number(resetRaw);\n if (!Number.isFinite(reset) || reset < 0) {\n return null;\n }\n const resetMs = reset * multiplier;\n if (!Number.isFinite(resetMs)) {\n return null;\n }\n return { remaining, resetAt: new Date(resetMs) };\n },\n };\n}\n","export interface SanitizeAllowedUrlOptions {\n url: string | null;\n host: string;\n pathname: string;\n protocol?: 'https:' | 'http:';\n}\n\nexport function sanitizeAllowedUrl(\n options: SanitizeAllowedUrlOptions,\n): string | null {\n const { url, host, pathname, protocol = 'https:' } = options;\n if (url === null) {\n return null;\n }\n try {\n const u = new URL(url);\n if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {\n return null;\n }\n return u.toString();\n } catch {\n return null;\n }\n}\n","export type EpochUnit = 'ms' | 's' | 'iso';\n\nexport function parseEpoch(\n value: number | string | null | undefined,\n unit: EpochUnit,\n): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n if (unit === 'iso') {\n if (typeof value !== 'string') {\n return null;\n }\n const ms = new Date(value).getTime();\n return Number.isFinite(ms) ? ms : null;\n }\n if (typeof value === 'string' && value.trim() === '') {\n return null;\n }\n const n = typeof value === 'number' ? value : Number(value);\n if (!Number.isFinite(n)) {\n return null;\n }\n const result = unit === 's' ? n * 1000 : n;\n return Number.isFinite(result) ? result : null;\n}\n","import { request } from './request';\nimport type { HttpRequest } from './types';\n\nexport function parseLinkHeader(header: string | null): Record<string, string> {\n if (!header) {\n return {};\n }\n const result: Record<string, string> = {};\n for (const part of header.split(',')) {\n const match = part.match(/<([^>]+)>\\s*;\\s*rel=\"([^\"]+)\"/);\n if (match) {\n result[match[2]!] = match[1]!;\n }\n }\n return result;\n}\n\nexport async function* paginateLink<T>(\n initial: HttpRequest,\n parse: (body: unknown) => T[],\n options: { resource: string },\n): AsyncIterable<T> {\n let next: string | null = initial.url;\n while (next) {\n const res: Awaited<ReturnType<typeof request>> = await request(\n {\n ...initial,\n url: next,\n },\n { resource: options.resource },\n );\n for (const item of parse(res.body)) {\n yield item;\n }\n const links = parseLinkHeader(res.headers.get('link'));\n next = links['next'] ?? null;\n }\n}\n\nexport async function* paginateCursor<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; nextCursor: string | null },\n buildNext: (req: HttpRequest, cursor: string) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let req: HttpRequest = initial;\n while (true) {\n const res = await request(req, { resource: options.resource });\n const { items, nextCursor } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!nextCursor) {\n return;\n }\n req = buildNext(req, nextCursor);\n }\n}\n\nexport async function* paginatePage<T>(\n initial: HttpRequest,\n parse: (body: unknown) => { items: T[]; hasMore: boolean },\n buildPage: (req: HttpRequest, page: number) => HttpRequest,\n options: { resource: string },\n): AsyncIterable<T> {\n let page = 1;\n while (true) {\n const req = page === 1 ? initial : buildPage(initial, page);\n const res = await request(req, { resource: options.resource });\n const { items, hasMore } = parse(res.body);\n for (const item of items) {\n yield item;\n }\n if (!hasMore || items.length === 0) {\n return;\n }\n page++;\n }\n}\n","export type LogFields = Record<string, unknown>;\n\nexport interface ConnectorLogger {\n info(event: string, fields?: LogFields): void;\n warn(event: string, fields?: LogFields): void;\n}\n\nexport interface ConnectorLoggerOptions {\n scope: string;\n}\n\nconst MAX_VALUE_LEN = 120;\n\nfunction truncate(s: string, max = MAX_VALUE_LEN): string {\n if (s.length <= max) {\n return s;\n }\n return `${s.slice(0, max - 1)}…`;\n}\n\nfunction formatValue(value: unknown): string {\n if (value === null) {\n return 'null';\n }\n if (value === undefined) {\n return '';\n }\n if (typeof value === 'number' || typeof value === 'boolean') {\n return String(value);\n }\n if (typeof value === 'string') {\n const t = truncate(value);\n if (/[\\s\"=]/.test(t)) {\n return JSON.stringify(t);\n }\n return t;\n }\n if (typeof value === 'bigint') {\n return value.toString();\n }\n let json: string | undefined;\n try {\n json = JSON.stringify(value);\n } catch {\n json = undefined;\n }\n return truncate(json ?? String(value));\n}\n\nexport function formatLogFields(fields?: LogFields): string {\n if (!fields) {\n return '';\n }\n const parts: string[] = [];\n for (const [k, v] of Object.entries(fields)) {\n if (v === undefined) {\n continue;\n }\n parts.push(`${k}=${formatValue(v)}`);\n }\n return parts.length > 0 ? ` ${parts.join(' ')}` : '';\n}\n\nexport function formatLogLine(\n scope: string,\n event: string,\n fields?: LogFields,\n): string {\n return `[${scope}] ${event}${formatLogFields(fields)}`;\n}\n\nexport function createDefaultConnectorLogger(\n opts: ConnectorLoggerOptions,\n): ConnectorLogger {\n return {\n info(event, fields) {\n console.info(formatLogLine(opts.scope, event, fields));\n },\n warn(event, fields) {\n console.warn(formatLogLine(opts.scope, event, fields));\n },\n };\n}\n\nconst NOOP_LOGGER: ConnectorLogger = {\n info() {},\n warn() {},\n};\n\nexport function noopConnectorLogger(): ConnectorLogger {\n return NOOP_LOGGER;\n}\n","import {\n type HttpResponse,\n connectorUserAgent,\n parseEpoch,\n} from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n accessToken: z.object({ $secret: z.string() }).meta({\n label: 'Private App access token',\n description:\n 'HubSpot private app access token with read scopes for contacts, companies, deals, and marketing email. Create one at Settings → Integrations → Private Apps.',\n placeholder: 'pat-na1-...',\n secret: true,\n }),\n resources: z\n .array(\n z.enum([\n 'contacts',\n 'companies',\n 'deals',\n 'deal_events',\n 'email_campaigns',\n 'email_stats',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which HubSpot resources to sync. Omit to sync all resources. The access token only needs read scopes for the resources listed here.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'HubSpot',\n category: 'sales',\n brandColor: '#FF7A59',\n tagline:\n 'Sync CRM contacts, companies, and deals plus deal stage-change events and marketing email campaign stats from HubSpot.',\n vendor: {\n name: 'HubSpot',\n apiDocs: 'https://developers.hubspot.com/docs/api/overview',\n website: 'https://www.hubspot.com',\n },\n auth: {\n summary:\n 'A HubSpot private app access token with read scopes for the resources you sync (contacts, companies, deals, and marketing email).',\n setup: [\n 'In HubSpot, go to Settings → Integrations → Private Apps and create a private app.',\n 'Grant read scopes for the resources you intend to sync (CRM contacts, companies, deals, and marketing email).',\n 'Copy the generated access token (starts with `pat-`).',\n 'Store it as a secret and reference it from the connector config as `accessToken: secret(\"HUBSPOT_ACCESS_TOKEN\")`.',\n ],\n },\n rateLimit:\n 'HubSpot allows 100 requests / 10s; the Search API caps results at 10,000 per query.',\n limitations: [\n 'Deal stage-change events are rewritten on every sync because the deal list endpoint has no incremental `since` filter.',\n 'Marketing email campaign data comes from the legacy email campaigns API and is only available for marketing emails.',\n 'Very large CRM portfolios may not backfill in full because the Search API caps at 10,000 results per query.',\n ],\n});\n\nexport interface HubSpotSettings {\n resources?: readonly HubSpotResource[];\n}\n\n// ---------------------------------------------------------------------------\n// HubSpot API types\n// ---------------------------------------------------------------------------\n\ntype HubSpotProperties = Record<string, string | null | undefined>;\n\ninterface CrmRecord {\n id: string;\n properties: HubSpotProperties;\n createdAt: string;\n updatedAt: string;\n archived?: boolean;\n}\n\ninterface CrmSearchResponse {\n total?: number;\n results: CrmRecord[];\n paging?: { next?: { after?: string } };\n}\n\ninterface CrmListResponse {\n results: DealHistoryRecord[];\n paging?: { next?: { after?: string } };\n}\n\ninterface DealHistoryEntry {\n value?: string | null;\n timestamp?: string | null;\n sourceType?: string | null;\n}\n\ninterface DealHistoryRecord {\n id: string;\n propertiesWithHistory?: {\n dealstage?: DealHistoryEntry[];\n };\n}\n\ninterface CampaignListResponse {\n campaigns: Array<{ id: number | string }>;\n hasMore?: boolean;\n offset?: number | string;\n}\n\ninterface CampaignCounters {\n sent?: number;\n delivered?: number;\n open?: number;\n click?: number;\n bounce?: number;\n unsubscribed?: number;\n}\n\ninterface CampaignDetail {\n id: number | string;\n name?: string | null;\n subject?: string | null;\n fromName?: string | null;\n type?: string | null;\n lastProcessingFinishedAt?: number | string | null;\n numIncluded?: number | null;\n counters?: CampaignCounters;\n}\n\n// ---------------------------------------------------------------------------\n// Credentials\n// ---------------------------------------------------------------------------\n\nconst hubspotCredentials = {\n accessToken: {\n description: 'HubSpot private app access token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype HubSpotCredentials = typeof hubspotCredentials;\n\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\n// ---------------------------------------------------------------------------\n\nconst PHASE_ORDER = [\n 'contacts',\n 'companies',\n 'deals',\n 'deal_events',\n 'email_campaigns',\n 'email_stats',\n] as const;\n\ntype HubSpotPhase = (typeof PHASE_ORDER)[number];\n\nexport type HubSpotResource = HubSpotPhase;\n\nconst isHubSpotSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst BASE_URL = 'https://api.hubapi.com';\nconst SEARCH_LIMIT = 100;\nconst LIST_LIMIT = 100;\n\ntype CrmObjectPhase = 'contacts' | 'companies' | 'deals';\n\n// CRM object name + property names requested per search phase.\nconst SEARCH_PROPERTIES: Record<CrmObjectPhase, readonly string[]> = {\n contacts: [\n 'email',\n 'lifecyclestage',\n 'hs_lead_status',\n 'createdate',\n 'lastmodifieddate',\n 'hubspot_owner_id',\n ],\n companies: [\n 'name',\n 'domain',\n 'industry',\n 'createdate',\n 'lifecyclestage',\n 'hs_lastmodifieddate',\n ],\n deals: [\n 'dealname',\n 'dealstage',\n 'pipeline',\n 'amount',\n 'closedate',\n 'hubspot_owner_id',\n 'createdate',\n 'hs_lastmodifieddate',\n ],\n};\n\n// Property each CRM object stamps with its last-modified time, used both for\n// the incremental `since` filter and the entity `updated_at` fallback.\nconst MODIFIED_PROPERTY: Record<CrmObjectPhase, string> = {\n contacts: 'lastmodifieddate',\n companies: 'hs_lastmodifieddate',\n deals: 'hs_lastmodifieddate',\n};\n\nconst ENTITY_TYPE_BY_PHASE: Partial<Record<HubSpotPhase, string>> = {\n contacts: 'hubspot_contact',\n companies: 'hubspot_company',\n deals: 'hubspot_deal',\n email_campaigns: 'hubspot_email_campaign',\n};\n\nconst DEAL_STAGE_EVENT = 'hubspot_deal_stage_change';\nconst EMAIL_STATS_METRIC = 'hubspot_email_stats';\n\n// ---------------------------------------------------------------------------\n// Value helpers\n// ---------------------------------------------------------------------------\n\nfunction finiteNumberOrNull(value: string | null | undefined): number | null {\n if (value === null || value === undefined || value.trim() === '') {\n return null;\n }\n const n = Number(value);\n return Number.isFinite(n) ? n : null;\n}\n\nfunction counterValue(value: number | undefined): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Schemas — describe the per-resource API response shape consumed by request()\n// ---------------------------------------------------------------------------\n\nconst idString = z.string().min(1);\n\nconst contactProperties = z.object({\n email: z.string().nullish(),\n lifecyclestage: z.string().nullish(),\n hs_lead_status: z.string().nullish(),\n createdate: z.string().nullish(),\n lastmodifieddate: z.string().nullish(),\n hubspot_owner_id: z.string().nullish(),\n});\n\nconst companyProperties = z.object({\n name: z.string().nullish(),\n domain: z.string().nullish(),\n industry: z.string().nullish(),\n createdate: z.string().nullish(),\n lifecyclestage: z.string().nullish(),\n hs_lastmodifieddate: z.string().nullish(),\n});\n\nconst dealProperties = z.object({\n dealname: z.string().nullish(),\n dealstage: z.string().nullish(),\n pipeline: z.string().nullish(),\n amount: z.string().nullish(),\n closedate: z.string().nullish(),\n hubspot_owner_id: z.string().nullish(),\n createdate: z.string().nullish(),\n hs_lastmodifieddate: z.string().nullish(),\n});\n\nfunction crmRecordSchema(props: z.ZodType): z.ZodType {\n return z.object({\n id: idString,\n properties: props,\n createdAt: z.string(),\n updatedAt: z.string(),\n archived: z.boolean().optional(),\n });\n}\n\nconst dealHistoryRecordSchema = z.object({\n id: idString,\n propertiesWithHistory: z\n .object({\n dealstage: z\n .array(\n z.object({\n value: z.string().nullish(),\n timestamp: z.string().nullish(),\n sourceType: z.string().nullish(),\n }),\n )\n .optional(),\n })\n .optional(),\n});\n\nconst campaignDetailSchema = z.object({\n id: z.union([z.string(), z.number()]),\n name: z.string().nullish(),\n subject: z.string().nullish(),\n fromName: z.string().nullish(),\n type: z.string().nullish(),\n lastProcessingFinishedAt: z.union([z.string(), z.number()]).nullish(),\n numIncluded: z.number().nullish(),\n counters: z\n .object({\n sent: z.number().optional(),\n delivered: z.number().optional(),\n open: z.number().optional(),\n click: z.number().optional(),\n bounce: z.number().optional(),\n unsubscribed: z.number().optional(),\n })\n .optional(),\n});\n\nconst contactsSchema = z.array(crmRecordSchema(contactProperties));\nconst companiesSchema = z.array(crmRecordSchema(companyProperties));\nconst dealsSchema = z.array(crmRecordSchema(dealProperties));\nconst dealEventsSchema = z.array(dealHistoryRecordSchema);\nconst campaignsSchema = z.array(campaignDetailSchema);\n\nexport const hubspotResources = defineResources({\n hubspot_contact: {\n shape: 'entity',\n description:\n 'CRM contacts with email, lifecycle stage, lead status, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/contacts/search',\n responses: { contacts: contactsSchema },\n },\n hubspot_company: {\n shape: 'entity',\n description:\n 'CRM companies with name, domain, industry, lifecycle stage, and creation time.',\n endpoint: 'POST /crm/v3/objects/companies/search',\n responses: { companies: companiesSchema },\n },\n hubspot_deal: {\n shape: 'entity',\n description:\n 'CRM deals with name, stage, pipeline, amount, close date, owner, and creation time.',\n endpoint: 'POST /crm/v3/objects/deals/search',\n responses: { deals: dealsSchema },\n },\n hubspot_deal_stage_change: {\n shape: 'event',\n description:\n 'Deal stage-change events derived from deal property history, one event per stage transition.',\n endpoint: 'GET /crm/v3/objects/deals?propertiesWithHistory=dealstage',\n responses: { deal_events: dealEventsSchema },\n },\n hubspot_email_campaign: {\n shape: 'entity',\n description:\n 'Marketing email campaigns with name, subject, sender, type, send date, and recipient count.',\n endpoint: 'GET /email/public/v1/campaigns',\n responses: { email_campaigns: campaignsSchema },\n },\n hubspot_email_stats: {\n shape: 'metric',\n description:\n 'Per-campaign marketing email engagement stats (sent, delivered, opened, clicked, bounced, unsubscribed) timestamped at the campaign send time.',\n endpoint: 'GET /email/public/v1/campaigns/{id}',\n unit: 'emails',\n notes:\n 'One sample per campaign; value is the sent count, and every counter (delivered, opened, clicked, bounced, unsubscribed) is also exposed in attributes.',\n dimensions: [\n { name: 'campaignId', description: 'HubSpot campaign id.' },\n { name: 'campaignName', description: 'Campaign name.' },\n { name: 'delivered', description: 'Number of emails delivered.' },\n { name: 'opened', description: 'Number of emails opened.' },\n { name: 'clicked', description: 'Number of emails clicked.' },\n { name: 'bounced', description: 'Number of emails bounced.' },\n {\n name: 'unsubscribed',\n description: 'Number of recipients who unsubscribed.',\n },\n ],\n responses: { email_stats: campaignsSchema },\n },\n});\n\n// ---------------------------------------------------------------------------\n// HubSpotConnector\n// ---------------------------------------------------------------------------\n\nexport const id = 'hubspot';\n\nexport class HubSpotConnector extends BaseConnector<\n HubSpotSettings,\n HubSpotCredentials\n> {\n static readonly id = id;\n\n static readonly resources = hubspotResources;\n\n static readonly schemas = schemasFromResources(hubspotResources);\n\n static create(input: unknown, ctx?: ConnectorContext): HubSpotConnector {\n const parsed = configFields.parse(input);\n return new HubSpotConnector(\n { resources: parsed.resources },\n { accessToken: parsed.accessToken },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = hubspotCredentials;\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.accessToken}`,\n 'Content-Type': 'application/json',\n 'User-Agent': connectorUserAgent('hubspot'),\n };\n }\n\n private apiGet<T>(\n url: string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.get<T>(url, {\n resource,\n headers: this.buildHeaders(),\n signal,\n });\n }\n\n private apiPost<T>(\n url: string,\n resource: string,\n body: unknown,\n signal?: AbortSignal,\n ): Promise<HttpResponse<T>> {\n return this.post<T>(url, {\n resource,\n headers: this.buildHeaders(),\n body: JSON.stringify(body),\n signal,\n });\n }\n\n // -------------------------------------------------------------------------\n // CRM search phases (contacts / companies / deals)\n // -------------------------------------------------------------------------\n\n private buildSearchBody(\n phase: CrmObjectPhase,\n after: string | null,\n options: SyncOptions,\n ): Record<string, unknown> {\n const modifiedProperty = MODIFIED_PROPERTY[phase];\n const filterGroups: unknown[] = [];\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n filterGroups.push({\n filters: [\n {\n propertyName: modifiedProperty,\n operator: 'GTE',\n value: String(sinceMs),\n },\n ],\n });\n }\n }\n return {\n filterGroups,\n // Ascending modified-time keeps pagination stable while the `since`\n // filter trims the set upstream, so an incremental sync never scans\n // the whole object.\n sorts: [{ propertyName: modifiedProperty, direction: 'ASCENDING' }],\n properties: SEARCH_PROPERTIES[phase],\n limit: SEARCH_LIMIT,\n ...(after ? { after } : {}),\n };\n }\n\n private async fetchSearchPage(\n phase: CrmObjectPhase,\n after: string | null,\n options: SyncOptions,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const body = this.buildSearchBody(phase, after, options);\n const res = await this.apiPost<CrmSearchResponse>(\n `${BASE_URL}/crm/v3/objects/${phase}/search`,\n phase,\n body,\n signal,\n );\n return {\n items: res.body.results,\n next: res.body.paging?.next?.after ?? null,\n };\n }\n\n private async writeSearchPhase(\n storage: StorageHandle,\n phase: CrmObjectPhase,\n items: CrmRecord[],\n ): Promise<void> {\n for (const record of items) {\n const props = record.properties;\n const modifiedProperty = MODIFIED_PROPERTY[phase];\n const updatedAt =\n parseEpoch(record.updatedAt, 'iso') ??\n parseEpoch(props[modifiedProperty], 'ms') ??\n 0;\n\n let attributes: Record<string, string | number | null>;\n if (phase === 'contacts') {\n attributes = {\n email: props.email ?? null,\n lifecycleStage: props.lifecyclestage ?? null,\n leadStatus: props.hs_lead_status ?? null,\n ownerId: props.hubspot_owner_id ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n } else if (phase === 'companies') {\n attributes = {\n name: props.name ?? null,\n domain: props.domain ?? null,\n industry: props.industry ?? null,\n lifecycleStage: props.lifecyclestage ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n } else {\n attributes = {\n dealName: props.dealname ?? null,\n dealStage: props.dealstage ?? null,\n pipeline: props.pipeline ?? null,\n amount: finiteNumberOrNull(props.amount),\n closeDate: parseEpoch(props.closedate, 'ms'),\n ownerId: props.hubspot_owner_id ?? null,\n createdAt: parseEpoch(props.createdate, 'ms'),\n };\n }\n\n await storage.entity({\n type: ENTITY_TYPE_BY_PHASE[phase]!,\n id: record.id,\n attributes,\n updated_at: updatedAt,\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Deal stage-change events (deal property history)\n // -------------------------------------------------------------------------\n\n private async fetchDealHistoryPage(\n after: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const url = new URL(`${BASE_URL}/crm/v3/objects/deals`);\n url.searchParams.set('limit', String(LIST_LIMIT));\n url.searchParams.set('properties', 'dealstage');\n url.searchParams.set('propertiesWithHistory', 'dealstage');\n if (after) {\n url.searchParams.set('after', after);\n }\n const res = await this.apiGet<CrmListResponse>(\n url.toString(),\n 'deal_events',\n signal,\n );\n return {\n items: res.body.results,\n next: res.body.paging?.next?.after ?? null,\n };\n }\n\n private async writeDealEvents(\n storage: StorageHandle,\n items: DealHistoryRecord[],\n _options: SyncOptions,\n ): Promise<void> {\n for (const record of items) {\n const history = record.propertiesWithHistory?.dealstage ?? [];\n for (const entry of history) {\n const ts = parseEpoch(entry.timestamp, 'iso');\n if (ts === null) {\n continue;\n }\n await storage.event({\n name: DEAL_STAGE_EVENT,\n start_ts: ts,\n end_ts: null,\n attributes: {\n dealId: record.id,\n stage: entry.value ?? null,\n sourceType: entry.sourceType ?? null,\n },\n });\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Marketing email campaigns + stats (legacy email campaigns API)\n // -------------------------------------------------------------------------\n\n private async fetchCampaignDetail(\n id: number | string,\n resource: string,\n signal?: AbortSignal,\n ): Promise<CampaignDetail> {\n const res = await this.apiGet<CampaignDetail>(\n `${BASE_URL}/email/public/v1/campaigns/${id}`,\n resource,\n signal,\n );\n return res.body;\n }\n\n private async fetchCampaignsPage(\n phase: 'email_campaigns' | 'email_stats',\n after: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const url = new URL(`${BASE_URL}/email/public/v1/campaigns`);\n url.searchParams.set('limit', String(LIST_LIMIT));\n if (after) {\n url.searchParams.set('offset', after);\n }\n const listRes = await this.apiGet<CampaignListResponse>(\n url.toString(),\n `${phase}_list`,\n signal,\n );\n const { campaigns, hasMore, offset } = listRes.body;\n\n const details: CampaignDetail[] = [];\n for (const campaign of campaigns) {\n details.push(await this.fetchCampaignDetail(campaign.id, phase, signal));\n }\n\n const next =\n hasMore && offset !== undefined && offset !== null\n ? String(offset)\n : null;\n return { items: details, next };\n }\n\n private async writeEmailCampaigns(\n storage: StorageHandle,\n items: CampaignDetail[],\n ): Promise<void> {\n for (const detail of items) {\n const sentDate = parseEpoch(detail.lastProcessingFinishedAt, 'ms');\n await storage.entity({\n type: ENTITY_TYPE_BY_PHASE.email_campaigns!,\n id: String(detail.id),\n attributes: {\n name: detail.name ?? null,\n subject: detail.subject ?? null,\n fromName: detail.fromName ?? null,\n type: detail.type ?? null,\n sentDate,\n numIncluded: detail.numIncluded ?? null,\n },\n updated_at: sentDate ?? 0,\n });\n }\n }\n\n private async writeEmailStats(\n storage: StorageHandle,\n items: CampaignDetail[],\n ): Promise<void> {\n for (const detail of items) {\n const ts = parseEpoch(detail.lastProcessingFinishedAt, 'ms');\n if (ts === null) {\n continue;\n }\n const counters = detail.counters ?? {};\n const sent = counterValue(counters.sent);\n await storage.metric({\n name: EMAIL_STATS_METRIC,\n ts,\n value: sent,\n attributes: {\n campaignId: String(detail.id),\n campaignName: detail.name ?? null,\n sent,\n delivered: counterValue(counters.delivered),\n opened: counterValue(counters.open),\n clicked: counterValue(counters.click),\n bounced: counterValue(counters.bounce),\n unsubscribed: counterValue(counters.unsubscribed),\n },\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // Scope clearing (idempotency)\n // -------------------------------------------------------------------------\n\n private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: HubSpotPhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'deal_events') {\n // Events never upsert and the list endpoint has no `since` filter, so\n // every sync rewrites the requested window — clear in both modes.\n await storage.events([], { names: [DEAL_STAGE_EVENT] });\n return;\n }\n if (phase === 'email_stats') {\n await storage.metrics([], { names: [EMAIL_STATS_METRIC] });\n return;\n }\n // Entity phases upsert by id, so only a full backfill needs to drop stale\n // rows; incremental ticks just overwrite the records they touch.\n if (!isFull) {\n return;\n }\n const entityType = ENTITY_TYPE_BY_PHASE[phase];\n if (entityType) {\n await storage.entities([], { types: [entityType] });\n }\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: HubSpotPhase,\n items: unknown[],\n options: SyncOptions,\n ): Promise<void> {\n switch (phase) {\n case 'contacts':\n case 'companies':\n case 'deals':\n await this.writeSearchPhase(storage, phase, items as CrmRecord[]);\n return;\n case 'deal_events':\n await this.writeDealEvents(\n storage,\n items as DealHistoryRecord[],\n options,\n );\n return;\n case 'email_campaigns':\n await this.writeEmailCampaigns(storage, items as CampaignDetail[]);\n return;\n case 'email_stats':\n await this.writeEmailStats(storage, items as CampaignDetail[]);\n return;\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isHubSpotSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<HubSpotResource, HubSpotPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<HubSpotPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (phase, page, sig) => {\n if (\n phase === 'contacts' ||\n phase === 'companies' ||\n phase === 'deals'\n ) {\n return this.fetchSearchPage(phase, page, options, sig);\n }\n if (phase === 'deal_events') {\n return this.fetchDealHistoryPage(page, sig);\n }\n return this.fetchCampaignsPage(phase, page, sig);\n },\n writeBatch: async (phase, items, page) => {\n if (page === null) {\n await this.clearScopeOnFirstPage(storage, phase, isFull);\n }\n await this.writePhase(storage, phase, items, options);\n },\n });\n }\n}\n","import { HubSpotConnector } from './hubspot';\n\nexport {\n configFields,\n doc,\n HubSpotConnector,\n hubspotResources as resources,\n id,\n} from './hubspot';\nexport type { HubSpotSettings, HubSpotResource } from './hubspot';\nexport default HubSpotConnector;\n"],"mappings":"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","names":["id"]}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@rawdash/connector-hubspot",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Rawdash connector for HubSpot — CRM contacts, companies, deals, and marketing email performance",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "https://github.com/rawdash/rawdash.git",
@@ -23,7 +24,7 @@
23
24
  },
24
25
  "dependencies": {
25
26
  "zod": "^4.4.3",
26
- "@rawdash/core": "0.16.0"
27
+ "@rawdash/core": "0.17.0"
27
28
  },
28
29
  "devDependencies": {
29
30
  "fast-check": "^4.8.0",
@@ -31,7 +32,7 @@
31
32
  "typescript": "^5.7.2",
32
33
  "vitest": "^4.1.4",
33
34
  "@rawdash/connector-shared": "0.3.0",
34
- "@rawdash/connector-test-utils": "0.0.3"
35
+ "@rawdash/connector-test-utils": "0.0.4"
35
36
  },
36
37
  "scripts": {
37
38
  "build": "tsup",