@drawbridge/drawbridge-utils 0.0.112 → 0.0.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connections/index.cjs +2534 -595
- package/dist/connections/index.d.cts +2343 -612
- package/dist/connections/index.d.ts +2343 -612
- package/dist/connections/index.js +2512 -589
- package/dist/connections/oauth.cjs +49 -50
- package/dist/connections/oauth.d.cts +83 -70
- package/dist/connections/oauth.d.ts +83 -70
- package/dist/connections/oauth.js +47 -47
- package/dist/features.cjs +15 -0
- package/dist/features.d.cts +15 -0
- package/dist/features.d.ts +15 -0
- package/dist/features.js +15 -0
- package/dist/fields-validate.js +1 -1
- package/dist/phone.cjs +12 -0
- package/dist/phone.d.cts +30 -2
- package/dist/phone.d.ts +30 -2
- package/dist/phone.js +12 -1
- package/dist/plans.cjs +17 -0
- package/dist/plans.d.cts +2 -0
- package/dist/plans.d.ts +2 -0
- package/dist/plans.js +17 -0
- package/dist/pricing.cjs +17 -0
- package/dist/pricing.js +17 -0
- package/dist/safe-http.d.cts +1 -1
- package/dist/safe-http.d.ts +1 -1
- package/dist/sendgrid.cjs +168 -0
- package/dist/sendgrid.d.cts +185 -0
- package/dist/sendgrid.d.ts +185 -0
- package/dist/sendgrid.js +140 -0
- package/dist/twilio.cjs +112 -0
- package/dist/twilio.d.cts +64 -0
- package/dist/twilio.d.ts +64 -0
- package/dist/twilio.js +86 -0
- package/package.json +12 -2
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// lib/sendgrid.js
|
|
20
|
+
var sendgrid_exports = {};
|
|
21
|
+
__export(sendgrid_exports, {
|
|
22
|
+
sendWithRetry: () => sendWithRetry,
|
|
23
|
+
sendgrid: () => sendgrid,
|
|
24
|
+
sendgridRequest: () => sendgridRequest
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(sendgrid_exports);
|
|
27
|
+
|
|
28
|
+
// lib/http.js
|
|
29
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
30
|
+
var request = async ({
|
|
31
|
+
body,
|
|
32
|
+
headers = {},
|
|
33
|
+
method = "GET",
|
|
34
|
+
query,
|
|
35
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
36
|
+
type = "json",
|
|
37
|
+
url
|
|
38
|
+
}) => {
|
|
39
|
+
const fullUrl = new URL(url);
|
|
40
|
+
if (query) {
|
|
41
|
+
Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
|
|
42
|
+
}
|
|
43
|
+
;
|
|
44
|
+
const isForm = type === "form";
|
|
45
|
+
const response = await fetch(fullUrl.toString(), {
|
|
46
|
+
method,
|
|
47
|
+
headers: {
|
|
48
|
+
"Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
|
|
49
|
+
...headers
|
|
50
|
+
},
|
|
51
|
+
signal: AbortSignal.timeout(timeout),
|
|
52
|
+
...body !== void 0 && {
|
|
53
|
+
body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const text2 = await response.text().catch(() => "");
|
|
58
|
+
const error = new Error(text2 || response.statusText);
|
|
59
|
+
error.status = response.status;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
;
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
try {
|
|
65
|
+
return text ? JSON.parse(text) : null;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// lib/sendgrid.js
|
|
72
|
+
var import_drawbridge_telemetry = require("@drawbridge/drawbridge-telemetry");
|
|
73
|
+
var logger = (0, import_drawbridge_telemetry.createLogger)();
|
|
74
|
+
var SENDGRID_BASE = "https://api.sendgrid.com";
|
|
75
|
+
var RETRYABLE_STATUSES = [429, 500, 502, 503];
|
|
76
|
+
var RETRY_DELAYS_MS = [1e3, 4e3];
|
|
77
|
+
var sendWithRetry = async (send, { delays = RETRY_DELAYS_MS } = {}) => {
|
|
78
|
+
for (let attempt = 0; ; attempt++) {
|
|
79
|
+
try {
|
|
80
|
+
return await send();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const status = Number(error == null ? void 0 : error.status);
|
|
83
|
+
const retryable = RETRYABLE_STATUSES.includes(status);
|
|
84
|
+
if (!retryable || attempt >= delays.length) {
|
|
85
|
+
if (!status) {
|
|
86
|
+
logger.warn("email.send.ambiguous-failure", {
|
|
87
|
+
name: error == null ? void 0 : error.name,
|
|
88
|
+
message: error == null ? void 0 : error.message
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
logger.info("email.send.retry", {
|
|
94
|
+
attempt: attempt + 1,
|
|
95
|
+
status,
|
|
96
|
+
delayMs: delays[attempt]
|
|
97
|
+
});
|
|
98
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var sendgridRequest = ({ apiKey, body, method, path, query, request: request2 = request }) => {
|
|
103
|
+
const key = apiKey || process.env.SENDGRID_API_KEY;
|
|
104
|
+
return request2({
|
|
105
|
+
body,
|
|
106
|
+
headers: {
|
|
107
|
+
"Authorization": "Bearer " + key
|
|
108
|
+
},
|
|
109
|
+
method,
|
|
110
|
+
query,
|
|
111
|
+
url: SENDGRID_BASE + path
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
var sendgrid = {
|
|
115
|
+
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
116
|
+
// commercial sends; omitted, the request body is byte-identical to the
|
|
117
|
+
// pre-opt-out-floor shape so system mail is untouched.
|
|
118
|
+
send: async ({ apiKey, from, headers, html, request: request2, subject, text, to }) => {
|
|
119
|
+
var _a, _b;
|
|
120
|
+
try {
|
|
121
|
+
const sender = (from == null ? void 0 : from.email) ? from : {
|
|
122
|
+
email: process.env.SENDGRID_FROM_ADDRESS,
|
|
123
|
+
name: "Drawbridge"
|
|
124
|
+
};
|
|
125
|
+
await sendWithRetry(() => sendgridRequest({
|
|
126
|
+
apiKey,
|
|
127
|
+
...request2 && { request: request2 },
|
|
128
|
+
method: "POST",
|
|
129
|
+
path: "/v3/mail/send",
|
|
130
|
+
body: {
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "text/plain",
|
|
134
|
+
value: text
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: "text/html",
|
|
138
|
+
value: html
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
from: sender,
|
|
142
|
+
...headers && { headers },
|
|
143
|
+
personalizations: [
|
|
144
|
+
{
|
|
145
|
+
to: [
|
|
146
|
+
{ email: to }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
subject
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
} catch (error) {
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(error.message);
|
|
156
|
+
if ((_b = (_a = parsed == null ? void 0 : parsed.errors) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) error.message = parsed.errors[0].message;
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
164
|
+
0 && (module.exports = {
|
|
165
|
+
sendWithRetry,
|
|
166
|
+
sendgrid,
|
|
167
|
+
sendgridRequest
|
|
168
|
+
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { request } from './http.cjs';
|
|
2
|
+
import { createLogger } from '@drawbridge/drawbridge-telemetry';
|
|
3
|
+
|
|
4
|
+
// SENDGRID TRANSPORT. Vendor HTTP and nothing else — no controller, no queue,
|
|
5
|
+
// no database.
|
|
6
|
+
//
|
|
7
|
+
// HERE rather than in drawbridge-sync because every other vendor's transport is
|
|
8
|
+
// here: Klaviyo's api(), Shopify's signature verification, Mailchimp's fetch.
|
|
9
|
+
// SendGrid was the one exception, and Drawbridge is a connection now like any
|
|
10
|
+
// other — the private `drawbridge` manifest declares email.send, email.notify
|
|
11
|
+
// and email.digest, so its transport belongs beside its manifest.
|
|
12
|
+
//
|
|
13
|
+
// The API key still falls back to process.env, which is a carry-over rather than
|
|
14
|
+
// a preference: a published package reading a deployment's environment is the
|
|
15
|
+
// thing `client : { id : 'KLAVIYO_OAUTH_CLIENT_ID' }` exists to avoid. Callers
|
|
16
|
+
// that pass a key explicitly never touch it.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const logger = createLogger();
|
|
20
|
+
|
|
21
|
+
const SENDGRID_BASE = 'https://api.sendgrid.com';
|
|
22
|
+
|
|
23
|
+
// --- provider-send retry -----------------------------------------------------
|
|
24
|
+
//
|
|
25
|
+
// Notification jobs are non-retryable BY DESIGN (non-idempotent $incs on the
|
|
26
|
+
// user-notification path), so a provider hiccup used to mean a silently
|
|
27
|
+
// dropped email. This retry wraps ONLY the provider POST, and only fires on
|
|
28
|
+
// DEFINITE pre-delivery failures — an HTTP status proving the provider
|
|
29
|
+
// REJECTED the request (rate limit / server error), where a retry can never
|
|
30
|
+
// double-send. Ambiguous outcomes (timeout, abort, connection reset — no
|
|
31
|
+
// status, because the request died in transit) are NOT retried: the provider
|
|
32
|
+
// may already have accepted the mail, and retrying risks a duplicate email in
|
|
33
|
+
// a real person's inbox. Those are logged and rethrown (plan guard-rail 12).
|
|
34
|
+
const RETRYABLE_STATUSES = [ 429, 500, 502, 503 ];
|
|
35
|
+
|
|
36
|
+
// 1s then 4s — long enough for a rate-limit window to roll over, short enough
|
|
37
|
+
// to keep the send inside the notification job's runtime budget.
|
|
38
|
+
const RETRY_DELAYS_MS = [ 1000, 4000 ];
|
|
39
|
+
|
|
40
|
+
const sendWithRetry = async ( send, { delays = RETRY_DELAYS_MS } = {} ) => {
|
|
41
|
+
|
|
42
|
+
for( let attempt = 0; ; attempt++ ){
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
|
|
46
|
+
return await send();
|
|
47
|
+
|
|
48
|
+
} catch ( error ) {
|
|
49
|
+
|
|
50
|
+
const status = Number( error?.status );
|
|
51
|
+
const retryable = RETRYABLE_STATUSES.includes( status );
|
|
52
|
+
|
|
53
|
+
if( ! retryable || attempt >= delays.length ){
|
|
54
|
+
|
|
55
|
+
if( ! status ){
|
|
56
|
+
|
|
57
|
+
// No HTTP status = timeout / abort / reset — the ambiguous
|
|
58
|
+
// "maybe the provider accepted it" case. Surface it in logs
|
|
59
|
+
// so possible-but-unconfirmed sends are searchable.
|
|
60
|
+
logger.warn( 'email.send.ambiguous-failure', {
|
|
61
|
+
name : error?.name,
|
|
62
|
+
message : error?.message
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw error;
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.info( 'email.send.retry', {
|
|
72
|
+
attempt : attempt + 1,
|
|
73
|
+
status,
|
|
74
|
+
delayMs : delays[ attempt ]
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await new Promise( ( resolve ) => setTimeout( resolve, delays[ attempt ] ) );
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// `request` is INJECTABLE, and that is not decoration. tsup emits this module's
|
|
86
|
+
// exports as getter-only, non-configurable properties, so the stub-by-mutation
|
|
87
|
+
// trick that worked when this lived in drawbridge-sync silently no-ops here —
|
|
88
|
+
// the assignment appears to succeed and the real HTTP call goes out. Injection
|
|
89
|
+
// is how every other vendor call in this package is tested (`fetcher` on each
|
|
90
|
+
// manifest hook), and it is the only thing that works across the boundary.
|
|
91
|
+
const sendgridRequest = ({ apiKey, body, method, path, query, request: request$1 = request }) => {
|
|
92
|
+
|
|
93
|
+
const key = apiKey || process.env.SENDGRID_API_KEY;
|
|
94
|
+
|
|
95
|
+
return request$1({
|
|
96
|
+
body,
|
|
97
|
+
headers : {
|
|
98
|
+
'Authorization' : 'Bearer ' + key
|
|
99
|
+
},
|
|
100
|
+
method,
|
|
101
|
+
query,
|
|
102
|
+
url : SENDGRID_BASE + path
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sendgrid = {
|
|
108
|
+
|
|
109
|
+
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
110
|
+
// commercial sends; omitted, the request body is byte-identical to the
|
|
111
|
+
// pre-opt-out-floor shape so system mail is untouched.
|
|
112
|
+
send : async ({ apiKey, from, headers, html, request, subject, text, to }) => {
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
|
|
116
|
+
const sender = ( from?.email ?
|
|
117
|
+
from
|
|
118
|
+
:
|
|
119
|
+
{
|
|
120
|
+
email : process.env.SENDGRID_FROM_ADDRESS,
|
|
121
|
+
name : 'Drawbridge'
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await sendWithRetry( () => sendgridRequest({
|
|
126
|
+
apiKey,
|
|
127
|
+
...( request && { request }),
|
|
128
|
+
method : 'POST',
|
|
129
|
+
path : '/v3/mail/send',
|
|
130
|
+
body : {
|
|
131
|
+
content : [
|
|
132
|
+
{
|
|
133
|
+
type : 'text/plain',
|
|
134
|
+
value : text
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type : 'text/html',
|
|
138
|
+
value : html
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
from : sender,
|
|
142
|
+
...( headers && { headers } ),
|
|
143
|
+
personalizations : [
|
|
144
|
+
{
|
|
145
|
+
to : [
|
|
146
|
+
{ email : to }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
subject
|
|
151
|
+
}
|
|
152
|
+
}) );
|
|
153
|
+
|
|
154
|
+
} catch ( error ) {
|
|
155
|
+
|
|
156
|
+
// SendGrid rejections carry a JSON body; surface its first message.
|
|
157
|
+
// Non-JSON failures (timeouts, resets) pass through untouched so the
|
|
158
|
+
// parse can't mask the real error, and error.status survives for the
|
|
159
|
+
// caller.
|
|
160
|
+
try {
|
|
161
|
+
|
|
162
|
+
const parsed = JSON.parse( error.message );
|
|
163
|
+
|
|
164
|
+
if( parsed?.errors?.[ 0 ]?.message ) error.message = parsed.errors[ 0 ].message;
|
|
165
|
+
|
|
166
|
+
} catch {}
|
|
167
|
+
|
|
168
|
+
throw error;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// The Mandrill sender that used to live here is gone. Drawbridge sends every
|
|
177
|
+
// lead-facing email from its own SendGrid account now, so there was no caller
|
|
178
|
+
// left — and the merchant `mailchimp` connection is becoming an audience sync,
|
|
179
|
+
// which speaks to the Marketing API rather than Mandrill's transactional one.
|
|
180
|
+
|
|
181
|
+
// sendgridRequest is exported so the sending-domain slice can call SendGrid's
|
|
182
|
+
// domain-authentication endpoints (/v3/whitelabel/domains) through the same
|
|
183
|
+
// authenticated client the mail send uses, rather than minting a second one.
|
|
184
|
+
|
|
185
|
+
export { sendWithRetry, sendgrid, sendgridRequest };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { request } from './http.js';
|
|
2
|
+
import { createLogger } from '@drawbridge/drawbridge-telemetry';
|
|
3
|
+
|
|
4
|
+
// SENDGRID TRANSPORT. Vendor HTTP and nothing else — no controller, no queue,
|
|
5
|
+
// no database.
|
|
6
|
+
//
|
|
7
|
+
// HERE rather than in drawbridge-sync because every other vendor's transport is
|
|
8
|
+
// here: Klaviyo's api(), Shopify's signature verification, Mailchimp's fetch.
|
|
9
|
+
// SendGrid was the one exception, and Drawbridge is a connection now like any
|
|
10
|
+
// other — the private `drawbridge` manifest declares email.send, email.notify
|
|
11
|
+
// and email.digest, so its transport belongs beside its manifest.
|
|
12
|
+
//
|
|
13
|
+
// The API key still falls back to process.env, which is a carry-over rather than
|
|
14
|
+
// a preference: a published package reading a deployment's environment is the
|
|
15
|
+
// thing `client : { id : 'KLAVIYO_OAUTH_CLIENT_ID' }` exists to avoid. Callers
|
|
16
|
+
// that pass a key explicitly never touch it.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
const logger = createLogger();
|
|
20
|
+
|
|
21
|
+
const SENDGRID_BASE = 'https://api.sendgrid.com';
|
|
22
|
+
|
|
23
|
+
// --- provider-send retry -----------------------------------------------------
|
|
24
|
+
//
|
|
25
|
+
// Notification jobs are non-retryable BY DESIGN (non-idempotent $incs on the
|
|
26
|
+
// user-notification path), so a provider hiccup used to mean a silently
|
|
27
|
+
// dropped email. This retry wraps ONLY the provider POST, and only fires on
|
|
28
|
+
// DEFINITE pre-delivery failures — an HTTP status proving the provider
|
|
29
|
+
// REJECTED the request (rate limit / server error), where a retry can never
|
|
30
|
+
// double-send. Ambiguous outcomes (timeout, abort, connection reset — no
|
|
31
|
+
// status, because the request died in transit) are NOT retried: the provider
|
|
32
|
+
// may already have accepted the mail, and retrying risks a duplicate email in
|
|
33
|
+
// a real person's inbox. Those are logged and rethrown (plan guard-rail 12).
|
|
34
|
+
const RETRYABLE_STATUSES = [ 429, 500, 502, 503 ];
|
|
35
|
+
|
|
36
|
+
// 1s then 4s — long enough for a rate-limit window to roll over, short enough
|
|
37
|
+
// to keep the send inside the notification job's runtime budget.
|
|
38
|
+
const RETRY_DELAYS_MS = [ 1000, 4000 ];
|
|
39
|
+
|
|
40
|
+
const sendWithRetry = async ( send, { delays = RETRY_DELAYS_MS } = {} ) => {
|
|
41
|
+
|
|
42
|
+
for( let attempt = 0; ; attempt++ ){
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
|
|
46
|
+
return await send();
|
|
47
|
+
|
|
48
|
+
} catch ( error ) {
|
|
49
|
+
|
|
50
|
+
const status = Number( error?.status );
|
|
51
|
+
const retryable = RETRYABLE_STATUSES.includes( status );
|
|
52
|
+
|
|
53
|
+
if( ! retryable || attempt >= delays.length ){
|
|
54
|
+
|
|
55
|
+
if( ! status ){
|
|
56
|
+
|
|
57
|
+
// No HTTP status = timeout / abort / reset — the ambiguous
|
|
58
|
+
// "maybe the provider accepted it" case. Surface it in logs
|
|
59
|
+
// so possible-but-unconfirmed sends are searchable.
|
|
60
|
+
logger.warn( 'email.send.ambiguous-failure', {
|
|
61
|
+
name : error?.name,
|
|
62
|
+
message : error?.message
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw error;
|
|
68
|
+
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
logger.info( 'email.send.retry', {
|
|
72
|
+
attempt : attempt + 1,
|
|
73
|
+
status,
|
|
74
|
+
delayMs : delays[ attempt ]
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
await new Promise( ( resolve ) => setTimeout( resolve, delays[ attempt ] ) );
|
|
78
|
+
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// `request` is INJECTABLE, and that is not decoration. tsup emits this module's
|
|
86
|
+
// exports as getter-only, non-configurable properties, so the stub-by-mutation
|
|
87
|
+
// trick that worked when this lived in drawbridge-sync silently no-ops here —
|
|
88
|
+
// the assignment appears to succeed and the real HTTP call goes out. Injection
|
|
89
|
+
// is how every other vendor call in this package is tested (`fetcher` on each
|
|
90
|
+
// manifest hook), and it is the only thing that works across the boundary.
|
|
91
|
+
const sendgridRequest = ({ apiKey, body, method, path, query, request: request$1 = request }) => {
|
|
92
|
+
|
|
93
|
+
const key = apiKey || process.env.SENDGRID_API_KEY;
|
|
94
|
+
|
|
95
|
+
return request$1({
|
|
96
|
+
body,
|
|
97
|
+
headers : {
|
|
98
|
+
'Authorization' : 'Bearer ' + key
|
|
99
|
+
},
|
|
100
|
+
method,
|
|
101
|
+
query,
|
|
102
|
+
url : SENDGRID_BASE + path
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const sendgrid = {
|
|
108
|
+
|
|
109
|
+
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
110
|
+
// commercial sends; omitted, the request body is byte-identical to the
|
|
111
|
+
// pre-opt-out-floor shape so system mail is untouched.
|
|
112
|
+
send : async ({ apiKey, from, headers, html, request, subject, text, to }) => {
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
|
|
116
|
+
const sender = ( from?.email ?
|
|
117
|
+
from
|
|
118
|
+
:
|
|
119
|
+
{
|
|
120
|
+
email : process.env.SENDGRID_FROM_ADDRESS,
|
|
121
|
+
name : 'Drawbridge'
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await sendWithRetry( () => sendgridRequest({
|
|
126
|
+
apiKey,
|
|
127
|
+
...( request && { request }),
|
|
128
|
+
method : 'POST',
|
|
129
|
+
path : '/v3/mail/send',
|
|
130
|
+
body : {
|
|
131
|
+
content : [
|
|
132
|
+
{
|
|
133
|
+
type : 'text/plain',
|
|
134
|
+
value : text
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type : 'text/html',
|
|
138
|
+
value : html
|
|
139
|
+
}
|
|
140
|
+
],
|
|
141
|
+
from : sender,
|
|
142
|
+
...( headers && { headers } ),
|
|
143
|
+
personalizations : [
|
|
144
|
+
{
|
|
145
|
+
to : [
|
|
146
|
+
{ email : to }
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
subject
|
|
151
|
+
}
|
|
152
|
+
}) );
|
|
153
|
+
|
|
154
|
+
} catch ( error ) {
|
|
155
|
+
|
|
156
|
+
// SendGrid rejections carry a JSON body; surface its first message.
|
|
157
|
+
// Non-JSON failures (timeouts, resets) pass through untouched so the
|
|
158
|
+
// parse can't mask the real error, and error.status survives for the
|
|
159
|
+
// caller.
|
|
160
|
+
try {
|
|
161
|
+
|
|
162
|
+
const parsed = JSON.parse( error.message );
|
|
163
|
+
|
|
164
|
+
if( parsed?.errors?.[ 0 ]?.message ) error.message = parsed.errors[ 0 ].message;
|
|
165
|
+
|
|
166
|
+
} catch {}
|
|
167
|
+
|
|
168
|
+
throw error;
|
|
169
|
+
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// The Mandrill sender that used to live here is gone. Drawbridge sends every
|
|
177
|
+
// lead-facing email from its own SendGrid account now, so there was no caller
|
|
178
|
+
// left — and the merchant `mailchimp` connection is becoming an audience sync,
|
|
179
|
+
// which speaks to the Marketing API rather than Mandrill's transactional one.
|
|
180
|
+
|
|
181
|
+
// sendgridRequest is exported so the sending-domain slice can call SendGrid's
|
|
182
|
+
// domain-authentication endpoints (/v3/whitelabel/domains) through the same
|
|
183
|
+
// authenticated client the mail send uses, rather than minting a second one.
|
|
184
|
+
|
|
185
|
+
export { sendWithRetry, sendgrid, sendgridRequest };
|
package/dist/sendgrid.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// lib/http.js
|
|
2
|
+
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
3
|
+
var request = async ({
|
|
4
|
+
body,
|
|
5
|
+
headers = {},
|
|
6
|
+
method = "GET",
|
|
7
|
+
query,
|
|
8
|
+
timeout = DEFAULT_TIMEOUT_MS,
|
|
9
|
+
type = "json",
|
|
10
|
+
url
|
|
11
|
+
}) => {
|
|
12
|
+
const fullUrl = new URL(url);
|
|
13
|
+
if (query) {
|
|
14
|
+
Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
|
|
15
|
+
}
|
|
16
|
+
;
|
|
17
|
+
const isForm = type === "form";
|
|
18
|
+
const response = await fetch(fullUrl.toString(), {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
"Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
|
|
22
|
+
...headers
|
|
23
|
+
},
|
|
24
|
+
signal: AbortSignal.timeout(timeout),
|
|
25
|
+
...body !== void 0 && {
|
|
26
|
+
body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
const text2 = await response.text().catch(() => "");
|
|
31
|
+
const error = new Error(text2 || response.statusText);
|
|
32
|
+
error.status = response.status;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
;
|
|
36
|
+
const text = await response.text();
|
|
37
|
+
try {
|
|
38
|
+
return text ? JSON.parse(text) : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// lib/sendgrid.js
|
|
45
|
+
import { createLogger } from "@drawbridge/drawbridge-telemetry";
|
|
46
|
+
var logger = createLogger();
|
|
47
|
+
var SENDGRID_BASE = "https://api.sendgrid.com";
|
|
48
|
+
var RETRYABLE_STATUSES = [429, 500, 502, 503];
|
|
49
|
+
var RETRY_DELAYS_MS = [1e3, 4e3];
|
|
50
|
+
var sendWithRetry = async (send, { delays = RETRY_DELAYS_MS } = {}) => {
|
|
51
|
+
for (let attempt = 0; ; attempt++) {
|
|
52
|
+
try {
|
|
53
|
+
return await send();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const status = Number(error == null ? void 0 : error.status);
|
|
56
|
+
const retryable = RETRYABLE_STATUSES.includes(status);
|
|
57
|
+
if (!retryable || attempt >= delays.length) {
|
|
58
|
+
if (!status) {
|
|
59
|
+
logger.warn("email.send.ambiguous-failure", {
|
|
60
|
+
name: error == null ? void 0 : error.name,
|
|
61
|
+
message: error == null ? void 0 : error.message
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
logger.info("email.send.retry", {
|
|
67
|
+
attempt: attempt + 1,
|
|
68
|
+
status,
|
|
69
|
+
delayMs: delays[attempt]
|
|
70
|
+
});
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var sendgridRequest = ({ apiKey, body, method, path, query, request: request2 = request }) => {
|
|
76
|
+
const key = apiKey || process.env.SENDGRID_API_KEY;
|
|
77
|
+
return request2({
|
|
78
|
+
body,
|
|
79
|
+
headers: {
|
|
80
|
+
"Authorization": "Bearer " + key
|
|
81
|
+
},
|
|
82
|
+
method,
|
|
83
|
+
query,
|
|
84
|
+
url: SENDGRID_BASE + path
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
var sendgrid = {
|
|
88
|
+
// `headers` (optional) carries the List-Unsubscribe pair on lead-facing
|
|
89
|
+
// commercial sends; omitted, the request body is byte-identical to the
|
|
90
|
+
// pre-opt-out-floor shape so system mail is untouched.
|
|
91
|
+
send: async ({ apiKey, from, headers, html, request: request2, subject, text, to }) => {
|
|
92
|
+
var _a, _b;
|
|
93
|
+
try {
|
|
94
|
+
const sender = (from == null ? void 0 : from.email) ? from : {
|
|
95
|
+
email: process.env.SENDGRID_FROM_ADDRESS,
|
|
96
|
+
name: "Drawbridge"
|
|
97
|
+
};
|
|
98
|
+
await sendWithRetry(() => sendgridRequest({
|
|
99
|
+
apiKey,
|
|
100
|
+
...request2 && { request: request2 },
|
|
101
|
+
method: "POST",
|
|
102
|
+
path: "/v3/mail/send",
|
|
103
|
+
body: {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text/plain",
|
|
107
|
+
value: text
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
type: "text/html",
|
|
111
|
+
value: html
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
from: sender,
|
|
115
|
+
...headers && { headers },
|
|
116
|
+
personalizations: [
|
|
117
|
+
{
|
|
118
|
+
to: [
|
|
119
|
+
{ email: to }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
subject
|
|
124
|
+
}
|
|
125
|
+
}));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(error.message);
|
|
129
|
+
if ((_b = (_a = parsed == null ? void 0 : parsed.errors) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) error.message = parsed.errors[0].message;
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
export {
|
|
137
|
+
sendWithRetry,
|
|
138
|
+
sendgrid,
|
|
139
|
+
sendgridRequest
|
|
140
|
+
};
|