@rawdash/connector-intercom 0.0.1
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/README.md +189 -0
- package/dist/index.d.ts +145 -0
- package/dist/index.js +537 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# @rawdash/connector-intercom
|
|
2
|
+
|
|
3
|
+
Rawdash connector for [Intercom](https://www.intercom.com) — syncs conversations, contacts, teams, and admins from the Intercom REST API into the six-shape storage model. Built for the support vertical: SMB teams using Intercom as their front-line inbox can chart conversation volume, queue depth, response latency, and tag/team distributions.
|
|
4
|
+
|
|
5
|
+
## Auth setup
|
|
6
|
+
|
|
7
|
+
The connector authenticates with an **access token** (personal access token or app access token).
|
|
8
|
+
|
|
9
|
+
1. In Intercom, open **Settings → Developers → Developer Hub**.
|
|
10
|
+
2. Either create a new app or open an existing one.
|
|
11
|
+
3. On the app's **Authentication** tab, copy the **Access token**.
|
|
12
|
+
4. Make sure the token has read access for the resources you want to sync (conversations, contacts, admins, teams).
|
|
13
|
+
|
|
14
|
+
Tokens issued from a Developer Hub app belong to the workspace that authorized the app; rotate them from the same screen if compromised.
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { secret } from '@rawdash/core';
|
|
20
|
+
|
|
21
|
+
const intercom = {
|
|
22
|
+
name: 'intercom',
|
|
23
|
+
connectorId: 'intercom',
|
|
24
|
+
config: {
|
|
25
|
+
accessToken: secret('INTERCOM_ACCESS_TOKEN'),
|
|
26
|
+
// region: 'eu', // optional, defaults to 'us'
|
|
27
|
+
// apiVersion: '2.11', // optional, defaults to '2.11'
|
|
28
|
+
// resources: ['conversations', 'admins'], // optional, defaults to all
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Register the connector class when mounting the engine:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { IntercomConnector } from '@rawdash/connector-intercom';
|
|
37
|
+
import { mountEngine } from '@rawdash/hono';
|
|
38
|
+
|
|
39
|
+
mountEngine(config, { connectorRegistry: { intercom: IntercomConnector } });
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Region
|
|
43
|
+
|
|
44
|
+
Set `region` to match the data residency of your Intercom workspace. The connector routes every request to the matching host:
|
|
45
|
+
|
|
46
|
+
| `region` | Host |
|
|
47
|
+
| -------- | ---------------------------- |
|
|
48
|
+
| `us` | `https://api.intercom.io` |
|
|
49
|
+
| `eu` | `https://api.eu.intercom.io` |
|
|
50
|
+
| `au` | `https://api.au.intercom.io` |
|
|
51
|
+
|
|
52
|
+
### Choosing resources
|
|
53
|
+
|
|
54
|
+
By default the connector syncs every supported resource. Pass `resources` to sync only a subset:
|
|
55
|
+
|
|
56
|
+
`admins`, `teams`, `contacts`, `conversations`, `conversation_events`
|
|
57
|
+
|
|
58
|
+
The access token only needs read scopes for the resources you list. `conversation_events` is derived from the same `/conversations/search` payload as `conversations`; disable it to skip the second pass.
|
|
59
|
+
|
|
60
|
+
### Configuration reference
|
|
61
|
+
|
|
62
|
+
| Field | Required | Description |
|
|
63
|
+
| ------------- | -------- | ------------------------------------------------------------------------------ |
|
|
64
|
+
| `accessToken` | yes | Intercom access token (secret). Bearer-authenticated. |
|
|
65
|
+
| `region` | no | Intercom region: `us`, `eu`, or `au`. Defaults to `us`. |
|
|
66
|
+
| `apiVersion` | no | Value sent in the `Intercom-Version` header (e.g. `2.11`). Defaults to `2.11`. |
|
|
67
|
+
| `resources` | no | Subset of resources to sync. Omit for all. |
|
|
68
|
+
|
|
69
|
+
### Example dashboard
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { defineConfig, defineDashboard, defineMetric } from '@rawdash/core';
|
|
73
|
+
|
|
74
|
+
export default defineConfig({
|
|
75
|
+
connectors: [intercom],
|
|
76
|
+
dashboards: {
|
|
77
|
+
support: defineDashboard({
|
|
78
|
+
widgets: {
|
|
79
|
+
open_conversations: {
|
|
80
|
+
kind: 'stat',
|
|
81
|
+
title: 'Open conversations',
|
|
82
|
+
metric: defineMetric({
|
|
83
|
+
connector: intercom,
|
|
84
|
+
shape: 'entity',
|
|
85
|
+
entityType: 'intercom_conversation',
|
|
86
|
+
fn: 'count',
|
|
87
|
+
filter: [{ field: 'state', op: 'eq', value: 'open' }],
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
conversations_today: {
|
|
91
|
+
kind: 'stat',
|
|
92
|
+
title: 'Conversations today',
|
|
93
|
+
metric: defineMetric({
|
|
94
|
+
connector: intercom,
|
|
95
|
+
shape: 'event',
|
|
96
|
+
name: 'intercom_conversation_state_change',
|
|
97
|
+
field: 'start_ts',
|
|
98
|
+
fn: 'count',
|
|
99
|
+
window: '1d',
|
|
100
|
+
filter: [{ field: 'transition', op: 'eq', value: 'created' }],
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
conversation_volume: {
|
|
104
|
+
kind: 'timeseries',
|
|
105
|
+
title: 'Conversation volume (7d)',
|
|
106
|
+
window: '7d',
|
|
107
|
+
metric: defineMetric({
|
|
108
|
+
connector: intercom,
|
|
109
|
+
shape: 'event',
|
|
110
|
+
name: 'intercom_conversation_state_change',
|
|
111
|
+
field: 'start_ts',
|
|
112
|
+
fn: 'count',
|
|
113
|
+
window: '7d',
|
|
114
|
+
filter: [{ field: 'transition', op: 'eq', value: 'created' }],
|
|
115
|
+
groupBy: { field: 'start_ts', granularity: 'day' },
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
conversations_by_team: {
|
|
119
|
+
kind: 'distribution',
|
|
120
|
+
title: 'Open conversations by team',
|
|
121
|
+
metric: defineMetric({
|
|
122
|
+
connector: intercom,
|
|
123
|
+
shape: 'entity',
|
|
124
|
+
entityType: 'intercom_conversation',
|
|
125
|
+
fn: 'count',
|
|
126
|
+
filter: [{ field: 'state', op: 'eq', value: 'open' }],
|
|
127
|
+
groupBy: { field: 'teamAssigneeId' },
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Data model
|
|
137
|
+
|
|
138
|
+
Timestamps stored in attributes are Unix milliseconds (Intercom returns Unix seconds; the connector multiplies by 1000 at write time).
|
|
139
|
+
|
|
140
|
+
| Storage shape | Entity/event type | Key attributes |
|
|
141
|
+
| ------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
142
|
+
| entity | `intercom_admin` | name, email, jobTitle, awayMode, hasInboxSeat |
|
|
143
|
+
| entity | `intercom_team` | name, adminCount |
|
|
144
|
+
| entity | `intercom_contact` | role, email, externalId, createdAt, lastSeenAt |
|
|
145
|
+
| entity | `intercom_conversation` | state, priority, adminAssigneeId, teamAssigneeId, createdAt, firstContactReplyAt, firstAdminReplyAt, snoozedUntil, count\*, tags[] |
|
|
146
|
+
| event | `intercom_conversation_state_change` | conversationId, transition (`created` / `assigned` / `closed` / `snoozed`), state, priority, adminAssigneeId, teamAssigneeId |
|
|
147
|
+
|
|
148
|
+
- **`intercom_conversation_state_change`** events are derived from the conversation's `statistics` block — one event per known transition timestamp (`created_at`, `last_assignment_at`, `last_close_at`, and `snoozed_until` when the conversation is currently snoozed). The event scope is cleared and rewritten on every sync, so each tick produces the latest snapshot of those transitions per conversation. Full per-part transition history requires a separate `/conversations/{id}` fetch and is intentionally out of scope for v0.1.
|
|
149
|
+
- **`intercom_conversation`** carries the rolled-up statistics (`countAssignments`, `countReopens`, `countConversationParts`) and tag names so distribution widgets can group by tag without a join.
|
|
150
|
+
|
|
151
|
+
## Schemas
|
|
152
|
+
|
|
153
|
+
`IntercomConnector.schemas` declares the Zod schema for each resource's raw API response (the admin / team / contact / conversation record arrays). Used by the cloud shape-drift pipeline to populate `connector_baselines`, and by the package's property tests.
|
|
154
|
+
|
|
155
|
+
## Sync behaviour
|
|
156
|
+
|
|
157
|
+
- **Backfill** (`mode: 'full'`): admins and teams come from `GET /admins` / `GET /teams` in a single page. Contacts and conversations stream through `POST /contacts/search` / `POST /conversations/search` with no query filter, sorted by `updated_at` ascending and paginated with the API's `starting_after` cursor.
|
|
158
|
+
- **Incremental** (`mode: 'latest'`): contact and conversation searches add a `query: { field: 'updated_at', operator: '>', value: <since> }` clause (Unix seconds), so only records modified since the last sync are returned. Entity phases upsert by id (no scope clear); `conversation_events` always clears and rewrites its scope.
|
|
159
|
+
- **Resumable**: each search phase yields a `starting_after` string cursor on abort, so an interrupted sync resumes from the same page. Admins and teams are single-shot.
|
|
160
|
+
- **Rate limits**: Intercom enforces per-app and per-workspace limits (the default is ~1000 requests/minute; see [Intercom's rate-limiting docs](https://developers.intercom.com/docs/references/rest-api/errors/rate-limiting) for current values) and signals quota state through the `X-RateLimit-*` response headers — notably `X-RateLimit-Reset`, a UNIX timestamp for when the window resets. On `429` the shared HTTP client backs off and retries, preferring `X-RateLimit-Reset` when present.
|
|
161
|
+
|
|
162
|
+
## Registering in the MCP server
|
|
163
|
+
|
|
164
|
+
To make the connector available via the `add_connector` MCP tool, include it in `connectorFactories`:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { IntercomConnector, configFields } from '@rawdash/connector-intercom';
|
|
168
|
+
|
|
169
|
+
createMcpServer({
|
|
170
|
+
// ...
|
|
171
|
+
connectorFactories: [
|
|
172
|
+
{
|
|
173
|
+
id: 'intercom',
|
|
174
|
+
configFields,
|
|
175
|
+
create: IntercomConnector.create,
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Property tests
|
|
182
|
+
|
|
183
|
+
`admins`, `teams`, `contacts`, and `conversations` have fast-check property tests under `src/property.test.ts` that generate synthetic API payloads from each resource's Zod schema, run them through `connector.sync()` against an `InMemoryStorage`, and assert universal invariants (non-empty ids, finite timestamps, no `undefined` in storage, no thrown errors) plus per-resource entity counts. State-change event mapping and the `since`/cursor wiring are covered by example-driven unit tests in `src/intercom.test.ts`.
|
|
184
|
+
|
|
185
|
+
## Out of scope
|
|
186
|
+
|
|
187
|
+
- Conversation message bodies and per-part transcripts — not dashboard-shaped.
|
|
188
|
+
- Help Center articles, knowledge base content, and outbound campaigns — covered by separate Intercom APIs that don't aggregate well.
|
|
189
|
+
- Full per-part state-transition history — would require an extra `GET /conversations/{id}` call per conversation and is deferred.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult } from '@rawdash/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
declare const configFields: z.ZodObject<{
|
|
5
|
+
accessToken: z.ZodObject<{
|
|
6
|
+
$secret: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
apiVersion: z.ZodDefault<z.ZodString>;
|
|
9
|
+
region: z.ZodDefault<z.ZodEnum<{
|
|
10
|
+
us: "us";
|
|
11
|
+
eu: "eu";
|
|
12
|
+
au: "au";
|
|
13
|
+
}>>;
|
|
14
|
+
resources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
15
|
+
admins: "admins";
|
|
16
|
+
teams: "teams";
|
|
17
|
+
contacts: "contacts";
|
|
18
|
+
conversations: "conversations";
|
|
19
|
+
conversation_events: "conversation_events";
|
|
20
|
+
}>>>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
interface IntercomSettings {
|
|
23
|
+
apiVersion: string;
|
|
24
|
+
region: 'us' | 'eu' | 'au';
|
|
25
|
+
resources?: readonly IntercomResource[];
|
|
26
|
+
}
|
|
27
|
+
declare const intercomCredentials: {
|
|
28
|
+
accessToken: {
|
|
29
|
+
description: string;
|
|
30
|
+
auth: "required";
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
type IntercomCredentials = typeof intercomCredentials;
|
|
34
|
+
declare const PHASE_ORDER: readonly ["admins", "teams", "contacts", "conversations", "conversation_events"];
|
|
35
|
+
type IntercomPhase = (typeof PHASE_ORDER)[number];
|
|
36
|
+
type IntercomResource = IntercomPhase;
|
|
37
|
+
declare class IntercomConnector extends BaseConnector<IntercomSettings, IntercomCredentials> {
|
|
38
|
+
static readonly id = "intercom";
|
|
39
|
+
static readonly schemas: {
|
|
40
|
+
readonly admins: z.ZodArray<z.ZodObject<{
|
|
41
|
+
id: z.ZodString;
|
|
42
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
43
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
job_title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
45
|
+
away_mode_enabled: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
46
|
+
has_inbox_seat: z.ZodOptional<z.ZodNullable<z.ZodBoolean>>;
|
|
47
|
+
}, z.core.$strip>>;
|
|
48
|
+
readonly teams: z.ZodArray<z.ZodObject<{
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
51
|
+
admin_ids: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNumber>>>;
|
|
52
|
+
}, z.core.$strip>>;
|
|
53
|
+
readonly contacts: z.ZodArray<z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
role: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
56
|
+
email: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
external_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
created_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
59
|
+
updated_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
60
|
+
last_seen_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
61
|
+
}, z.core.$strip>>;
|
|
62
|
+
readonly conversations: z.ZodArray<z.ZodObject<{
|
|
63
|
+
id: z.ZodString;
|
|
64
|
+
state: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
65
|
+
priority: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
66
|
+
admin_assignee_id: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
67
|
+
team_assignee_id: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
68
|
+
created_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
69
|
+
updated_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
70
|
+
snoozed_until: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
71
|
+
tags: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
72
|
+
tags: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
73
|
+
id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
74
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
75
|
+
}, z.core.$strip>>>>;
|
|
76
|
+
}, z.core.$strip>>>;
|
|
77
|
+
statistics: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
78
|
+
first_contact_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
79
|
+
first_admin_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
80
|
+
last_assignment_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
81
|
+
last_admin_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
82
|
+
last_contact_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
83
|
+
last_close_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
84
|
+
count_assignments: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
85
|
+
count_reopens: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
86
|
+
count_conversation_parts: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
87
|
+
}, z.core.$strip>>>;
|
|
88
|
+
}, z.core.$strip>>;
|
|
89
|
+
readonly conversation_events: z.ZodArray<z.ZodObject<{
|
|
90
|
+
id: z.ZodString;
|
|
91
|
+
state: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
92
|
+
priority: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
93
|
+
admin_assignee_id: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
94
|
+
team_assignee_id: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
95
|
+
created_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
96
|
+
updated_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
97
|
+
snoozed_until: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
98
|
+
tags: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
99
|
+
tags: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
100
|
+
id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
101
|
+
name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
102
|
+
}, z.core.$strip>>>>;
|
|
103
|
+
}, z.core.$strip>>>;
|
|
104
|
+
statistics: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
105
|
+
first_contact_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
106
|
+
first_admin_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
107
|
+
last_assignment_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
108
|
+
last_admin_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
109
|
+
last_contact_reply_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
110
|
+
last_close_at: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
111
|
+
count_assignments: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
112
|
+
count_reopens: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
113
|
+
count_conversation_parts: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
114
|
+
}, z.core.$strip>>>;
|
|
115
|
+
}, z.core.$strip>>;
|
|
116
|
+
};
|
|
117
|
+
static create(input: unknown, ctx?: ConnectorContext): IntercomConnector;
|
|
118
|
+
readonly id = "intercom";
|
|
119
|
+
readonly credentials: {
|
|
120
|
+
accessToken: {
|
|
121
|
+
description: string;
|
|
122
|
+
auth: "required";
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
private get baseUrl();
|
|
126
|
+
private buildHeaders;
|
|
127
|
+
private apiGet;
|
|
128
|
+
private apiPost;
|
|
129
|
+
private fetchAdmins;
|
|
130
|
+
private writeAdmins;
|
|
131
|
+
private fetchTeams;
|
|
132
|
+
private writeTeams;
|
|
133
|
+
private buildContactSearchBody;
|
|
134
|
+
private fetchContacts;
|
|
135
|
+
private writeContacts;
|
|
136
|
+
private buildConversationSearchBody;
|
|
137
|
+
private fetchConversations;
|
|
138
|
+
private writeConversations;
|
|
139
|
+
private writeConversationEvents;
|
|
140
|
+
private clearScopeOnFirstPage;
|
|
141
|
+
private writePhase;
|
|
142
|
+
sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { IntercomConnector, type IntercomResource, type IntercomSettings, configFields, IntercomConnector as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
// ../../connector-shared/dist/index.js
|
|
2
|
+
var HTTP_CLIENT_VERSION = "0.0.0";
|
|
3
|
+
var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
4
|
+
function connectorUserAgent(connectorId) {
|
|
5
|
+
return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
6
|
+
}
|
|
7
|
+
function parseEpoch(value, unit) {
|
|
8
|
+
if (value === null || value === void 0) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (unit === "iso") {
|
|
12
|
+
if (typeof value !== "string") {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const ms = new Date(value).getTime();
|
|
16
|
+
return Number.isFinite(ms) ? ms : null;
|
|
17
|
+
}
|
|
18
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
22
|
+
if (!Number.isFinite(n)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const result = unit === "s" ? n * 1e3 : n;
|
|
26
|
+
return Number.isFinite(result) ? result : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/intercom.ts
|
|
30
|
+
import {
|
|
31
|
+
BaseConnector,
|
|
32
|
+
defineConfigFields,
|
|
33
|
+
makeChunkedCursorGuard,
|
|
34
|
+
paginateChunked,
|
|
35
|
+
selectActivePhases
|
|
36
|
+
} from "@rawdash/core";
|
|
37
|
+
import { z } from "zod";
|
|
38
|
+
var configFields = defineConfigFields(
|
|
39
|
+
z.object({
|
|
40
|
+
accessToken: z.object({ $secret: z.string() }).meta({
|
|
41
|
+
label: "Access token",
|
|
42
|
+
description: "Intercom access token (personal or app) with read access to conversations, contacts, teams, and admins. Generate one at Settings \u2192 Developers \u2192 Developer Hub \u2192 Authentication.",
|
|
43
|
+
placeholder: "dG9rOj...",
|
|
44
|
+
secret: true
|
|
45
|
+
}),
|
|
46
|
+
apiVersion: z.string().trim().regex(
|
|
47
|
+
/^\d+\.\d+$/,
|
|
48
|
+
'Use a numeric Intercom API version like "2.11" (no leading "v").'
|
|
49
|
+
).default("2.11").meta({
|
|
50
|
+
label: "Intercom API version",
|
|
51
|
+
description: "Value sent in the Intercom-Version header. Defaults to 2.11 \u2014 pin a specific version here when upgrading deliberately.",
|
|
52
|
+
placeholder: "2.11"
|
|
53
|
+
}),
|
|
54
|
+
region: z.enum(["us", "eu", "au"]).default("us").meta({
|
|
55
|
+
label: "Region",
|
|
56
|
+
description: "Intercom region of your workspace. Selects the API host: us \u2192 api.intercom.io, eu \u2192 api.eu.intercom.io, au \u2192 api.au.intercom.io.",
|
|
57
|
+
placeholder: "us"
|
|
58
|
+
}),
|
|
59
|
+
resources: z.array(
|
|
60
|
+
z.enum([
|
|
61
|
+
"admins",
|
|
62
|
+
"teams",
|
|
63
|
+
"contacts",
|
|
64
|
+
"conversations",
|
|
65
|
+
"conversation_events"
|
|
66
|
+
])
|
|
67
|
+
).nonempty().optional().meta({
|
|
68
|
+
label: "Resources",
|
|
69
|
+
description: "Which Intercom resources to sync. Omit to sync all of them. The access token only needs read scopes for the resources listed here."
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
var intercomCredentials = {
|
|
74
|
+
accessToken: {
|
|
75
|
+
description: "Intercom access token",
|
|
76
|
+
auth: "required"
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var PHASE_ORDER = [
|
|
80
|
+
"admins",
|
|
81
|
+
"teams",
|
|
82
|
+
"contacts",
|
|
83
|
+
"conversations",
|
|
84
|
+
"conversation_events"
|
|
85
|
+
];
|
|
86
|
+
var isIntercomSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
|
|
87
|
+
var SEARCH_PAGE_SIZE = 150;
|
|
88
|
+
var ADMIN_ENTITY = "intercom_admin";
|
|
89
|
+
var TEAM_ENTITY = "intercom_team";
|
|
90
|
+
var CONTACT_ENTITY = "intercom_contact";
|
|
91
|
+
var CONVERSATION_ENTITY = "intercom_conversation";
|
|
92
|
+
var CONVERSATION_STATE_EVENT = "intercom_conversation_state_change";
|
|
93
|
+
var REGION_HOSTS = {
|
|
94
|
+
us: "https://api.intercom.io",
|
|
95
|
+
eu: "https://api.eu.intercom.io",
|
|
96
|
+
au: "https://api.au.intercom.io"
|
|
97
|
+
};
|
|
98
|
+
var idString = z.string().min(1);
|
|
99
|
+
var adminSchema = z.object({
|
|
100
|
+
id: idString,
|
|
101
|
+
name: z.string().nullish(),
|
|
102
|
+
email: z.string().nullish(),
|
|
103
|
+
job_title: z.string().nullish(),
|
|
104
|
+
away_mode_enabled: z.boolean().nullish(),
|
|
105
|
+
has_inbox_seat: z.boolean().nullish()
|
|
106
|
+
});
|
|
107
|
+
var teamSchema = z.object({
|
|
108
|
+
id: idString,
|
|
109
|
+
name: z.string().nullish(),
|
|
110
|
+
admin_ids: z.array(z.number()).nullish()
|
|
111
|
+
});
|
|
112
|
+
var contactSchema = z.object({
|
|
113
|
+
id: idString,
|
|
114
|
+
role: z.string().nullish(),
|
|
115
|
+
email: z.string().nullish(),
|
|
116
|
+
external_id: z.string().nullish(),
|
|
117
|
+
created_at: z.number().nullish(),
|
|
118
|
+
updated_at: z.number().nullish(),
|
|
119
|
+
last_seen_at: z.number().nullish()
|
|
120
|
+
});
|
|
121
|
+
var conversationSchema = z.object({
|
|
122
|
+
id: idString,
|
|
123
|
+
state: z.string().nullish(),
|
|
124
|
+
priority: z.string().nullish(),
|
|
125
|
+
admin_assignee_id: z.union([z.number(), z.string()]).nullish(),
|
|
126
|
+
team_assignee_id: z.union([z.number(), z.string()]).nullish(),
|
|
127
|
+
created_at: z.number().nullish(),
|
|
128
|
+
updated_at: z.number().nullish(),
|
|
129
|
+
snoozed_until: z.number().nullish(),
|
|
130
|
+
tags: z.object({
|
|
131
|
+
tags: z.array(
|
|
132
|
+
z.object({ id: z.string().nullish(), name: z.string().nullish() })
|
|
133
|
+
).nullish()
|
|
134
|
+
}).nullish(),
|
|
135
|
+
statistics: z.object({
|
|
136
|
+
first_contact_reply_at: z.number().nullish(),
|
|
137
|
+
first_admin_reply_at: z.number().nullish(),
|
|
138
|
+
last_assignment_at: z.number().nullish(),
|
|
139
|
+
last_admin_reply_at: z.number().nullish(),
|
|
140
|
+
last_contact_reply_at: z.number().nullish(),
|
|
141
|
+
last_close_at: z.number().nullish(),
|
|
142
|
+
count_assignments: z.number().nullish(),
|
|
143
|
+
count_reopens: z.number().nullish(),
|
|
144
|
+
count_conversation_parts: z.number().nullish()
|
|
145
|
+
}).nullish()
|
|
146
|
+
});
|
|
147
|
+
function assigneeIdOrNull(value) {
|
|
148
|
+
if (value === null || value === void 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const s = String(value);
|
|
152
|
+
return s === "" || s === "0" ? null : s;
|
|
153
|
+
}
|
|
154
|
+
function tagNames(tags) {
|
|
155
|
+
const list = tags?.tags ?? [];
|
|
156
|
+
const names = [];
|
|
157
|
+
for (const tag of list) {
|
|
158
|
+
if (tag && typeof tag.name === "string" && tag.name !== "") {
|
|
159
|
+
names.push(tag.name);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return names;
|
|
163
|
+
}
|
|
164
|
+
function epochSecToMs(value) {
|
|
165
|
+
return parseEpoch(value ?? null, "s");
|
|
166
|
+
}
|
|
167
|
+
function epochSecToMsOrZero(value) {
|
|
168
|
+
return epochSecToMs(value) ?? 0;
|
|
169
|
+
}
|
|
170
|
+
var IntercomConnector = class _IntercomConnector extends BaseConnector {
|
|
171
|
+
static id = "intercom";
|
|
172
|
+
static schemas = {
|
|
173
|
+
admins: z.array(adminSchema),
|
|
174
|
+
teams: z.array(teamSchema),
|
|
175
|
+
contacts: z.array(contactSchema),
|
|
176
|
+
conversations: z.array(conversationSchema),
|
|
177
|
+
conversation_events: z.array(conversationSchema)
|
|
178
|
+
};
|
|
179
|
+
static create(input, ctx) {
|
|
180
|
+
const parsed = configFields.parse(input);
|
|
181
|
+
return new _IntercomConnector(
|
|
182
|
+
{
|
|
183
|
+
apiVersion: parsed.apiVersion,
|
|
184
|
+
region: parsed.region,
|
|
185
|
+
resources: parsed.resources
|
|
186
|
+
},
|
|
187
|
+
{ accessToken: parsed.accessToken },
|
|
188
|
+
ctx
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
id = "intercom";
|
|
192
|
+
credentials = intercomCredentials;
|
|
193
|
+
get baseUrl() {
|
|
194
|
+
return REGION_HOSTS[this.settings.region];
|
|
195
|
+
}
|
|
196
|
+
buildHeaders() {
|
|
197
|
+
return {
|
|
198
|
+
Authorization: `Bearer ${this.creds.accessToken}`,
|
|
199
|
+
"Intercom-Version": this.settings.apiVersion,
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
Accept: "application/json",
|
|
202
|
+
"User-Agent": connectorUserAgent("intercom")
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
apiGet(url, resource, signal) {
|
|
206
|
+
return this.get(url, {
|
|
207
|
+
resource,
|
|
208
|
+
headers: this.buildHeaders(),
|
|
209
|
+
signal
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
apiPost(url, resource, body, signal) {
|
|
213
|
+
return this.post(url, {
|
|
214
|
+
resource,
|
|
215
|
+
headers: this.buildHeaders(),
|
|
216
|
+
body: JSON.stringify(body),
|
|
217
|
+
signal
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// -------------------------------------------------------------------------
|
|
221
|
+
// admins — GET /admins (single page, not paginated)
|
|
222
|
+
// -------------------------------------------------------------------------
|
|
223
|
+
async fetchAdmins(page, signal) {
|
|
224
|
+
if (page !== null) {
|
|
225
|
+
return { items: [], next: null };
|
|
226
|
+
}
|
|
227
|
+
const res = await this.apiGet(
|
|
228
|
+
`${this.baseUrl}/admins`,
|
|
229
|
+
"admins",
|
|
230
|
+
signal
|
|
231
|
+
);
|
|
232
|
+
return { items: res.body.admins ?? [], next: null };
|
|
233
|
+
}
|
|
234
|
+
async writeAdmins(storage, items) {
|
|
235
|
+
for (const admin of items) {
|
|
236
|
+
await storage.entity({
|
|
237
|
+
type: ADMIN_ENTITY,
|
|
238
|
+
id: admin.id,
|
|
239
|
+
attributes: {
|
|
240
|
+
name: admin.name ?? null,
|
|
241
|
+
email: admin.email ?? null,
|
|
242
|
+
jobTitle: admin.job_title ?? null,
|
|
243
|
+
awayMode: admin.away_mode_enabled ?? null,
|
|
244
|
+
hasInboxSeat: admin.has_inbox_seat ?? null
|
|
245
|
+
},
|
|
246
|
+
// Admins have no updatedAt in the API; stamp with sync time so newer
|
|
247
|
+
// syncs win on conflict.
|
|
248
|
+
updated_at: Date.now()
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// -------------------------------------------------------------------------
|
|
253
|
+
// teams — GET /teams (single page, not paginated)
|
|
254
|
+
// -------------------------------------------------------------------------
|
|
255
|
+
async fetchTeams(page, signal) {
|
|
256
|
+
if (page !== null) {
|
|
257
|
+
return { items: [], next: null };
|
|
258
|
+
}
|
|
259
|
+
const res = await this.apiGet(
|
|
260
|
+
`${this.baseUrl}/teams`,
|
|
261
|
+
"teams",
|
|
262
|
+
signal
|
|
263
|
+
);
|
|
264
|
+
return { items: res.body.teams ?? [], next: null };
|
|
265
|
+
}
|
|
266
|
+
async writeTeams(storage, items) {
|
|
267
|
+
for (const team of items) {
|
|
268
|
+
await storage.entity({
|
|
269
|
+
type: TEAM_ENTITY,
|
|
270
|
+
id: team.id,
|
|
271
|
+
attributes: {
|
|
272
|
+
name: team.name ?? null,
|
|
273
|
+
adminCount: team.admin_ids?.length ?? 0
|
|
274
|
+
},
|
|
275
|
+
updated_at: Date.now()
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// -------------------------------------------------------------------------
|
|
280
|
+
// contacts — POST /contacts/search (cursor-paginated)
|
|
281
|
+
// -------------------------------------------------------------------------
|
|
282
|
+
buildContactSearchBody(startingAfter, options) {
|
|
283
|
+
const sinceSec = sinceUnixSec(options);
|
|
284
|
+
const body = {
|
|
285
|
+
pagination: {
|
|
286
|
+
per_page: SEARCH_PAGE_SIZE,
|
|
287
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
288
|
+
},
|
|
289
|
+
sort: { field: "updated_at", order: "ascending" }
|
|
290
|
+
};
|
|
291
|
+
if (sinceSec !== null) {
|
|
292
|
+
body["query"] = {
|
|
293
|
+
field: "updated_at",
|
|
294
|
+
operator: ">",
|
|
295
|
+
value: sinceSec
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return body;
|
|
299
|
+
}
|
|
300
|
+
async fetchContacts(page, options, signal) {
|
|
301
|
+
const res = await this.apiPost(
|
|
302
|
+
`${this.baseUrl}/contacts/search`,
|
|
303
|
+
"contacts",
|
|
304
|
+
this.buildContactSearchBody(page, options),
|
|
305
|
+
signal
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
items: res.body.data ?? [],
|
|
309
|
+
next: res.body.pages?.next?.starting_after ?? null
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async writeContacts(storage, items) {
|
|
313
|
+
for (const contact of items) {
|
|
314
|
+
await storage.entity({
|
|
315
|
+
type: CONTACT_ENTITY,
|
|
316
|
+
id: contact.id,
|
|
317
|
+
attributes: {
|
|
318
|
+
role: contact.role ?? null,
|
|
319
|
+
email: contact.email ?? null,
|
|
320
|
+
externalId: contact.external_id ?? null,
|
|
321
|
+
createdAt: epochSecToMs(contact.created_at),
|
|
322
|
+
lastSeenAt: epochSecToMs(contact.last_seen_at)
|
|
323
|
+
},
|
|
324
|
+
updated_at: epochSecToMsOrZero(
|
|
325
|
+
contact.updated_at ?? contact.created_at
|
|
326
|
+
)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// -------------------------------------------------------------------------
|
|
331
|
+
// conversations — POST /conversations/search (entities)
|
|
332
|
+
// -------------------------------------------------------------------------
|
|
333
|
+
buildConversationSearchBody(startingAfter, options) {
|
|
334
|
+
const sinceSec = sinceUnixSec(options);
|
|
335
|
+
const body = {
|
|
336
|
+
pagination: {
|
|
337
|
+
per_page: SEARCH_PAGE_SIZE,
|
|
338
|
+
...startingAfter ? { starting_after: startingAfter } : {}
|
|
339
|
+
},
|
|
340
|
+
sort: { field: "updated_at", order: "ascending" }
|
|
341
|
+
};
|
|
342
|
+
if (sinceSec !== null) {
|
|
343
|
+
body["query"] = {
|
|
344
|
+
field: "updated_at",
|
|
345
|
+
operator: ">",
|
|
346
|
+
value: sinceSec
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return body;
|
|
350
|
+
}
|
|
351
|
+
async fetchConversations(page, resource, options, signal) {
|
|
352
|
+
const fetchOptions = resource === "conversation_events" ? { ...options, since: void 0 } : options;
|
|
353
|
+
const res = await this.apiPost(
|
|
354
|
+
`${this.baseUrl}/conversations/search`,
|
|
355
|
+
resource,
|
|
356
|
+
this.buildConversationSearchBody(page, fetchOptions),
|
|
357
|
+
signal
|
|
358
|
+
);
|
|
359
|
+
return {
|
|
360
|
+
items: res.body.conversations ?? [],
|
|
361
|
+
next: res.body.pages?.next?.starting_after ?? null
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
async writeConversations(storage, items) {
|
|
365
|
+
for (const conv of items) {
|
|
366
|
+
const stats = conv.statistics ?? {};
|
|
367
|
+
const attributes = {
|
|
368
|
+
state: conv.state ?? null,
|
|
369
|
+
priority: conv.priority ?? null,
|
|
370
|
+
adminAssigneeId: assigneeIdOrNull(conv.admin_assignee_id),
|
|
371
|
+
teamAssigneeId: assigneeIdOrNull(conv.team_assignee_id),
|
|
372
|
+
createdAt: epochSecToMs(conv.created_at),
|
|
373
|
+
firstContactReplyAt: epochSecToMs(stats.first_contact_reply_at),
|
|
374
|
+
firstAdminReplyAt: epochSecToMs(stats.first_admin_reply_at),
|
|
375
|
+
snoozedUntil: epochSecToMs(conv.snoozed_until),
|
|
376
|
+
countAssignments: stats.count_assignments ?? null,
|
|
377
|
+
countReopens: stats.count_reopens ?? null,
|
|
378
|
+
countConversationParts: stats.count_conversation_parts ?? null,
|
|
379
|
+
tags: tagNames(conv.tags)
|
|
380
|
+
};
|
|
381
|
+
await storage.entity({
|
|
382
|
+
type: CONVERSATION_ENTITY,
|
|
383
|
+
id: conv.id,
|
|
384
|
+
attributes,
|
|
385
|
+
updated_at: epochSecToMsOrZero(conv.updated_at ?? conv.created_at)
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// -------------------------------------------------------------------------
|
|
390
|
+
// conversation_events — derived from the same /conversations/search payload
|
|
391
|
+
// -------------------------------------------------------------------------
|
|
392
|
+
async writeConversationEvents(storage, items) {
|
|
393
|
+
for (const conv of items) {
|
|
394
|
+
const stats = conv.statistics ?? {};
|
|
395
|
+
const baseAttrs = {
|
|
396
|
+
conversationId: conv.id,
|
|
397
|
+
state: conv.state ?? null,
|
|
398
|
+
priority: conv.priority ?? null,
|
|
399
|
+
teamAssigneeId: assigneeIdOrNull(conv.team_assignee_id),
|
|
400
|
+
adminAssigneeId: assigneeIdOrNull(conv.admin_assignee_id)
|
|
401
|
+
};
|
|
402
|
+
const createdMs = epochSecToMs(conv.created_at);
|
|
403
|
+
if (createdMs !== null) {
|
|
404
|
+
await storage.event({
|
|
405
|
+
name: CONVERSATION_STATE_EVENT,
|
|
406
|
+
start_ts: createdMs,
|
|
407
|
+
end_ts: null,
|
|
408
|
+
attributes: { ...baseAttrs, transition: "created" }
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const assignedMs = epochSecToMs(stats.last_assignment_at);
|
|
412
|
+
if (assignedMs !== null) {
|
|
413
|
+
await storage.event({
|
|
414
|
+
name: CONVERSATION_STATE_EVENT,
|
|
415
|
+
start_ts: assignedMs,
|
|
416
|
+
end_ts: null,
|
|
417
|
+
attributes: { ...baseAttrs, transition: "assigned" }
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const closedMs = epochSecToMs(stats.last_close_at);
|
|
421
|
+
if (closedMs !== null) {
|
|
422
|
+
await storage.event({
|
|
423
|
+
name: CONVERSATION_STATE_EVENT,
|
|
424
|
+
start_ts: closedMs,
|
|
425
|
+
end_ts: null,
|
|
426
|
+
attributes: { ...baseAttrs, transition: "closed" }
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
const snoozedMs = epochSecToMs(conv.snoozed_until);
|
|
430
|
+
if (snoozedMs !== null && conv.state === "snoozed") {
|
|
431
|
+
await storage.event({
|
|
432
|
+
name: CONVERSATION_STATE_EVENT,
|
|
433
|
+
start_ts: snoozedMs,
|
|
434
|
+
end_ts: null,
|
|
435
|
+
attributes: { ...baseAttrs, transition: "snoozed" }
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
// -------------------------------------------------------------------------
|
|
441
|
+
// Scope clearing (idempotency)
|
|
442
|
+
// -------------------------------------------------------------------------
|
|
443
|
+
async clearScopeOnFirstPage(storage, phase, isFull) {
|
|
444
|
+
if (phase === "conversation_events") {
|
|
445
|
+
await storage.events([], { names: [CONVERSATION_STATE_EVENT] });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!isFull) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const entityType = ENTITY_TYPE_BY_PHASE[phase];
|
|
452
|
+
if (entityType) {
|
|
453
|
+
await storage.entities([], { types: [entityType] });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async writePhase(storage, phase, items) {
|
|
457
|
+
switch (phase) {
|
|
458
|
+
case "admins":
|
|
459
|
+
await this.writeAdmins(storage, items);
|
|
460
|
+
return;
|
|
461
|
+
case "teams":
|
|
462
|
+
await this.writeTeams(storage, items);
|
|
463
|
+
return;
|
|
464
|
+
case "contacts":
|
|
465
|
+
await this.writeContacts(storage, items);
|
|
466
|
+
return;
|
|
467
|
+
case "conversations":
|
|
468
|
+
await this.writeConversations(storage, items);
|
|
469
|
+
return;
|
|
470
|
+
case "conversation_events":
|
|
471
|
+
await this.writeConversationEvents(
|
|
472
|
+
storage,
|
|
473
|
+
items
|
|
474
|
+
);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async sync(options, storage, signal) {
|
|
479
|
+
const cursor = isIntercomSyncCursor(options.cursor) ? options.cursor : void 0;
|
|
480
|
+
const isFull = options.mode === "full";
|
|
481
|
+
const phases = selectActivePhases(
|
|
482
|
+
(r) => r,
|
|
483
|
+
PHASE_ORDER,
|
|
484
|
+
this.settings.resources
|
|
485
|
+
);
|
|
486
|
+
return paginateChunked({
|
|
487
|
+
phases,
|
|
488
|
+
cursor,
|
|
489
|
+
signal,
|
|
490
|
+
logger: this.logger,
|
|
491
|
+
fetchPage: async (phase, page, sig) => {
|
|
492
|
+
switch (phase) {
|
|
493
|
+
case "admins":
|
|
494
|
+
return this.fetchAdmins(page, sig);
|
|
495
|
+
case "teams":
|
|
496
|
+
return this.fetchTeams(page, sig);
|
|
497
|
+
case "contacts":
|
|
498
|
+
return this.fetchContacts(page, options, sig);
|
|
499
|
+
case "conversations":
|
|
500
|
+
case "conversation_events":
|
|
501
|
+
return this.fetchConversations(page, phase, options, sig);
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
writeBatch: async (phase, items, page) => {
|
|
505
|
+
if (page === null) {
|
|
506
|
+
await this.clearScopeOnFirstPage(storage, phase, isFull);
|
|
507
|
+
}
|
|
508
|
+
await this.writePhase(storage, phase, items);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var ENTITY_TYPE_BY_PHASE = {
|
|
514
|
+
admins: ADMIN_ENTITY,
|
|
515
|
+
teams: TEAM_ENTITY,
|
|
516
|
+
contacts: CONTACT_ENTITY,
|
|
517
|
+
conversations: CONVERSATION_ENTITY
|
|
518
|
+
};
|
|
519
|
+
function sinceUnixSec(options) {
|
|
520
|
+
if (!options.since) {
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
const ms = new Date(options.since).getTime();
|
|
524
|
+
if (!Number.isFinite(ms)) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
return Math.floor(ms / 1e3);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/index.ts
|
|
531
|
+
var index_default = IntercomConnector;
|
|
532
|
+
export {
|
|
533
|
+
IntercomConnector,
|
|
534
|
+
configFields,
|
|
535
|
+
index_default as default
|
|
536
|
+
};
|
|
537
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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/intercom.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 CredentialsSchema,\n type JSONValue,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n makeChunkedCursorGuard,\n paginateChunked,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\n// ---------------------------------------------------------------------------\n// configFields\n// ---------------------------------------------------------------------------\n\nexport const configFields = defineConfigFields(\n z.object({\n accessToken: z.object({ $secret: z.string() }).meta({\n label: 'Access token',\n description:\n 'Intercom access token (personal or app) with read access to conversations, contacts, teams, and admins. Generate one at Settings → Developers → Developer Hub → Authentication.',\n placeholder: 'dG9rOj...',\n secret: true,\n }),\n apiVersion: z\n .string()\n .trim()\n .regex(\n /^\\d+\\.\\d+$/,\n 'Use a numeric Intercom API version like \"2.11\" (no leading \"v\").',\n )\n .default('2.11')\n .meta({\n label: 'Intercom API version',\n description:\n 'Value sent in the Intercom-Version header. Defaults to 2.11 — pin a specific version here when upgrading deliberately.',\n placeholder: '2.11',\n }),\n region: z.enum(['us', 'eu', 'au']).default('us').meta({\n label: 'Region',\n description:\n 'Intercom region of your workspace. Selects the API host: us → api.intercom.io, eu → api.eu.intercom.io, au → api.au.intercom.io.',\n placeholder: 'us',\n }),\n resources: z\n .array(\n z.enum([\n 'admins',\n 'teams',\n 'contacts',\n 'conversations',\n 'conversation_events',\n ]),\n )\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Intercom resources to sync. Omit to sync all of them. The access token only needs read scopes for the resources listed here.',\n }),\n }),\n);\n\n// ---------------------------------------------------------------------------\n// Settings / credentials\n// ---------------------------------------------------------------------------\n\nexport interface IntercomSettings {\n apiVersion: string;\n region: 'us' | 'eu' | 'au';\n resources?: readonly IntercomResource[];\n}\n\nconst intercomCredentials = {\n accessToken: {\n description: 'Intercom access token',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype IntercomCredentials = typeof intercomCredentials;\n\n// ---------------------------------------------------------------------------\n// Sync phases + cursor\n// ---------------------------------------------------------------------------\n\nconst PHASE_ORDER = [\n 'admins',\n 'teams',\n 'contacts',\n 'conversations',\n 'conversation_events',\n] as const;\n\ntype IntercomPhase = (typeof PHASE_ORDER)[number];\n\nexport type IntercomResource = IntercomPhase;\n\nconst isIntercomSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst SEARCH_PAGE_SIZE = 150;\n\nconst ADMIN_ENTITY = 'intercom_admin';\nconst TEAM_ENTITY = 'intercom_team';\nconst CONTACT_ENTITY = 'intercom_contact';\nconst CONVERSATION_ENTITY = 'intercom_conversation';\nconst CONVERSATION_STATE_EVENT = 'intercom_conversation_state_change';\n\nconst REGION_HOSTS: Record<IntercomSettings['region'], string> = {\n us: 'https://api.intercom.io',\n eu: 'https://api.eu.intercom.io',\n au: 'https://api.au.intercom.io',\n};\n\n// ---------------------------------------------------------------------------\n// API response types\n// ---------------------------------------------------------------------------\n\ninterface AdminRecord {\n id: string;\n name?: string | null;\n email?: string | null;\n job_title?: string | null;\n away_mode_enabled?: boolean | null;\n has_inbox_seat?: boolean | null;\n}\n\ninterface AdminListResponse {\n admins: AdminRecord[];\n}\n\ninterface TeamRecord {\n id: string;\n name?: string | null;\n admin_ids?: number[] | null;\n}\n\ninterface TeamListResponse {\n teams: TeamRecord[];\n}\n\ninterface ContactRecord {\n id: string;\n role?: string | null;\n email?: string | null;\n external_id?: string | null;\n created_at?: number | null;\n updated_at?: number | null;\n last_seen_at?: number | null;\n}\n\ninterface ConversationTagsBlock {\n tags?: Array<{ id?: string | null; name?: string | null }> | null;\n}\n\ninterface ConversationStatistics {\n first_contact_reply_at?: number | null;\n first_admin_reply_at?: number | null;\n last_assignment_at?: number | null;\n last_admin_reply_at?: number | null;\n last_contact_reply_at?: number | null;\n last_close_at?: number | null;\n count_assignments?: number | null;\n count_reopens?: number | null;\n count_conversation_parts?: number | null;\n}\n\ninterface ConversationRecord {\n id: string;\n state?: string | null;\n priority?: string | null;\n admin_assignee_id?: number | string | null;\n team_assignee_id?: number | string | null;\n created_at?: number | null;\n updated_at?: number | null;\n snoozed_until?: number | null;\n tags?: ConversationTagsBlock | null;\n statistics?: ConversationStatistics | null;\n}\n\ninterface SearchPagingBlock {\n next?: { starting_after?: string | null } | null;\n}\n\ninterface ConversationSearchResponse {\n conversations: ConversationRecord[];\n pages?: SearchPagingBlock | null;\n}\n\ninterface ContactSearchResponse {\n data: ContactRecord[];\n pages?: SearchPagingBlock | null;\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 adminSchema = z.object({\n id: idString,\n name: z.string().nullish(),\n email: z.string().nullish(),\n job_title: z.string().nullish(),\n away_mode_enabled: z.boolean().nullish(),\n has_inbox_seat: z.boolean().nullish(),\n});\n\nconst teamSchema = z.object({\n id: idString,\n name: z.string().nullish(),\n admin_ids: z.array(z.number()).nullish(),\n});\n\nconst contactSchema = z.object({\n id: idString,\n role: z.string().nullish(),\n email: z.string().nullish(),\n external_id: z.string().nullish(),\n created_at: z.number().nullish(),\n updated_at: z.number().nullish(),\n last_seen_at: z.number().nullish(),\n});\n\nconst conversationSchema = z.object({\n id: idString,\n state: z.string().nullish(),\n priority: z.string().nullish(),\n admin_assignee_id: z.union([z.number(), z.string()]).nullish(),\n team_assignee_id: z.union([z.number(), z.string()]).nullish(),\n created_at: z.number().nullish(),\n updated_at: z.number().nullish(),\n snoozed_until: z.number().nullish(),\n tags: z\n .object({\n tags: z\n .array(\n z.object({ id: z.string().nullish(), name: z.string().nullish() }),\n )\n .nullish(),\n })\n .nullish(),\n statistics: z\n .object({\n first_contact_reply_at: z.number().nullish(),\n first_admin_reply_at: z.number().nullish(),\n last_assignment_at: z.number().nullish(),\n last_admin_reply_at: z.number().nullish(),\n last_contact_reply_at: z.number().nullish(),\n last_close_at: z.number().nullish(),\n count_assignments: z.number().nullish(),\n count_reopens: z.number().nullish(),\n count_conversation_parts: z.number().nullish(),\n })\n .nullish(),\n});\n\n// ---------------------------------------------------------------------------\n// Value helpers\n// ---------------------------------------------------------------------------\n\nfunction assigneeIdOrNull(\n value: number | string | null | undefined,\n): string | null {\n if (value === null || value === undefined) {\n return null;\n }\n const s = String(value);\n // Intercom returns 0 (number) for unassigned conversations.\n return s === '' || s === '0' ? null : s;\n}\n\nfunction tagNames(tags: ConversationTagsBlock | null | undefined): string[] {\n const list = tags?.tags ?? [];\n const names: string[] = [];\n for (const tag of list) {\n if (tag && typeof tag.name === 'string' && tag.name !== '') {\n names.push(tag.name);\n }\n }\n return names;\n}\n\n// Intercom timestamps come as Unix seconds; storage uses Unix milliseconds.\nfunction epochSecToMs(value: number | null | undefined): number | null {\n return parseEpoch(value ?? null, 's');\n}\n\nfunction epochSecToMsOrZero(value: number | null | undefined): number {\n return epochSecToMs(value) ?? 0;\n}\n\n// ---------------------------------------------------------------------------\n// IntercomConnector\n// ---------------------------------------------------------------------------\n\nexport class IntercomConnector extends BaseConnector<\n IntercomSettings,\n IntercomCredentials\n> {\n static readonly id = 'intercom';\n\n static readonly schemas = {\n admins: z.array(adminSchema),\n teams: z.array(teamSchema),\n contacts: z.array(contactSchema),\n conversations: z.array(conversationSchema),\n conversation_events: z.array(conversationSchema),\n } as const;\n\n static create(input: unknown, ctx?: ConnectorContext): IntercomConnector {\n const parsed = configFields.parse(input);\n return new IntercomConnector(\n {\n apiVersion: parsed.apiVersion,\n region: parsed.region,\n resources: parsed.resources,\n },\n { accessToken: parsed.accessToken },\n ctx,\n );\n }\n\n readonly id = 'intercom';\n override readonly credentials = intercomCredentials;\n\n private get baseUrl(): string {\n return REGION_HOSTS[this.settings.region];\n }\n\n private buildHeaders(): Record<string, string> {\n return {\n Authorization: `Bearer ${this.creds.accessToken}`,\n 'Intercom-Version': this.settings.apiVersion,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('intercom'),\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 // admins — GET /admins (single page, not paginated)\n // -------------------------------------------------------------------------\n\n private async fetchAdmins(\n page: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n if (page !== null) {\n // Single-page resource; only fetched when cursor is null.\n return { items: [], next: null };\n }\n const res = await this.apiGet<AdminListResponse>(\n `${this.baseUrl}/admins`,\n 'admins',\n signal,\n );\n return { items: res.body.admins ?? [], next: null };\n }\n\n private async writeAdmins(\n storage: StorageHandle,\n items: AdminRecord[],\n ): Promise<void> {\n for (const admin of items) {\n await storage.entity({\n type: ADMIN_ENTITY,\n id: admin.id,\n attributes: {\n name: admin.name ?? null,\n email: admin.email ?? null,\n jobTitle: admin.job_title ?? null,\n awayMode: admin.away_mode_enabled ?? null,\n hasInboxSeat: admin.has_inbox_seat ?? null,\n },\n // Admins have no updatedAt in the API; stamp with sync time so newer\n // syncs win on conflict.\n updated_at: Date.now(),\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // teams — GET /teams (single page, not paginated)\n // -------------------------------------------------------------------------\n\n private async fetchTeams(\n page: string | null,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n if (page !== null) {\n return { items: [], next: null };\n }\n const res = await this.apiGet<TeamListResponse>(\n `${this.baseUrl}/teams`,\n 'teams',\n signal,\n );\n return { items: res.body.teams ?? [], next: null };\n }\n\n private async writeTeams(\n storage: StorageHandle,\n items: TeamRecord[],\n ): Promise<void> {\n for (const team of items) {\n await storage.entity({\n type: TEAM_ENTITY,\n id: team.id,\n attributes: {\n name: team.name ?? null,\n adminCount: team.admin_ids?.length ?? 0,\n },\n updated_at: Date.now(),\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // contacts — POST /contacts/search (cursor-paginated)\n // -------------------------------------------------------------------------\n\n private buildContactSearchBody(\n startingAfter: string | null,\n options: SyncOptions,\n ): Record<string, unknown> {\n const sinceSec = sinceUnixSec(options);\n const body: Record<string, unknown> = {\n pagination: {\n per_page: SEARCH_PAGE_SIZE,\n ...(startingAfter ? { starting_after: startingAfter } : {}),\n },\n sort: { field: 'updated_at', order: 'ascending' },\n };\n if (sinceSec !== null) {\n body['query'] = {\n field: 'updated_at',\n operator: '>',\n value: sinceSec,\n };\n }\n return body;\n }\n\n private async fetchContacts(\n page: string | null,\n options: SyncOptions,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n const res = await this.apiPost<ContactSearchResponse>(\n `${this.baseUrl}/contacts/search`,\n 'contacts',\n this.buildContactSearchBody(page, options),\n signal,\n );\n return {\n items: res.body.data ?? [],\n next: res.body.pages?.next?.starting_after ?? null,\n };\n }\n\n private async writeContacts(\n storage: StorageHandle,\n items: ContactRecord[],\n ): Promise<void> {\n for (const contact of items) {\n await storage.entity({\n type: CONTACT_ENTITY,\n id: contact.id,\n attributes: {\n role: contact.role ?? null,\n email: contact.email ?? null,\n externalId: contact.external_id ?? null,\n createdAt: epochSecToMs(contact.created_at),\n lastSeenAt: epochSecToMs(contact.last_seen_at),\n },\n updated_at: epochSecToMsOrZero(\n contact.updated_at ?? contact.created_at,\n ),\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // conversations — POST /conversations/search (entities)\n // -------------------------------------------------------------------------\n\n private buildConversationSearchBody(\n startingAfter: string | null,\n options: SyncOptions,\n ): Record<string, unknown> {\n const sinceSec = sinceUnixSec(options);\n const body: Record<string, unknown> = {\n pagination: {\n per_page: SEARCH_PAGE_SIZE,\n ...(startingAfter ? { starting_after: startingAfter } : {}),\n },\n sort: { field: 'updated_at', order: 'ascending' },\n };\n if (sinceSec !== null) {\n body['query'] = {\n field: 'updated_at',\n operator: '>',\n value: sinceSec,\n };\n }\n return body;\n }\n\n private async fetchConversations(\n page: string | null,\n resource: 'conversations' | 'conversation_events',\n options: SyncOptions,\n signal?: AbortSignal,\n ): Promise<{ items: unknown[]; next: string | null }> {\n // conversation_events clears and rewrites its whole scope on every sync\n // (events can't be upserted by key), so it must re-fetch every conversation\n // even on incremental ticks — otherwise a `since` filter would drop the\n // historical events for conversations untouched in this window.\n const fetchOptions =\n resource === 'conversation_events'\n ? { ...options, since: undefined }\n : options;\n const res = await this.apiPost<ConversationSearchResponse>(\n `${this.baseUrl}/conversations/search`,\n resource,\n this.buildConversationSearchBody(page, fetchOptions),\n signal,\n );\n return {\n items: res.body.conversations ?? [],\n next: res.body.pages?.next?.starting_after ?? null,\n };\n }\n\n private async writeConversations(\n storage: StorageHandle,\n items: ConversationRecord[],\n ): Promise<void> {\n for (const conv of items) {\n const stats = conv.statistics ?? {};\n const attributes: Record<string, JSONValue> = {\n state: conv.state ?? null,\n priority: conv.priority ?? null,\n adminAssigneeId: assigneeIdOrNull(conv.admin_assignee_id),\n teamAssigneeId: assigneeIdOrNull(conv.team_assignee_id),\n createdAt: epochSecToMs(conv.created_at),\n firstContactReplyAt: epochSecToMs(stats.first_contact_reply_at),\n firstAdminReplyAt: epochSecToMs(stats.first_admin_reply_at),\n snoozedUntil: epochSecToMs(conv.snoozed_until),\n countAssignments: stats.count_assignments ?? null,\n countReopens: stats.count_reopens ?? null,\n countConversationParts: stats.count_conversation_parts ?? null,\n tags: tagNames(conv.tags),\n };\n await storage.entity({\n type: CONVERSATION_ENTITY,\n id: conv.id,\n attributes,\n updated_at: epochSecToMsOrZero(conv.updated_at ?? conv.created_at),\n });\n }\n }\n\n // -------------------------------------------------------------------------\n // conversation_events — derived from the same /conversations/search payload\n // -------------------------------------------------------------------------\n\n private async writeConversationEvents(\n storage: StorageHandle,\n items: ConversationRecord[],\n ): Promise<void> {\n for (const conv of items) {\n const stats = conv.statistics ?? {};\n const baseAttrs: Record<string, JSONValue> = {\n conversationId: conv.id,\n state: conv.state ?? null,\n priority: conv.priority ?? null,\n teamAssigneeId: assigneeIdOrNull(conv.team_assignee_id),\n adminAssigneeId: assigneeIdOrNull(conv.admin_assignee_id),\n };\n\n const createdMs = epochSecToMs(conv.created_at);\n if (createdMs !== null) {\n await storage.event({\n name: CONVERSATION_STATE_EVENT,\n start_ts: createdMs,\n end_ts: null,\n attributes: { ...baseAttrs, transition: 'created' },\n });\n }\n\n const assignedMs = epochSecToMs(stats.last_assignment_at);\n if (assignedMs !== null) {\n await storage.event({\n name: CONVERSATION_STATE_EVENT,\n start_ts: assignedMs,\n end_ts: null,\n attributes: { ...baseAttrs, transition: 'assigned' },\n });\n }\n\n const closedMs = epochSecToMs(stats.last_close_at);\n if (closedMs !== null) {\n await storage.event({\n name: CONVERSATION_STATE_EVENT,\n start_ts: closedMs,\n end_ts: null,\n attributes: { ...baseAttrs, transition: 'closed' },\n });\n }\n\n const snoozedMs = epochSecToMs(conv.snoozed_until);\n if (snoozedMs !== null && conv.state === 'snoozed') {\n await storage.event({\n name: CONVERSATION_STATE_EVENT,\n start_ts: snoozedMs,\n end_ts: null,\n attributes: { ...baseAttrs, transition: 'snoozed' },\n });\n }\n }\n }\n\n // -------------------------------------------------------------------------\n // Scope clearing (idempotency)\n // -------------------------------------------------------------------------\n\n private async clearScopeOnFirstPage(\n storage: StorageHandle,\n phase: IntercomPhase,\n isFull: boolean,\n ): Promise<void> {\n if (phase === 'conversation_events') {\n // Events can't be upserted by key, so the only way to keep a sync\n // idempotent is to wipe the scope and rewrite from the freshly fetched\n // statistics. Cheap because the scope is per-name.\n await storage.events([], { names: [CONVERSATION_STATE_EVENT] });\n return;\n }\n if (!isFull) {\n // Entity phases upsert by id, so incremental ticks just overwrite the\n // records they touch — no need to drop the rest of the entity scope.\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: IntercomPhase,\n items: unknown[],\n ): Promise<void> {\n switch (phase) {\n case 'admins':\n await this.writeAdmins(storage, items as AdminRecord[]);\n return;\n case 'teams':\n await this.writeTeams(storage, items as TeamRecord[]);\n return;\n case 'contacts':\n await this.writeContacts(storage, items as ContactRecord[]);\n return;\n case 'conversations':\n await this.writeConversations(storage, items as ConversationRecord[]);\n return;\n case 'conversation_events':\n await this.writeConversationEvents(\n storage,\n items as ConversationRecord[],\n );\n return;\n }\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor = isIntercomSyncCursor(options.cursor)\n ? options.cursor\n : undefined;\n const isFull = options.mode === 'full';\n\n const phases = selectActivePhases<IntercomResource, IntercomPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<IntercomPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (phase, page, sig) => {\n switch (phase) {\n case 'admins':\n return this.fetchAdmins(page, sig);\n case 'teams':\n return this.fetchTeams(page, sig);\n case 'contacts':\n return this.fetchContacts(page, options, sig);\n case 'conversations':\n case 'conversation_events':\n return this.fetchConversations(page, phase, options, sig);\n }\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);\n },\n });\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers (module-scoped)\n// ---------------------------------------------------------------------------\n\nconst ENTITY_TYPE_BY_PHASE: Partial<Record<IntercomPhase, string>> = {\n admins: ADMIN_ENTITY,\n teams: TEAM_ENTITY,\n contacts: CONTACT_ENTITY,\n conversations: CONVERSATION_ENTITY,\n};\n\n// Intercom search filters use Unix seconds, while SyncOptions.since is an\n// ISO timestamp. Returns null when no incremental window applies, which keeps\n// the search body free of a no-op filter clause.\nfunction sinceUnixSec(options: SyncOptions): number | null {\n if (!options.since) {\n return null;\n }\n const ms = new Date(options.since).getTime();\n if (!Number.isFinite(ms)) {\n return null;\n }\n return Math.floor(ms / 1000);\n}\n","import { IntercomConnector } from './intercom';\n\nexport { configFields, IntercomConnector } from './intercom';\nexport type { IntercomSettings, IntercomResource } from './intercom';\nexport default IntercomConnector;\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rawdash/connector-intercom",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Rawdash connector for Intercom — syncs conversations, contacts, teams, and admins from the Intercom REST API into the six-shape storage model",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/rawdash/rawdash.git",
|
|
10
|
+
"directory": "packages/connectors/intercom"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"@rawdash/source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "eslint src",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@rawdash/core": "workspace:*",
|
|
32
|
+
"zod": "^4.4.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@rawdash/connector-shared": "workspace:*",
|
|
36
|
+
"@rawdash/connector-test-utils": "workspace:*",
|
|
37
|
+
"fast-check": "^4.8.0",
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"typescript": "^5.7.2",
|
|
40
|
+
"vitest": "^4.1.4"
|
|
41
|
+
}
|
|
42
|
+
}
|