@rawdash/connector-mailgun 0.28.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/README.md +126 -0
- package/dist/index.d.ts +340 -0
- package/dist/index.js +476 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!-- This file is generated from connector metadata by scripts/generate-connector-docs.ts. Do not edit by hand. -->
|
|
2
|
+
|
|
3
|
+
# @rawdash/connector-mailgun
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@rawdash/connector-mailgun)
|
|
6
|
+
[](https://github.com/rawdash/rawdash/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
Sync transactional email volume, delivery, bounce, and complaint metrics plus recent delivery events from Mailgun.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @rawdash/connector-mailgun
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Authentication
|
|
17
|
+
|
|
18
|
+
A Mailgun API key with read access to analytics, sent via HTTP basic auth (username `api`, password is the key).
|
|
19
|
+
|
|
20
|
+
1. In the Mailgun dashboard open Settings -> API Keys and create or copy an API key with analytics read access.
|
|
21
|
+
2. Note which region hosts your domain (US or EU); set the connector `region` accordingly.
|
|
22
|
+
3. Store the key as a secret and reference it from the connector config as `apiKey: secret("MAILGUN_API_KEY")`, and set `domain` to the sending domain you want to report on.
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
| Field | Type | Required | Description |
|
|
27
|
+
| -------------- | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `apiKey` | secret | Yes | A Mailgun API key with read access to analytics. Create one in the Mailgun dashboard under Settings -> API Keys. |
|
|
29
|
+
| `domain` | string | Yes | The Mailgun sending domain to report on (e.g. mg.example.com). Metrics and logs are filtered to this domain. |
|
|
30
|
+
| `region` | `us` \| `eu` | No | Which Mailgun region hosts the domain. 'us' uses api.mailgun.net; 'eu' uses api.eu.mailgun.net. |
|
|
31
|
+
| `lookbackDays` | number | No | How many calendar days of stats/events to fetch on a full sync. Defaults to 90. |
|
|
32
|
+
| `resources` | array | No | Which Mailgun resources to sync. Omit to sync all of them. |
|
|
33
|
+
|
|
34
|
+
## Resources
|
|
35
|
+
|
|
36
|
+
- **`mailgun_email_stats`** _(metric)_ - Daily transactional email volume and engagement for the configured domain. The canonical value is `accepted` (messages accepted for sending); delivery, failure, and engagement counts are carried as measures.
|
|
37
|
+
- Endpoint: `POST /v1/analytics/metrics`
|
|
38
|
+
- Unit: emails
|
|
39
|
+
- Granularity: day
|
|
40
|
+
- Dimensions: `date`, `domain`
|
|
41
|
+
- Measures: `delivered`, `failed`, `opened`, `clicked`, `unsubscribed`, `complained`
|
|
42
|
+
- **`mailgun_event`** _(event)_ - Recent per-message delivery events (accepted, delivered, failed, opened, clicked, unsubscribed, complained) for the configured domain. Deduplicated by Mailgun event id.
|
|
43
|
+
- Endpoint: `POST /v1/analytics/logs`
|
|
44
|
+
- A bounded sample of the most recent logs is stored; Mailgun retains log data for a limited period.
|
|
45
|
+
- `eventId`: Mailgun event id (stable per event).
|
|
46
|
+
- `eventType`: Event type (accepted, delivered, failed, opened, clicked, unsubscribed, complained).
|
|
47
|
+
- `recipient`: Recipient email address.
|
|
48
|
+
- `domain`: The Mailgun sending domain.
|
|
49
|
+
- `severity`: Failure severity, when present.
|
|
50
|
+
- `reason`: Failure reason, when present.
|
|
51
|
+
|
|
52
|
+
## Example
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import {
|
|
56
|
+
defineConfig,
|
|
57
|
+
defineDashboard,
|
|
58
|
+
defineMetric,
|
|
59
|
+
secret,
|
|
60
|
+
} from '@rawdash/core';
|
|
61
|
+
|
|
62
|
+
const mailgun = {
|
|
63
|
+
name: 'mailgun',
|
|
64
|
+
connectorId: 'mailgun',
|
|
65
|
+
config: {
|
|
66
|
+
apiKey: secret('MAILGUN_API_KEY'),
|
|
67
|
+
domain: 'mg.example.com',
|
|
68
|
+
region: 'us' as const,
|
|
69
|
+
lookbackDays: 90,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default defineConfig({
|
|
74
|
+
connectors: [mailgun],
|
|
75
|
+
dashboards: {
|
|
76
|
+
email: defineDashboard({
|
|
77
|
+
widgets: {
|
|
78
|
+
sends_30d: {
|
|
79
|
+
kind: 'stat',
|
|
80
|
+
title: 'Emails sent (30d)',
|
|
81
|
+
window: '30d',
|
|
82
|
+
metric: defineMetric({
|
|
83
|
+
connector: mailgun,
|
|
84
|
+
shape: 'metric',
|
|
85
|
+
name: 'mailgun_email_stats',
|
|
86
|
+
field: 'value',
|
|
87
|
+
fn: 'sum',
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
daily_sends: {
|
|
91
|
+
kind: 'timeseries',
|
|
92
|
+
title: 'Daily email volume',
|
|
93
|
+
window: '30d',
|
|
94
|
+
metric: defineMetric({
|
|
95
|
+
connector: mailgun,
|
|
96
|
+
shape: 'metric',
|
|
97
|
+
name: 'mailgun_email_stats',
|
|
98
|
+
field: 'value',
|
|
99
|
+
fn: 'sum',
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Rate limits
|
|
109
|
+
|
|
110
|
+
Mailgun applies per-endpoint rate limits and returns 429 with a Retry-After header when exceeded; the shared HTTP client backs off and retries automatically.
|
|
111
|
+
|
|
112
|
+
## Limitations
|
|
113
|
+
|
|
114
|
+
- Metrics are reported at daily resolution; the connector requests `resolution=day` from the analytics API.
|
|
115
|
+
- Incremental syncs re-fetch a fixed trailing window and replace only that window, so older samples are preserved.
|
|
116
|
+
- The events resource stores a bounded sample of the most recent delivery logs (Mailgun retains log data for a limited period), not a complete event archive.
|
|
117
|
+
|
|
118
|
+
## Links
|
|
119
|
+
|
|
120
|
+
- [Rawdash docs](https://rawdash.dev/docs/connectors)
|
|
121
|
+
- [Mailgun API docs](https://documentation.mailgun.com/docs/mailgun/api-reference/)
|
|
122
|
+
- [GitHub](https://github.com/rawdash/rawdash)
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import { BaseConnector, ConnectorContext, SyncOptions, StorageHandle, SyncResult, ConnectorDoc, Event, MetricSample } from '@rawdash/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
declare const configFields: z.ZodObject<{
|
|
5
|
+
apiKey: z.ZodObject<{
|
|
6
|
+
$secret: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
domain: z.ZodString;
|
|
9
|
+
region: z.ZodDefault<z.ZodEnum<{
|
|
10
|
+
us: "us";
|
|
11
|
+
eu: "eu";
|
|
12
|
+
}>>;
|
|
13
|
+
lookbackDays: z.ZodOptional<z.ZodNumber>;
|
|
14
|
+
resources: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
15
|
+
events: "events";
|
|
16
|
+
email_stats: "email_stats";
|
|
17
|
+
}>>>;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
declare const doc: ConnectorDoc;
|
|
20
|
+
interface MailgunSettings {
|
|
21
|
+
domain: string;
|
|
22
|
+
region: 'us' | 'eu';
|
|
23
|
+
lookbackDays?: number;
|
|
24
|
+
resources?: readonly MailgunResource[];
|
|
25
|
+
}
|
|
26
|
+
declare const mailgunCredentials: {
|
|
27
|
+
apiKey: {
|
|
28
|
+
description: string;
|
|
29
|
+
auth: "required";
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
type MailgunCredentials = typeof mailgunCredentials;
|
|
33
|
+
declare const PHASE_ORDER: readonly ["email_stats", "events"];
|
|
34
|
+
type MailgunPhase = (typeof PHASE_ORDER)[number];
|
|
35
|
+
type MailgunResource = MailgunPhase;
|
|
36
|
+
declare const metricsItemSchema: z.ZodObject<{
|
|
37
|
+
dimensions: z.ZodArray<z.ZodObject<{
|
|
38
|
+
dimension: z.ZodString;
|
|
39
|
+
value: z.ZodString;
|
|
40
|
+
display_value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
41
|
+
}, z.core.$strip>>;
|
|
42
|
+
metrics: z.ZodObject<{
|
|
43
|
+
accepted_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
44
|
+
delivered_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
45
|
+
failed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
46
|
+
opened_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
47
|
+
clicked_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
48
|
+
unsubscribed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
49
|
+
complained_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
50
|
+
}, z.core.$strip>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
declare const logsItemSchema: z.ZodObject<{
|
|
53
|
+
id: z.ZodString;
|
|
54
|
+
event: z.ZodString;
|
|
55
|
+
'@timestamp': z.ZodString;
|
|
56
|
+
recipient: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
'recipient-domain': z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
severity: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
59
|
+
reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
60
|
+
}, z.core.$strip>;
|
|
61
|
+
declare const mailgunResources: {
|
|
62
|
+
readonly mailgun_email_stats: {
|
|
63
|
+
readonly shape: "metric";
|
|
64
|
+
readonly description: "Daily transactional email volume and engagement for the configured domain. The canonical value is `accepted` (messages accepted for sending); delivery, failure, and engagement counts are carried as measures.";
|
|
65
|
+
readonly endpoint: "POST /v1/analytics/metrics";
|
|
66
|
+
readonly unit: "emails";
|
|
67
|
+
readonly granularity: "day";
|
|
68
|
+
readonly dimensions: [{
|
|
69
|
+
readonly name: "date";
|
|
70
|
+
readonly description: "Calendar day of the metric sample (UTC).";
|
|
71
|
+
}, {
|
|
72
|
+
readonly name: "domain";
|
|
73
|
+
readonly description: "The Mailgun sending domain.";
|
|
74
|
+
}];
|
|
75
|
+
readonly measures: [{
|
|
76
|
+
readonly name: "delivered";
|
|
77
|
+
readonly description: "Messages delivered on the day.";
|
|
78
|
+
}, {
|
|
79
|
+
readonly name: "failed";
|
|
80
|
+
readonly description: "Messages that failed (bounced/dropped).";
|
|
81
|
+
}, {
|
|
82
|
+
readonly name: "opened";
|
|
83
|
+
readonly description: "Message opens recorded on the day.";
|
|
84
|
+
}, {
|
|
85
|
+
readonly name: "clicked";
|
|
86
|
+
readonly description: "Link clicks recorded on the day.";
|
|
87
|
+
}, {
|
|
88
|
+
readonly name: "unsubscribed";
|
|
89
|
+
readonly description: "Unsubscribes recorded on the day.";
|
|
90
|
+
}, {
|
|
91
|
+
readonly name: "complained";
|
|
92
|
+
readonly description: "Spam complaints recorded on the day.";
|
|
93
|
+
}];
|
|
94
|
+
readonly responses: {
|
|
95
|
+
readonly email_stats: z.ZodObject<{
|
|
96
|
+
items: z.ZodArray<z.ZodObject<{
|
|
97
|
+
dimensions: z.ZodArray<z.ZodObject<{
|
|
98
|
+
dimension: z.ZodString;
|
|
99
|
+
value: z.ZodString;
|
|
100
|
+
display_value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
101
|
+
}, z.core.$strip>>;
|
|
102
|
+
metrics: z.ZodObject<{
|
|
103
|
+
accepted_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
104
|
+
delivered_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
105
|
+
failed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
106
|
+
opened_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
107
|
+
clicked_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
108
|
+
unsubscribed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
109
|
+
complained_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
110
|
+
}, z.core.$strip>;
|
|
111
|
+
}, z.core.$strip>>;
|
|
112
|
+
pagination: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
113
|
+
skip: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
114
|
+
limit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
115
|
+
total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
116
|
+
}, z.core.$strip>>>;
|
|
117
|
+
}, z.core.$strip>;
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
readonly mailgun_event: {
|
|
121
|
+
readonly shape: "event";
|
|
122
|
+
readonly description: "Recent per-message delivery events (accepted, delivered, failed, opened, clicked, unsubscribed, complained) for the configured domain. Deduplicated by Mailgun event id.";
|
|
123
|
+
readonly endpoint: "POST /v1/analytics/logs";
|
|
124
|
+
readonly notes: "A bounded sample of the most recent logs is stored; Mailgun retains log data for a limited period.";
|
|
125
|
+
readonly fields: [{
|
|
126
|
+
readonly name: "eventId";
|
|
127
|
+
readonly description: "Mailgun event id (stable per event).";
|
|
128
|
+
}, {
|
|
129
|
+
readonly name: "eventType";
|
|
130
|
+
readonly description: "Event type (accepted, delivered, failed, opened, clicked, unsubscribed, complained).";
|
|
131
|
+
}, {
|
|
132
|
+
readonly name: "recipient";
|
|
133
|
+
readonly description: "Recipient email address.";
|
|
134
|
+
}, {
|
|
135
|
+
readonly name: "domain";
|
|
136
|
+
readonly description: "The Mailgun sending domain.";
|
|
137
|
+
}, {
|
|
138
|
+
readonly name: "severity";
|
|
139
|
+
readonly description: "Failure severity, when present.";
|
|
140
|
+
}, {
|
|
141
|
+
readonly name: "reason";
|
|
142
|
+
readonly description: "Failure reason, when present.";
|
|
143
|
+
}];
|
|
144
|
+
readonly filterable: [];
|
|
145
|
+
readonly responses: {
|
|
146
|
+
readonly events: z.ZodObject<{
|
|
147
|
+
items: z.ZodArray<z.ZodObject<{
|
|
148
|
+
id: z.ZodString;
|
|
149
|
+
event: z.ZodString;
|
|
150
|
+
'@timestamp': z.ZodString;
|
|
151
|
+
recipient: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
152
|
+
'recipient-domain': z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
153
|
+
severity: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
154
|
+
reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
155
|
+
}, z.core.$strip>>;
|
|
156
|
+
pagination: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
157
|
+
next: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
158
|
+
previous: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
159
|
+
total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
160
|
+
}, z.core.$strip>>>;
|
|
161
|
+
}, z.core.$strip>;
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
type MailgunMetricsItem = z.infer<typeof metricsItemSchema>;
|
|
166
|
+
type MailgunLogsItem = z.infer<typeof logsItemSchema>;
|
|
167
|
+
interface MailgunWindow {
|
|
168
|
+
startMs: number;
|
|
169
|
+
endMs: number;
|
|
170
|
+
}
|
|
171
|
+
declare function getWindow(options: SyncOptions, lookbackDays: number, now?: number): MailgunWindow;
|
|
172
|
+
declare function metricsItemToSample(item: MailgunMetricsItem, domain: string): MetricSample;
|
|
173
|
+
declare function logsItemToEvent(item: MailgunLogsItem, domain: string): Event;
|
|
174
|
+
declare const id = "mailgun";
|
|
175
|
+
declare class MailgunConnector extends BaseConnector<MailgunSettings, MailgunCredentials> {
|
|
176
|
+
static readonly id = "mailgun";
|
|
177
|
+
static readonly resources: {
|
|
178
|
+
readonly mailgun_email_stats: {
|
|
179
|
+
readonly shape: "metric";
|
|
180
|
+
readonly description: "Daily transactional email volume and engagement for the configured domain. The canonical value is `accepted` (messages accepted for sending); delivery, failure, and engagement counts are carried as measures.";
|
|
181
|
+
readonly endpoint: "POST /v1/analytics/metrics";
|
|
182
|
+
readonly unit: "emails";
|
|
183
|
+
readonly granularity: "day";
|
|
184
|
+
readonly dimensions: [{
|
|
185
|
+
readonly name: "date";
|
|
186
|
+
readonly description: "Calendar day of the metric sample (UTC).";
|
|
187
|
+
}, {
|
|
188
|
+
readonly name: "domain";
|
|
189
|
+
readonly description: "The Mailgun sending domain.";
|
|
190
|
+
}];
|
|
191
|
+
readonly measures: [{
|
|
192
|
+
readonly name: "delivered";
|
|
193
|
+
readonly description: "Messages delivered on the day.";
|
|
194
|
+
}, {
|
|
195
|
+
readonly name: "failed";
|
|
196
|
+
readonly description: "Messages that failed (bounced/dropped).";
|
|
197
|
+
}, {
|
|
198
|
+
readonly name: "opened";
|
|
199
|
+
readonly description: "Message opens recorded on the day.";
|
|
200
|
+
}, {
|
|
201
|
+
readonly name: "clicked";
|
|
202
|
+
readonly description: "Link clicks recorded on the day.";
|
|
203
|
+
}, {
|
|
204
|
+
readonly name: "unsubscribed";
|
|
205
|
+
readonly description: "Unsubscribes recorded on the day.";
|
|
206
|
+
}, {
|
|
207
|
+
readonly name: "complained";
|
|
208
|
+
readonly description: "Spam complaints recorded on the day.";
|
|
209
|
+
}];
|
|
210
|
+
readonly responses: {
|
|
211
|
+
readonly email_stats: z.ZodObject<{
|
|
212
|
+
items: z.ZodArray<z.ZodObject<{
|
|
213
|
+
dimensions: z.ZodArray<z.ZodObject<{
|
|
214
|
+
dimension: z.ZodString;
|
|
215
|
+
value: z.ZodString;
|
|
216
|
+
display_value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
217
|
+
}, z.core.$strip>>;
|
|
218
|
+
metrics: z.ZodObject<{
|
|
219
|
+
accepted_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
220
|
+
delivered_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
221
|
+
failed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
222
|
+
opened_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
223
|
+
clicked_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
224
|
+
unsubscribed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
225
|
+
complained_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
226
|
+
}, z.core.$strip>;
|
|
227
|
+
}, z.core.$strip>>;
|
|
228
|
+
pagination: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
229
|
+
skip: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
230
|
+
limit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
231
|
+
total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
232
|
+
}, z.core.$strip>>>;
|
|
233
|
+
}, z.core.$strip>;
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
readonly mailgun_event: {
|
|
237
|
+
readonly shape: "event";
|
|
238
|
+
readonly description: "Recent per-message delivery events (accepted, delivered, failed, opened, clicked, unsubscribed, complained) for the configured domain. Deduplicated by Mailgun event id.";
|
|
239
|
+
readonly endpoint: "POST /v1/analytics/logs";
|
|
240
|
+
readonly notes: "A bounded sample of the most recent logs is stored; Mailgun retains log data for a limited period.";
|
|
241
|
+
readonly fields: [{
|
|
242
|
+
readonly name: "eventId";
|
|
243
|
+
readonly description: "Mailgun event id (stable per event).";
|
|
244
|
+
}, {
|
|
245
|
+
readonly name: "eventType";
|
|
246
|
+
readonly description: "Event type (accepted, delivered, failed, opened, clicked, unsubscribed, complained).";
|
|
247
|
+
}, {
|
|
248
|
+
readonly name: "recipient";
|
|
249
|
+
readonly description: "Recipient email address.";
|
|
250
|
+
}, {
|
|
251
|
+
readonly name: "domain";
|
|
252
|
+
readonly description: "The Mailgun sending domain.";
|
|
253
|
+
}, {
|
|
254
|
+
readonly name: "severity";
|
|
255
|
+
readonly description: "Failure severity, when present.";
|
|
256
|
+
}, {
|
|
257
|
+
readonly name: "reason";
|
|
258
|
+
readonly description: "Failure reason, when present.";
|
|
259
|
+
}];
|
|
260
|
+
readonly filterable: [];
|
|
261
|
+
readonly responses: {
|
|
262
|
+
readonly events: z.ZodObject<{
|
|
263
|
+
items: z.ZodArray<z.ZodObject<{
|
|
264
|
+
id: z.ZodString;
|
|
265
|
+
event: z.ZodString;
|
|
266
|
+
'@timestamp': z.ZodString;
|
|
267
|
+
recipient: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
268
|
+
'recipient-domain': z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
269
|
+
severity: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
270
|
+
reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
271
|
+
}, z.core.$strip>>;
|
|
272
|
+
pagination: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
273
|
+
next: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
274
|
+
previous: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
275
|
+
total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
276
|
+
}, z.core.$strip>>>;
|
|
277
|
+
}, z.core.$strip>;
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
static readonly schemas: {
|
|
282
|
+
readonly email_stats: z.ZodObject<{
|
|
283
|
+
items: z.ZodArray<z.ZodObject<{
|
|
284
|
+
dimensions: z.ZodArray<z.ZodObject<{
|
|
285
|
+
dimension: z.ZodString;
|
|
286
|
+
value: z.ZodString;
|
|
287
|
+
display_value: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
288
|
+
}, z.core.$strip>>;
|
|
289
|
+
metrics: z.ZodObject<{
|
|
290
|
+
accepted_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
291
|
+
delivered_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
292
|
+
failed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
293
|
+
opened_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
294
|
+
clicked_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
295
|
+
unsubscribed_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
296
|
+
complained_count: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
297
|
+
}, z.core.$strip>;
|
|
298
|
+
}, z.core.$strip>>;
|
|
299
|
+
pagination: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
300
|
+
skip: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
301
|
+
limit: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
302
|
+
total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
303
|
+
}, z.core.$strip>>>;
|
|
304
|
+
}, z.core.$strip>;
|
|
305
|
+
} & {
|
|
306
|
+
readonly events: z.ZodObject<{
|
|
307
|
+
items: z.ZodArray<z.ZodObject<{
|
|
308
|
+
id: z.ZodString;
|
|
309
|
+
event: z.ZodString;
|
|
310
|
+
'@timestamp': z.ZodString;
|
|
311
|
+
recipient: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
312
|
+
'recipient-domain': z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
313
|
+
severity: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
314
|
+
reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
315
|
+
}, z.core.$strip>>;
|
|
316
|
+
pagination: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
317
|
+
next: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
318
|
+
previous: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
319
|
+
total: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
320
|
+
}, z.core.$strip>>>;
|
|
321
|
+
}, z.core.$strip>;
|
|
322
|
+
} & Readonly<Record<string, z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>>;
|
|
323
|
+
static create(input: unknown, ctx?: ConnectorContext): MailgunConnector;
|
|
324
|
+
readonly id = "mailgun";
|
|
325
|
+
readonly credentials: {
|
|
326
|
+
apiKey: {
|
|
327
|
+
description: string;
|
|
328
|
+
auth: "required";
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
private baseUrl;
|
|
332
|
+
private buildHeaders;
|
|
333
|
+
private domainFilter;
|
|
334
|
+
private fetchMetricItems;
|
|
335
|
+
private fetchLogItems;
|
|
336
|
+
private writePhase;
|
|
337
|
+
sync(options: SyncOptions, storage: StorageHandle, signal?: AbortSignal): Promise<SyncResult>;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export { MailgunConnector, type MailgunLogsItem, type MailgunMetricsItem, type MailgunResource, type MailgunSettings, type MailgunWindow, configFields, MailgunConnector as default, doc, getWindow, id, logsItemToEvent, metricsItemToSample, mailgunResources as resources };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
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/mailgun.ts
|
|
30
|
+
import {
|
|
31
|
+
BaseConnector,
|
|
32
|
+
defineConfigFields,
|
|
33
|
+
defineConnectorDoc,
|
|
34
|
+
defineResources,
|
|
35
|
+
makeChunkedCursorGuard,
|
|
36
|
+
metricSample,
|
|
37
|
+
paginateChunked,
|
|
38
|
+
schemasFromResources,
|
|
39
|
+
selectActivePhases
|
|
40
|
+
} from "@rawdash/core";
|
|
41
|
+
import { z } from "zod";
|
|
42
|
+
var configFields = defineConfigFields(
|
|
43
|
+
z.object({
|
|
44
|
+
apiKey: z.object({ $secret: z.string() }).meta({
|
|
45
|
+
label: "API key",
|
|
46
|
+
description: "A Mailgun API key with read access to analytics. Create one in the Mailgun dashboard under Settings -> API Keys.",
|
|
47
|
+
placeholder: "key-...",
|
|
48
|
+
secret: true
|
|
49
|
+
}),
|
|
50
|
+
domain: z.string().trim().min(1, "A sending domain is required").meta({
|
|
51
|
+
label: "Sending domain",
|
|
52
|
+
description: "The Mailgun sending domain to report on (e.g. mg.example.com). Metrics and logs are filtered to this domain.",
|
|
53
|
+
placeholder: "mg.example.com"
|
|
54
|
+
}),
|
|
55
|
+
region: z.enum(["us", "eu"]).default("us").meta({
|
|
56
|
+
label: "Region",
|
|
57
|
+
description: "Which Mailgun region hosts the domain. 'us' uses api.mailgun.net; 'eu' uses api.eu.mailgun.net.",
|
|
58
|
+
placeholder: "us"
|
|
59
|
+
}),
|
|
60
|
+
lookbackDays: z.number().int().positive().optional().meta({
|
|
61
|
+
label: "Lookback days (full sync)",
|
|
62
|
+
description: "How many calendar days of stats/events to fetch on a full sync. Defaults to 90.",
|
|
63
|
+
placeholder: "90"
|
|
64
|
+
}),
|
|
65
|
+
resources: z.array(z.enum(["email_stats", "events"])).nonempty().optional().meta({
|
|
66
|
+
label: "Resources",
|
|
67
|
+
description: "Which Mailgun resources to sync. Omit to sync all of them."
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
var doc = defineConnectorDoc({
|
|
72
|
+
displayName: "Mailgun",
|
|
73
|
+
category: "engineering",
|
|
74
|
+
brandColor: "#F06B66",
|
|
75
|
+
tagline: "Sync transactional email volume, delivery, bounce, and complaint metrics plus recent delivery events from Mailgun.",
|
|
76
|
+
vendor: {
|
|
77
|
+
name: "Mailgun",
|
|
78
|
+
domain: "mailgun.com",
|
|
79
|
+
apiDocs: "https://documentation.mailgun.com/docs/mailgun/api-reference/",
|
|
80
|
+
website: "https://www.mailgun.com"
|
|
81
|
+
},
|
|
82
|
+
auth: {
|
|
83
|
+
summary: "A Mailgun API key with read access to analytics, sent via HTTP basic auth (username `api`, password is the key).",
|
|
84
|
+
setup: [
|
|
85
|
+
"In the Mailgun dashboard open Settings -> API Keys and create or copy an API key with analytics read access.",
|
|
86
|
+
"Note which region hosts your domain (US or EU); set the connector `region` accordingly.",
|
|
87
|
+
'Store the key as a secret and reference it from the connector config as `apiKey: secret("MAILGUN_API_KEY")`, and set `domain` to the sending domain you want to report on.'
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
rateLimit: "Mailgun applies per-endpoint rate limits and returns 429 with a Retry-After header when exceeded; the shared HTTP client backs off and retries automatically.",
|
|
91
|
+
limitations: [
|
|
92
|
+
"Metrics are reported at daily resolution; the connector requests `resolution=day` from the analytics API.",
|
|
93
|
+
"Incremental syncs re-fetch a fixed trailing window and replace only that window, so older samples are preserved.",
|
|
94
|
+
"The events resource stores a bounded sample of the most recent delivery logs (Mailgun retains log data for a limited period), not a complete event archive."
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
var mailgunCredentials = {
|
|
98
|
+
apiKey: {
|
|
99
|
+
description: "Mailgun API key",
|
|
100
|
+
auth: "required"
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var PHASE_ORDER = ["email_stats", "events"];
|
|
104
|
+
var isMailgunSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
|
|
105
|
+
var MS_PER_DAY = 24 * 60 * 60 * 1e3;
|
|
106
|
+
var DEFAULT_LOOKBACK_DAYS = 90;
|
|
107
|
+
var INCREMENTAL_LOOKBACK_DAYS = 7;
|
|
108
|
+
var METRICS_PAGE_LIMIT = 1e3;
|
|
109
|
+
var EVENTS_PAGE_LIMIT = 300;
|
|
110
|
+
var MAX_EVENT_PAGES = 20;
|
|
111
|
+
var EMAIL_STATS_METRIC = "mailgun_email_stats";
|
|
112
|
+
var MAILGUN_EVENT = "mailgun_event";
|
|
113
|
+
var METRIC_FIELDS = [
|
|
114
|
+
"accepted_count",
|
|
115
|
+
"delivered_count",
|
|
116
|
+
"failed_count",
|
|
117
|
+
"opened_count",
|
|
118
|
+
"clicked_count",
|
|
119
|
+
"unsubscribed_count",
|
|
120
|
+
"complained_count"
|
|
121
|
+
];
|
|
122
|
+
var EVENT_TYPES = [
|
|
123
|
+
"accepted",
|
|
124
|
+
"delivered",
|
|
125
|
+
"failed",
|
|
126
|
+
"opened",
|
|
127
|
+
"clicked",
|
|
128
|
+
"unsubscribed",
|
|
129
|
+
"complained"
|
|
130
|
+
];
|
|
131
|
+
var metricsDimensionSchema = z.object({
|
|
132
|
+
dimension: z.string(),
|
|
133
|
+
value: z.string(),
|
|
134
|
+
display_value: z.string().nullish()
|
|
135
|
+
});
|
|
136
|
+
var metricsItemSchema = z.object({
|
|
137
|
+
dimensions: z.array(metricsDimensionSchema),
|
|
138
|
+
metrics: z.object({
|
|
139
|
+
accepted_count: z.number().nullish(),
|
|
140
|
+
delivered_count: z.number().nullish(),
|
|
141
|
+
failed_count: z.number().nullish(),
|
|
142
|
+
opened_count: z.number().nullish(),
|
|
143
|
+
clicked_count: z.number().nullish(),
|
|
144
|
+
unsubscribed_count: z.number().nullish(),
|
|
145
|
+
complained_count: z.number().nullish()
|
|
146
|
+
})
|
|
147
|
+
});
|
|
148
|
+
var metricsResponseSchema = z.object({
|
|
149
|
+
items: z.array(metricsItemSchema),
|
|
150
|
+
pagination: z.object({
|
|
151
|
+
skip: z.number().nullish(),
|
|
152
|
+
limit: z.number().nullish(),
|
|
153
|
+
total: z.number().nullish()
|
|
154
|
+
}).nullish()
|
|
155
|
+
});
|
|
156
|
+
var logsItemSchema = z.object({
|
|
157
|
+
id: z.string(),
|
|
158
|
+
event: z.string(),
|
|
159
|
+
"@timestamp": z.string(),
|
|
160
|
+
recipient: z.string().nullish(),
|
|
161
|
+
"recipient-domain": z.string().nullish(),
|
|
162
|
+
severity: z.string().nullish(),
|
|
163
|
+
reason: z.string().nullish()
|
|
164
|
+
});
|
|
165
|
+
var logsResponseSchema = z.object({
|
|
166
|
+
items: z.array(logsItemSchema),
|
|
167
|
+
pagination: z.object({
|
|
168
|
+
next: z.string().nullish(),
|
|
169
|
+
previous: z.string().nullish(),
|
|
170
|
+
total: z.number().nullish()
|
|
171
|
+
}).nullish()
|
|
172
|
+
});
|
|
173
|
+
var mailgunResources = defineResources({
|
|
174
|
+
[EMAIL_STATS_METRIC]: {
|
|
175
|
+
shape: "metric",
|
|
176
|
+
description: "Daily transactional email volume and engagement for the configured domain. The canonical value is `accepted` (messages accepted for sending); delivery, failure, and engagement counts are carried as measures.",
|
|
177
|
+
endpoint: "POST /v1/analytics/metrics",
|
|
178
|
+
unit: "emails",
|
|
179
|
+
granularity: "day",
|
|
180
|
+
dimensions: [
|
|
181
|
+
{ name: "date", description: "Calendar day of the metric sample (UTC)." },
|
|
182
|
+
{ name: "domain", description: "The Mailgun sending domain." }
|
|
183
|
+
],
|
|
184
|
+
measures: [
|
|
185
|
+
{ name: "delivered", description: "Messages delivered on the day." },
|
|
186
|
+
{
|
|
187
|
+
name: "failed",
|
|
188
|
+
description: "Messages that failed (bounced/dropped)."
|
|
189
|
+
},
|
|
190
|
+
{ name: "opened", description: "Message opens recorded on the day." },
|
|
191
|
+
{ name: "clicked", description: "Link clicks recorded on the day." },
|
|
192
|
+
{
|
|
193
|
+
name: "unsubscribed",
|
|
194
|
+
description: "Unsubscribes recorded on the day."
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "complained",
|
|
198
|
+
description: "Spam complaints recorded on the day."
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
responses: { email_stats: metricsResponseSchema }
|
|
202
|
+
},
|
|
203
|
+
[MAILGUN_EVENT]: {
|
|
204
|
+
shape: "event",
|
|
205
|
+
description: "Recent per-message delivery events (accepted, delivered, failed, opened, clicked, unsubscribed, complained) for the configured domain. Deduplicated by Mailgun event id.",
|
|
206
|
+
endpoint: "POST /v1/analytics/logs",
|
|
207
|
+
notes: "A bounded sample of the most recent logs is stored; Mailgun retains log data for a limited period.",
|
|
208
|
+
fields: [
|
|
209
|
+
{ name: "eventId", description: "Mailgun event id (stable per event)." },
|
|
210
|
+
{
|
|
211
|
+
name: "eventType",
|
|
212
|
+
description: "Event type (accepted, delivered, failed, opened, clicked, unsubscribed, complained)."
|
|
213
|
+
},
|
|
214
|
+
{ name: "recipient", description: "Recipient email address." },
|
|
215
|
+
{ name: "domain", description: "The Mailgun sending domain." },
|
|
216
|
+
{ name: "severity", description: "Failure severity, when present." },
|
|
217
|
+
{ name: "reason", description: "Failure reason, when present." }
|
|
218
|
+
],
|
|
219
|
+
filterable: [],
|
|
220
|
+
responses: { events: logsResponseSchema }
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
function startOfUtcDay(ms) {
|
|
224
|
+
return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;
|
|
225
|
+
}
|
|
226
|
+
function pad2(n) {
|
|
227
|
+
return String(n).padStart(2, "0");
|
|
228
|
+
}
|
|
229
|
+
function toIsoDate(ms) {
|
|
230
|
+
const d = new Date(ms);
|
|
231
|
+
return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
|
|
232
|
+
}
|
|
233
|
+
function getWindow(options, lookbackDays, now = Date.now()) {
|
|
234
|
+
const endMs = startOfUtcDay(now) + MS_PER_DAY - 1;
|
|
235
|
+
const today = startOfUtcDay(now);
|
|
236
|
+
if (options.mode === "latest") {
|
|
237
|
+
return {
|
|
238
|
+
startMs: today - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY,
|
|
239
|
+
endMs
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (options.since) {
|
|
243
|
+
const sinceMs = new Date(options.since).getTime();
|
|
244
|
+
if (Number.isFinite(sinceMs)) {
|
|
245
|
+
const requested = Math.max(
|
|
246
|
+
1,
|
|
247
|
+
Math.ceil((today - startOfUtcDay(sinceMs)) / MS_PER_DAY) + 1
|
|
248
|
+
);
|
|
249
|
+
const capped = Math.min(requested, lookbackDays);
|
|
250
|
+
return { startMs: today - (capped - 1) * MS_PER_DAY, endMs };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { startMs: today - (lookbackDays - 1) * MS_PER_DAY, endMs };
|
|
254
|
+
}
|
|
255
|
+
function toNumber(value) {
|
|
256
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
257
|
+
}
|
|
258
|
+
function timeDimensionValue(item) {
|
|
259
|
+
const timeDim = item.dimensions.find((d) => d.dimension === "time") ?? item.dimensions[0];
|
|
260
|
+
return timeDim ? timeDim.value : null;
|
|
261
|
+
}
|
|
262
|
+
function metricsItemToSample(item, domain) {
|
|
263
|
+
const raw = timeDimensionValue(item);
|
|
264
|
+
const parsed = raw ? parseEpoch(raw, "iso") : null;
|
|
265
|
+
const ts = parsed ?? (raw ? Date.parse(raw) : NaN);
|
|
266
|
+
const tsMs = Number.isFinite(ts) ? ts : 0;
|
|
267
|
+
const date = Number.isFinite(ts) ? toIsoDate(tsMs) : raw ?? "";
|
|
268
|
+
const m = item.metrics;
|
|
269
|
+
return metricSample(mailgunResources, EMAIL_STATS_METRIC, {
|
|
270
|
+
ts: tsMs,
|
|
271
|
+
value: toNumber(m.accepted_count),
|
|
272
|
+
attributes: {
|
|
273
|
+
date,
|
|
274
|
+
domain,
|
|
275
|
+
delivered: toNumber(m.delivered_count),
|
|
276
|
+
failed: toNumber(m.failed_count),
|
|
277
|
+
opened: toNumber(m.opened_count),
|
|
278
|
+
clicked: toNumber(m.clicked_count),
|
|
279
|
+
unsubscribed: toNumber(m.unsubscribed_count),
|
|
280
|
+
complained: toNumber(m.complained_count)
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function logsItemToEvent(item, domain) {
|
|
285
|
+
const parsed = parseEpoch(item["@timestamp"], "iso");
|
|
286
|
+
const ts = parsed ?? Date.parse(item["@timestamp"]);
|
|
287
|
+
const startTs = Number.isFinite(ts) ? ts : 0;
|
|
288
|
+
return {
|
|
289
|
+
name: MAILGUN_EVENT,
|
|
290
|
+
start_ts: startTs,
|
|
291
|
+
end_ts: startTs,
|
|
292
|
+
attributes: {
|
|
293
|
+
eventId: item.id,
|
|
294
|
+
eventType: item.event,
|
|
295
|
+
recipient: item.recipient ?? null,
|
|
296
|
+
domain,
|
|
297
|
+
severity: item.severity ?? null,
|
|
298
|
+
reason: item.reason ?? null
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
var id = "mailgun";
|
|
303
|
+
var MailgunConnector = class _MailgunConnector extends BaseConnector {
|
|
304
|
+
static id = id;
|
|
305
|
+
static resources = mailgunResources;
|
|
306
|
+
static schemas = schemasFromResources(mailgunResources);
|
|
307
|
+
static create(input, ctx) {
|
|
308
|
+
const parsed = configFields.parse(input);
|
|
309
|
+
return new _MailgunConnector(
|
|
310
|
+
{
|
|
311
|
+
domain: parsed.domain,
|
|
312
|
+
region: parsed.region,
|
|
313
|
+
lookbackDays: parsed.lookbackDays,
|
|
314
|
+
resources: parsed.resources
|
|
315
|
+
},
|
|
316
|
+
{ apiKey: parsed.apiKey },
|
|
317
|
+
ctx
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
id = id;
|
|
321
|
+
credentials = mailgunCredentials;
|
|
322
|
+
baseUrl() {
|
|
323
|
+
return this.settings.region === "eu" ? "https://api.eu.mailgun.net" : "https://api.mailgun.net";
|
|
324
|
+
}
|
|
325
|
+
buildHeaders() {
|
|
326
|
+
const token = btoa(`api:${this.creds.apiKey}`);
|
|
327
|
+
return {
|
|
328
|
+
Authorization: `Basic ${token}`,
|
|
329
|
+
"Content-Type": "application/json",
|
|
330
|
+
Accept: "application/json",
|
|
331
|
+
"User-Agent": connectorUserAgent("mailgun")
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
domainFilter() {
|
|
335
|
+
return {
|
|
336
|
+
AND: [
|
|
337
|
+
{
|
|
338
|
+
attribute: "domain",
|
|
339
|
+
comparator: "=",
|
|
340
|
+
values: [
|
|
341
|
+
{ label: this.settings.domain, value: this.settings.domain }
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async fetchMetricItems(window, signal) {
|
|
348
|
+
const url = `${this.baseUrl()}/v1/analytics/metrics`;
|
|
349
|
+
const start = new Date(window.startMs).toUTCString();
|
|
350
|
+
const end = new Date(window.endMs).toUTCString();
|
|
351
|
+
const items = [];
|
|
352
|
+
let skip = 0;
|
|
353
|
+
for (; ; ) {
|
|
354
|
+
const body = JSON.stringify({
|
|
355
|
+
start,
|
|
356
|
+
end,
|
|
357
|
+
resolution: "day",
|
|
358
|
+
dimensions: ["time"],
|
|
359
|
+
metrics: METRIC_FIELDS,
|
|
360
|
+
filter: this.domainFilter(),
|
|
361
|
+
include_subaccounts: false,
|
|
362
|
+
include_aggregates: false,
|
|
363
|
+
skip,
|
|
364
|
+
limit: METRICS_PAGE_LIMIT
|
|
365
|
+
});
|
|
366
|
+
const res = await this.post(url, {
|
|
367
|
+
resource: "email_stats",
|
|
368
|
+
headers: this.buildHeaders(),
|
|
369
|
+
body,
|
|
370
|
+
signal
|
|
371
|
+
});
|
|
372
|
+
const page = res.body.items ?? [];
|
|
373
|
+
items.push(...page);
|
|
374
|
+
const total = res.body.pagination?.total ?? null;
|
|
375
|
+
skip += page.length;
|
|
376
|
+
if (page.length < METRICS_PAGE_LIMIT) {
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
if (total !== null && skip >= total) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return items;
|
|
384
|
+
}
|
|
385
|
+
async fetchLogItems(window, signal) {
|
|
386
|
+
const url = `${this.baseUrl()}/v1/analytics/logs`;
|
|
387
|
+
const start = new Date(window.startMs).toUTCString();
|
|
388
|
+
const end = new Date(window.endMs).toUTCString();
|
|
389
|
+
const byId = /* @__PURE__ */ new Map();
|
|
390
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
391
|
+
let token = null;
|
|
392
|
+
for (let page = 0; page < MAX_EVENT_PAGES; page++) {
|
|
393
|
+
const body = JSON.stringify(
|
|
394
|
+
token ? { pagination: { token, limit: EVENTS_PAGE_LIMIT } } : {
|
|
395
|
+
start,
|
|
396
|
+
end,
|
|
397
|
+
events: EVENT_TYPES,
|
|
398
|
+
filter: this.domainFilter(),
|
|
399
|
+
include_subaccounts: false,
|
|
400
|
+
pagination: { sort: "timestamp:asc", limit: EVENTS_PAGE_LIMIT }
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
const res = await this.post(url, {
|
|
404
|
+
resource: "events",
|
|
405
|
+
headers: this.buildHeaders(),
|
|
406
|
+
body,
|
|
407
|
+
signal
|
|
408
|
+
});
|
|
409
|
+
for (const item of res.body.items ?? []) {
|
|
410
|
+
byId.set(item.id, item);
|
|
411
|
+
}
|
|
412
|
+
const next = res.body.pagination?.next ?? null;
|
|
413
|
+
if (!next || seenTokens.has(next) || (res.body.items ?? []).length === 0) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
seenTokens.add(next);
|
|
417
|
+
token = next;
|
|
418
|
+
}
|
|
419
|
+
return Array.from(byId.values());
|
|
420
|
+
}
|
|
421
|
+
async writePhase(storage, phase, window, signal) {
|
|
422
|
+
if (phase === "email_stats") {
|
|
423
|
+
const items2 = await this.fetchMetricItems(window, signal);
|
|
424
|
+
const samples = items2.map(
|
|
425
|
+
(item) => metricsItemToSample(item, this.settings.domain)
|
|
426
|
+
);
|
|
427
|
+
await storage.metrics(samples, {
|
|
428
|
+
names: [EMAIL_STATS_METRIC],
|
|
429
|
+
replaceWindow: { start: window.startMs, end: window.endMs }
|
|
430
|
+
});
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
const items = await this.fetchLogItems(window, signal);
|
|
434
|
+
const events = items.map(
|
|
435
|
+
(item) => logsItemToEvent(item, this.settings.domain)
|
|
436
|
+
);
|
|
437
|
+
await storage.events(events, { names: [MAILGUN_EVENT] });
|
|
438
|
+
}
|
|
439
|
+
async sync(options, storage, signal) {
|
|
440
|
+
const cursor = isMailgunSyncCursor(
|
|
441
|
+
options.cursor
|
|
442
|
+
) ? options.cursor : void 0;
|
|
443
|
+
const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;
|
|
444
|
+
const window = getWindow(options, lookbackDays);
|
|
445
|
+
const phases = selectActivePhases(
|
|
446
|
+
(r) => r,
|
|
447
|
+
PHASE_ORDER,
|
|
448
|
+
this.settings.resources
|
|
449
|
+
);
|
|
450
|
+
return paginateChunked({
|
|
451
|
+
phases,
|
|
452
|
+
cursor,
|
|
453
|
+
signal,
|
|
454
|
+
logger: this.logger,
|
|
455
|
+
fetchPage: async (_phase, _page, _sig) => ({ items: [null], next: null }),
|
|
456
|
+
writeBatch: async (phase, _items, _page) => {
|
|
457
|
+
await this.writePhase(storage, phase, window, signal);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/index.ts
|
|
464
|
+
var index_default = MailgunConnector;
|
|
465
|
+
export {
|
|
466
|
+
MailgunConnector,
|
|
467
|
+
configFields,
|
|
468
|
+
index_default as default,
|
|
469
|
+
doc,
|
|
470
|
+
getWindow,
|
|
471
|
+
id,
|
|
472
|
+
logsItemToEvent,
|
|
473
|
+
metricsItemToSample,
|
|
474
|
+
mailgunResources as resources
|
|
475
|
+
};
|
|
476
|
+
//# 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/map-concurrent.ts","../../../connector-shared/src/sanitize.ts","../../../connector-shared/src/epoch.ts","../../../connector-shared/src/pagination.ts","../../../connector-shared/src/logger.ts","../src/mailgun.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(\n res: Response,\n parseJson: boolean,\n binary: boolean,\n): Promise<unknown> {\n if (res.status === 204 || res.status === 205) {\n return null;\n }\n if (binary) {\n return new Uint8Array(await res.arrayBuffer());\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 const binary = req.binary ?? false;\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, binary);\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 async function mapWithConcurrency<T, R>(\n items: readonly T[],\n concurrency: number,\n fn: (item: T, index: number) => Promise<R>,\n): Promise<R[]> {\n const results = new Array<R>(items.length);\n if (items.length === 0) {\n return results;\n }\n const normalized = Number.isFinite(concurrency) ? Math.floor(concurrency) : 1;\n const limit = Math.max(1, Math.min(normalized, items.length));\n let next = 0;\n let failed = false;\n\n async function worker(): Promise<void> {\n while (!failed) {\n const i = next++;\n if (i >= items.length) {\n return;\n }\n try {\n results[i] = await fn(items[i]!, i);\n } catch (err) {\n failed = true;\n throw err;\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let w = 0; w < limit; w++) {\n workers.push(worker());\n }\n await Promise.all(workers);\n return results;\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 { connectorUserAgent, parseEpoch } from '@rawdash/connector-shared';\nimport {\n BaseConnector,\n type ChunkedSyncCursor,\n type ConnectorContext,\n type ConnectorDoc,\n type CredentialsSchema,\n type Event,\n type MetricSample,\n type StorageHandle,\n type SyncOptions,\n type SyncResult,\n defineConfigFields,\n defineConnectorDoc,\n defineResources,\n makeChunkedCursorGuard,\n metricSample,\n paginateChunked,\n schemasFromResources,\n selectActivePhases,\n} from '@rawdash/core';\nimport { z } from 'zod';\n\nexport const configFields = defineConfigFields(\n z.object({\n apiKey: z.object({ $secret: z.string() }).meta({\n label: 'API key',\n description:\n 'A Mailgun API key with read access to analytics. Create one in the Mailgun dashboard under Settings -> API Keys.',\n placeholder: 'key-...',\n secret: true,\n }),\n domain: z.string().trim().min(1, 'A sending domain is required').meta({\n label: 'Sending domain',\n description:\n 'The Mailgun sending domain to report on (e.g. mg.example.com). Metrics and logs are filtered to this domain.',\n placeholder: 'mg.example.com',\n }),\n region: z.enum(['us', 'eu']).default('us').meta({\n label: 'Region',\n description:\n \"Which Mailgun region hosts the domain. 'us' uses api.mailgun.net; 'eu' uses api.eu.mailgun.net.\",\n placeholder: 'us',\n }),\n lookbackDays: z.number().int().positive().optional().meta({\n label: 'Lookback days (full sync)',\n description:\n 'How many calendar days of stats/events to fetch on a full sync. Defaults to 90.',\n placeholder: '90',\n }),\n resources: z\n .array(z.enum(['email_stats', 'events']))\n .nonempty()\n .optional()\n .meta({\n label: 'Resources',\n description:\n 'Which Mailgun resources to sync. Omit to sync all of them.',\n }),\n }),\n);\n\nexport const doc: ConnectorDoc = defineConnectorDoc({\n displayName: 'Mailgun',\n category: 'engineering',\n brandColor: '#F06B66',\n tagline:\n 'Sync transactional email volume, delivery, bounce, and complaint metrics plus recent delivery events from Mailgun.',\n vendor: {\n name: 'Mailgun',\n domain: 'mailgun.com',\n apiDocs: 'https://documentation.mailgun.com/docs/mailgun/api-reference/',\n website: 'https://www.mailgun.com',\n },\n auth: {\n summary:\n 'A Mailgun API key with read access to analytics, sent via HTTP basic auth (username `api`, password is the key).',\n setup: [\n 'In the Mailgun dashboard open Settings -> API Keys and create or copy an API key with analytics read access.',\n 'Note which region hosts your domain (US or EU); set the connector `region` accordingly.',\n 'Store the key as a secret and reference it from the connector config as `apiKey: secret(\"MAILGUN_API_KEY\")`, and set `domain` to the sending domain you want to report on.',\n ],\n },\n rateLimit:\n 'Mailgun applies per-endpoint rate limits and returns 429 with a Retry-After header when exceeded; the shared HTTP client backs off and retries automatically.',\n limitations: [\n 'Metrics are reported at daily resolution; the connector requests `resolution=day` from the analytics API.',\n 'Incremental syncs re-fetch a fixed trailing window and replace only that window, so older samples are preserved.',\n 'The events resource stores a bounded sample of the most recent delivery logs (Mailgun retains log data for a limited period), not a complete event archive.',\n ],\n});\n\nexport interface MailgunSettings {\n domain: string;\n region: 'us' | 'eu';\n lookbackDays?: number;\n resources?: readonly MailgunResource[];\n}\n\nconst mailgunCredentials = {\n apiKey: {\n description: 'Mailgun API key',\n auth: 'required' as const,\n },\n} satisfies CredentialsSchema;\n\ntype MailgunCredentials = typeof mailgunCredentials;\n\nconst PHASE_ORDER = ['email_stats', 'events'] as const;\n\ntype MailgunPhase = (typeof PHASE_ORDER)[number];\n\nexport type MailgunResource = MailgunPhase;\n\ntype MailgunSyncCursor = ChunkedSyncCursor<MailgunPhase, string>;\n\nconst isMailgunSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);\n\nconst MS_PER_DAY = 24 * 60 * 60 * 1000;\nconst DEFAULT_LOOKBACK_DAYS = 90;\nconst INCREMENTAL_LOOKBACK_DAYS = 7;\nconst METRICS_PAGE_LIMIT = 1000;\nconst EVENTS_PAGE_LIMIT = 300;\nconst MAX_EVENT_PAGES = 20;\n\nconst EMAIL_STATS_METRIC = 'mailgun_email_stats';\nconst MAILGUN_EVENT = 'mailgun_event';\n\nconst METRIC_FIELDS = [\n 'accepted_count',\n 'delivered_count',\n 'failed_count',\n 'opened_count',\n 'clicked_count',\n 'unsubscribed_count',\n 'complained_count',\n] as const;\n\nconst EVENT_TYPES = [\n 'accepted',\n 'delivered',\n 'failed',\n 'opened',\n 'clicked',\n 'unsubscribed',\n 'complained',\n] as const;\n\nconst metricsDimensionSchema = z.object({\n dimension: z.string(),\n value: z.string(),\n display_value: z.string().nullish(),\n});\n\nconst metricsItemSchema = z.object({\n dimensions: z.array(metricsDimensionSchema),\n metrics: z.object({\n accepted_count: z.number().nullish(),\n delivered_count: z.number().nullish(),\n failed_count: z.number().nullish(),\n opened_count: z.number().nullish(),\n clicked_count: z.number().nullish(),\n unsubscribed_count: z.number().nullish(),\n complained_count: z.number().nullish(),\n }),\n});\n\nconst metricsResponseSchema = z.object({\n items: z.array(metricsItemSchema),\n pagination: z\n .object({\n skip: z.number().nullish(),\n limit: z.number().nullish(),\n total: z.number().nullish(),\n })\n .nullish(),\n});\n\nconst logsItemSchema = z.object({\n id: z.string(),\n event: z.string(),\n '@timestamp': z.string(),\n recipient: z.string().nullish(),\n 'recipient-domain': z.string().nullish(),\n severity: z.string().nullish(),\n reason: z.string().nullish(),\n});\n\nconst logsResponseSchema = z.object({\n items: z.array(logsItemSchema),\n pagination: z\n .object({\n next: z.string().nullish(),\n previous: z.string().nullish(),\n total: z.number().nullish(),\n })\n .nullish(),\n});\n\nexport const mailgunResources = defineResources({\n [EMAIL_STATS_METRIC]: {\n shape: 'metric',\n description:\n 'Daily transactional email volume and engagement for the configured domain. The canonical value is `accepted` (messages accepted for sending); delivery, failure, and engagement counts are carried as measures.',\n endpoint: 'POST /v1/analytics/metrics',\n unit: 'emails',\n granularity: 'day',\n dimensions: [\n { name: 'date', description: 'Calendar day of the metric sample (UTC).' },\n { name: 'domain', description: 'The Mailgun sending domain.' },\n ],\n measures: [\n { name: 'delivered', description: 'Messages delivered on the day.' },\n {\n name: 'failed',\n description: 'Messages that failed (bounced/dropped).',\n },\n { name: 'opened', description: 'Message opens recorded on the day.' },\n { name: 'clicked', description: 'Link clicks recorded on the day.' },\n {\n name: 'unsubscribed',\n description: 'Unsubscribes recorded on the day.',\n },\n {\n name: 'complained',\n description: 'Spam complaints recorded on the day.',\n },\n ],\n responses: { email_stats: metricsResponseSchema },\n },\n [MAILGUN_EVENT]: {\n shape: 'event',\n description:\n 'Recent per-message delivery events (accepted, delivered, failed, opened, clicked, unsubscribed, complained) for the configured domain. Deduplicated by Mailgun event id.',\n endpoint: 'POST /v1/analytics/logs',\n notes:\n 'A bounded sample of the most recent logs is stored; Mailgun retains log data for a limited period.',\n fields: [\n { name: 'eventId', description: 'Mailgun event id (stable per event).' },\n {\n name: 'eventType',\n description:\n 'Event type (accepted, delivered, failed, opened, clicked, unsubscribed, complained).',\n },\n { name: 'recipient', description: 'Recipient email address.' },\n { name: 'domain', description: 'The Mailgun sending domain.' },\n { name: 'severity', description: 'Failure severity, when present.' },\n { name: 'reason', description: 'Failure reason, when present.' },\n ],\n filterable: [],\n responses: { events: logsResponseSchema },\n },\n});\n\nexport type MailgunMetricsItem = z.infer<typeof metricsItemSchema>;\nexport type MailgunLogsItem = z.infer<typeof logsItemSchema>;\n\nexport interface MailgunWindow {\n startMs: number;\n endMs: number;\n}\n\nfunction startOfUtcDay(ms: number): number {\n return Math.floor(ms / MS_PER_DAY) * MS_PER_DAY;\n}\n\nfunction pad2(n: number): string {\n return String(n).padStart(2, '0');\n}\n\nfunction toIsoDate(ms: number): string {\n const d = new Date(ms);\n return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;\n}\n\nexport function getWindow(\n options: SyncOptions,\n lookbackDays: number,\n now: number = Date.now(),\n): MailgunWindow {\n const endMs = startOfUtcDay(now) + MS_PER_DAY - 1;\n const today = startOfUtcDay(now);\n if (options.mode === 'latest') {\n return {\n startMs: today - (INCREMENTAL_LOOKBACK_DAYS - 1) * MS_PER_DAY,\n endMs,\n };\n }\n if (options.since) {\n const sinceMs = new Date(options.since).getTime();\n if (Number.isFinite(sinceMs)) {\n const requested = Math.max(\n 1,\n Math.ceil((today - startOfUtcDay(sinceMs)) / MS_PER_DAY) + 1,\n );\n const capped = Math.min(requested, lookbackDays);\n return { startMs: today - (capped - 1) * MS_PER_DAY, endMs };\n }\n }\n return { startMs: today - (lookbackDays - 1) * MS_PER_DAY, endMs };\n}\n\nfunction toNumber(value: number | null | undefined): number {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0;\n}\n\nfunction timeDimensionValue(item: MailgunMetricsItem): string | null {\n const timeDim =\n item.dimensions.find((d) => d.dimension === 'time') ?? item.dimensions[0];\n return timeDim ? timeDim.value : null;\n}\n\nexport function metricsItemToSample(\n item: MailgunMetricsItem,\n domain: string,\n): MetricSample {\n const raw = timeDimensionValue(item);\n const parsed = raw ? parseEpoch(raw, 'iso') : null;\n const ts = parsed ?? (raw ? Date.parse(raw) : NaN);\n const tsMs = Number.isFinite(ts) ? ts : 0;\n const date = Number.isFinite(ts) ? toIsoDate(tsMs) : (raw ?? '');\n const m = item.metrics;\n return metricSample(mailgunResources, EMAIL_STATS_METRIC, {\n ts: tsMs,\n value: toNumber(m.accepted_count),\n attributes: {\n date,\n domain,\n delivered: toNumber(m.delivered_count),\n failed: toNumber(m.failed_count),\n opened: toNumber(m.opened_count),\n clicked: toNumber(m.clicked_count),\n unsubscribed: toNumber(m.unsubscribed_count),\n complained: toNumber(m.complained_count),\n },\n });\n}\n\nexport function logsItemToEvent(item: MailgunLogsItem, domain: string): Event {\n const parsed = parseEpoch(item['@timestamp'], 'iso');\n const ts = parsed ?? Date.parse(item['@timestamp']);\n const startTs = Number.isFinite(ts) ? ts : 0;\n return {\n name: MAILGUN_EVENT,\n start_ts: startTs,\n end_ts: startTs,\n attributes: {\n eventId: item.id,\n eventType: item.event,\n recipient: item.recipient ?? null,\n domain,\n severity: item.severity ?? null,\n reason: item.reason ?? null,\n },\n };\n}\n\nexport const id = 'mailgun';\n\nexport class MailgunConnector extends BaseConnector<\n MailgunSettings,\n MailgunCredentials\n> {\n static readonly id = id;\n\n static readonly resources = mailgunResources;\n\n static readonly schemas = schemasFromResources(mailgunResources);\n\n static create(input: unknown, ctx?: ConnectorContext): MailgunConnector {\n const parsed = configFields.parse(input);\n return new MailgunConnector(\n {\n domain: parsed.domain,\n region: parsed.region,\n lookbackDays: parsed.lookbackDays,\n resources: parsed.resources,\n },\n { apiKey: parsed.apiKey },\n ctx,\n );\n }\n\n readonly id = id;\n override readonly credentials = mailgunCredentials;\n\n private baseUrl(): string {\n return this.settings.region === 'eu'\n ? 'https://api.eu.mailgun.net'\n : 'https://api.mailgun.net';\n }\n\n private buildHeaders(): Record<string, string> {\n const token = btoa(`api:${this.creds.apiKey}`);\n return {\n Authorization: `Basic ${token}`,\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'User-Agent': connectorUserAgent('mailgun'),\n };\n }\n\n private domainFilter(): Record<string, unknown> {\n return {\n AND: [\n {\n attribute: 'domain',\n comparator: '=',\n values: [\n { label: this.settings.domain, value: this.settings.domain },\n ],\n },\n ],\n };\n }\n\n private async fetchMetricItems(\n window: MailgunWindow,\n signal?: AbortSignal,\n ): Promise<MailgunMetricsItem[]> {\n const url = `${this.baseUrl()}/v1/analytics/metrics`;\n const start = new Date(window.startMs).toUTCString();\n const end = new Date(window.endMs).toUTCString();\n const items: MailgunMetricsItem[] = [];\n let skip = 0;\n for (;;) {\n const body = JSON.stringify({\n start,\n end,\n resolution: 'day',\n dimensions: ['time'],\n metrics: METRIC_FIELDS,\n filter: this.domainFilter(),\n include_subaccounts: false,\n include_aggregates: false,\n skip,\n limit: METRICS_PAGE_LIMIT,\n });\n const res = await this.post<z.infer<typeof metricsResponseSchema>>(url, {\n resource: 'email_stats',\n headers: this.buildHeaders(),\n body,\n signal,\n });\n const page = res.body.items ?? [];\n items.push(...page);\n const total = res.body.pagination?.total ?? null;\n skip += page.length;\n if (page.length < METRICS_PAGE_LIMIT) {\n break;\n }\n if (total !== null && skip >= total) {\n break;\n }\n }\n return items;\n }\n\n private async fetchLogItems(\n window: MailgunWindow,\n signal?: AbortSignal,\n ): Promise<MailgunLogsItem[]> {\n const url = `${this.baseUrl()}/v1/analytics/logs`;\n const start = new Date(window.startMs).toUTCString();\n const end = new Date(window.endMs).toUTCString();\n const byId = new Map<string, MailgunLogsItem>();\n const seenTokens = new Set<string>();\n let token: string | null = null;\n for (let page = 0; page < MAX_EVENT_PAGES; page++) {\n const body: string = JSON.stringify(\n token\n ? { pagination: { token, limit: EVENTS_PAGE_LIMIT } }\n : {\n start,\n end,\n events: EVENT_TYPES,\n filter: this.domainFilter(),\n include_subaccounts: false,\n pagination: { sort: 'timestamp:asc', limit: EVENTS_PAGE_LIMIT },\n },\n );\n const res = await this.post<z.infer<typeof logsResponseSchema>>(url, {\n resource: 'events',\n headers: this.buildHeaders(),\n body,\n signal,\n });\n for (const item of res.body.items ?? []) {\n byId.set(item.id, item);\n }\n const next: string | null = res.body.pagination?.next ?? null;\n if (\n !next ||\n seenTokens.has(next) ||\n (res.body.items ?? []).length === 0\n ) {\n break;\n }\n seenTokens.add(next);\n token = next;\n }\n return Array.from(byId.values());\n }\n\n private async writePhase(\n storage: StorageHandle,\n phase: MailgunPhase,\n window: MailgunWindow,\n signal?: AbortSignal,\n ): Promise<void> {\n if (phase === 'email_stats') {\n const items = await this.fetchMetricItems(window, signal);\n const samples = items.map((item) =>\n metricsItemToSample(item, this.settings.domain),\n );\n await storage.metrics(samples, {\n names: [EMAIL_STATS_METRIC],\n replaceWindow: { start: window.startMs, end: window.endMs },\n });\n return;\n }\n const items = await this.fetchLogItems(window, signal);\n const events = items.map((item) =>\n logsItemToEvent(item, this.settings.domain),\n );\n await storage.events(events, { names: [MAILGUN_EVENT] });\n }\n\n async sync(\n options: SyncOptions,\n storage: StorageHandle,\n signal?: AbortSignal,\n ): Promise<SyncResult> {\n const cursor: MailgunSyncCursor | undefined = isMailgunSyncCursor(\n options.cursor,\n )\n ? options.cursor\n : undefined;\n const lookbackDays = this.settings.lookbackDays ?? DEFAULT_LOOKBACK_DAYS;\n const window = getWindow(options, lookbackDays);\n\n const phases = selectActivePhases<MailgunResource, MailgunPhase>(\n (r) => r,\n PHASE_ORDER,\n this.settings.resources,\n );\n\n return paginateChunked<MailgunPhase, string>({\n phases,\n cursor,\n signal,\n logger: this.logger,\n fetchPage: async (_phase, _page, _sig) => ({ items: [null], next: null }),\n writeBatch: async (phase, _items, _page) => {\n await this.writePhase(storage, phase, window, signal);\n },\n });\n }\n}\n","import { MailgunConnector } from './mailgun';\n\nexport {\n MailgunConnector,\n configFields,\n doc,\n getWindow,\n id,\n logsItemToEvent,\n mailgunResources as resources,\n metricsItemToSample,\n} from './mailgun';\nexport type {\n MailgunLogsItem,\n MailgunMetricsItem,\n MailgunResource,\n MailgunSettings,\n MailgunWindow,\n} from './mailgun';\nexport default MailgunConnector;\n"],"mappings":"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","names":["items"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rawdash/connector-mailgun",
|
|
3
|
+
"version": "0.28.0",
|
|
4
|
+
"description": "Rawdash connector for Mailgun — syncs daily transactional email metrics (accepted, delivered, failed, opens, clicks, unsubscribes, complaints) and recent delivery events via the Mailgun Analytics API",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/rawdash/rawdash.git",
|
|
11
|
+
"directory": "packages/connectors/mailgun"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"@rawdash/source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"lint": "eslint src",
|
|
29
|
+
"test": "vitest run"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@rawdash/core": "workspace:*",
|
|
33
|
+
"zod": "^4.4.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@rawdash/connector-shared": "workspace:*",
|
|
37
|
+
"@rawdash/connector-test-utils": "workspace:*",
|
|
38
|
+
"fast-check": "^4.8.0",
|
|
39
|
+
"tsup": "^8.0.0",
|
|
40
|
+
"typescript": "^5.7.2",
|
|
41
|
+
"vitest": "^4.1.4"
|
|
42
|
+
}
|
|
43
|
+
}
|