@drawbridge/drawbridge-utils 0.0.111 → 0.0.114

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.
@@ -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
+ };
@@ -0,0 +1,112 @@
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/twilio.js
20
+ var twilio_exports = {};
21
+ __export(twilio_exports, {
22
+ twilio: () => twilio
23
+ });
24
+ module.exports = __toCommonJS(twilio_exports);
25
+
26
+ // lib/http.js
27
+ var DEFAULT_TIMEOUT_MS = 15e3;
28
+ var request = async ({
29
+ body,
30
+ headers = {},
31
+ method = "GET",
32
+ query,
33
+ timeout = DEFAULT_TIMEOUT_MS,
34
+ type = "json",
35
+ url
36
+ }) => {
37
+ const fullUrl = new URL(url);
38
+ if (query) {
39
+ Object.entries(query).forEach(([k, v]) => fullUrl.searchParams.set(k, v));
40
+ }
41
+ ;
42
+ const isForm = type === "form";
43
+ const response = await fetch(fullUrl.toString(), {
44
+ method,
45
+ headers: {
46
+ "Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
47
+ ...headers
48
+ },
49
+ signal: AbortSignal.timeout(timeout),
50
+ ...body !== void 0 && {
51
+ body: isForm ? new URLSearchParams(body).toString() : JSON.stringify(body)
52
+ }
53
+ });
54
+ if (!response.ok) {
55
+ const text2 = await response.text().catch(() => "");
56
+ const error = new Error(text2 || response.statusText);
57
+ error.status = response.status;
58
+ throw error;
59
+ }
60
+ ;
61
+ const text = await response.text();
62
+ try {
63
+ return text ? JSON.parse(text) : null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ };
68
+
69
+ // lib/twilio.js
70
+ var twilio = {
71
+ sms: async ({
72
+ accountSid,
73
+ authToken,
74
+ from,
75
+ link,
76
+ message,
77
+ optOut = false,
78
+ prize,
79
+ // Injectable for the same reason sendgrid's is — see sendgrid.js.
80
+ request: request2 = request,
81
+ title,
82
+ to
83
+ }) => {
84
+ accountSid = accountSid || process.env.TWILIO_ACCOUNT_SID;
85
+ authToken = authToken || process.env.TWILIO_AUTH_TOKEN;
86
+ from = from || process.env.TWILIO_ACCOUNT_FROM;
87
+ const body = [
88
+ title,
89
+ message,
90
+ ...prize ? [prize.title + " (" + prize.value + ")"] : [],
91
+ ...link ? ["Verify here: " + link] : [],
92
+ ...optOut ? ["Reply STOP to opt out"] : []
93
+ ].filter(Boolean).join("\n");
94
+ return await request2({
95
+ method: "POST",
96
+ type: "form",
97
+ url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/Messages.json",
98
+ headers: {
99
+ "Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
100
+ },
101
+ body: {
102
+ Body: body,
103
+ From: from,
104
+ To: to
105
+ }
106
+ });
107
+ }
108
+ };
109
+ // Annotate the CommonJS export names for ESM import in node:
110
+ 0 && (module.exports = {
111
+ twilio
112
+ });
@@ -0,0 +1,64 @@
1
+ import { request } from './http.cjs';
2
+
3
+ // TWILIO TRANSPORT. Vendor HTTP and nothing else, for the same reason SendGrid
4
+ // sits beside it — see sendgrid.js.
5
+ //
6
+ // The message COMPOSITION is here too, deliberately: the newline-separated
7
+ // shape, brand-first ordering and the absence of "Your prize is" / "Click here
8
+ // to verify" phrasing are carrier-filter avoidance that got these messages
9
+ // flagged once. That is a property of sending SMS, not of any one caller, and
10
+ // splitting it from the transport is how one caller quietly reverts it.
11
+
12
+
13
+ const twilio = {
14
+
15
+ sms : async ({
16
+ accountSid,
17
+ authToken,
18
+ from,
19
+ link,
20
+ message,
21
+ optOut = false,
22
+ prize,
23
+ // Injectable for the same reason sendgrid's is — see sendgrid.js.
24
+ request: request$1 = request,
25
+ title,
26
+ to
27
+ }) => {
28
+
29
+ accountSid = accountSid || process.env.TWILIO_ACCOUNT_SID;
30
+ authToken = authToken || process.env.TWILIO_AUTH_TOKEN;
31
+ from = from || process.env.TWILIO_ACCOUNT_FROM;
32
+
33
+ // Newline-separated, brand/title first, and no "Your prize is" /
34
+ // "Click here to verify your prize" phrasing — textbook carrier-filter
35
+ // bait that got these messages flagged. The opt-out line is appended
36
+ // on lead-facing sends (carrier compliance); OTC/auth sends pass
37
+ // nothing new and keep their exact copy.
38
+ const body = [
39
+ title,
40
+ message,
41
+ ...( prize ? [ prize.title + ' (' + prize.value + ')' ] : [] ),
42
+ ...( link ? [ 'Verify here: ' + link ] : [] ),
43
+ ...( optOut ? [ 'Reply STOP to opt out' ] : [] )
44
+ ].filter( Boolean ).join( '\n' );
45
+
46
+ return await request$1({
47
+ method : 'POST',
48
+ type : 'form',
49
+ url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/Messages.json',
50
+ headers : {
51
+ 'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
52
+ },
53
+ body : {
54
+ Body : body,
55
+ From : from,
56
+ To : to
57
+ }
58
+ });
59
+
60
+ }
61
+
62
+ };
63
+
64
+ export { twilio };
@@ -0,0 +1,64 @@
1
+ import { request } from './http.js';
2
+
3
+ // TWILIO TRANSPORT. Vendor HTTP and nothing else, for the same reason SendGrid
4
+ // sits beside it — see sendgrid.js.
5
+ //
6
+ // The message COMPOSITION is here too, deliberately: the newline-separated
7
+ // shape, brand-first ordering and the absence of "Your prize is" / "Click here
8
+ // to verify" phrasing are carrier-filter avoidance that got these messages
9
+ // flagged once. That is a property of sending SMS, not of any one caller, and
10
+ // splitting it from the transport is how one caller quietly reverts it.
11
+
12
+
13
+ const twilio = {
14
+
15
+ sms : async ({
16
+ accountSid,
17
+ authToken,
18
+ from,
19
+ link,
20
+ message,
21
+ optOut = false,
22
+ prize,
23
+ // Injectable for the same reason sendgrid's is — see sendgrid.js.
24
+ request: request$1 = request,
25
+ title,
26
+ to
27
+ }) => {
28
+
29
+ accountSid = accountSid || process.env.TWILIO_ACCOUNT_SID;
30
+ authToken = authToken || process.env.TWILIO_AUTH_TOKEN;
31
+ from = from || process.env.TWILIO_ACCOUNT_FROM;
32
+
33
+ // Newline-separated, brand/title first, and no "Your prize is" /
34
+ // "Click here to verify your prize" phrasing — textbook carrier-filter
35
+ // bait that got these messages flagged. The opt-out line is appended
36
+ // on lead-facing sends (carrier compliance); OTC/auth sends pass
37
+ // nothing new and keep their exact copy.
38
+ const body = [
39
+ title,
40
+ message,
41
+ ...( prize ? [ prize.title + ' (' + prize.value + ')' ] : [] ),
42
+ ...( link ? [ 'Verify here: ' + link ] : [] ),
43
+ ...( optOut ? [ 'Reply STOP to opt out' ] : [] )
44
+ ].filter( Boolean ).join( '\n' );
45
+
46
+ return await request$1({
47
+ method : 'POST',
48
+ type : 'form',
49
+ url : 'https://api.twilio.com/2010-04-01/Accounts/' + accountSid + '/Messages.json',
50
+ headers : {
51
+ 'Authorization' : 'Basic ' + Buffer.from( accountSid + ':' + authToken ).toString( 'base64' )
52
+ },
53
+ body : {
54
+ Body : body,
55
+ From : from,
56
+ To : to
57
+ }
58
+ });
59
+
60
+ }
61
+
62
+ };
63
+
64
+ export { twilio };
package/dist/twilio.js ADDED
@@ -0,0 +1,86 @@
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/twilio.js
45
+ var twilio = {
46
+ sms: async ({
47
+ accountSid,
48
+ authToken,
49
+ from,
50
+ link,
51
+ message,
52
+ optOut = false,
53
+ prize,
54
+ // Injectable for the same reason sendgrid's is — see sendgrid.js.
55
+ request: request2 = request,
56
+ title,
57
+ to
58
+ }) => {
59
+ accountSid = accountSid || process.env.TWILIO_ACCOUNT_SID;
60
+ authToken = authToken || process.env.TWILIO_AUTH_TOKEN;
61
+ from = from || process.env.TWILIO_ACCOUNT_FROM;
62
+ const body = [
63
+ title,
64
+ message,
65
+ ...prize ? [prize.title + " (" + prize.value + ")"] : [],
66
+ ...link ? ["Verify here: " + link] : [],
67
+ ...optOut ? ["Reply STOP to opt out"] : []
68
+ ].filter(Boolean).join("\n");
69
+ return await request2({
70
+ method: "POST",
71
+ type: "form",
72
+ url: "https://api.twilio.com/2010-04-01/Accounts/" + accountSid + "/Messages.json",
73
+ headers: {
74
+ "Authorization": "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
75
+ },
76
+ body: {
77
+ Body: body,
78
+ From: from,
79
+ To: to
80
+ }
81
+ });
82
+ }
83
+ };
84
+ export {
85
+ twilio
86
+ };
package/package.json CHANGED
@@ -133,6 +133,16 @@
133
133
  "import": "./dist/fetch.js",
134
134
  "require": "./dist/fetch.cjs"
135
135
  },
136
+ "./sendgrid": {
137
+ "types": "./dist/sendgrid.d.ts",
138
+ "import": "./dist/sendgrid.js",
139
+ "require": "./dist/sendgrid.cjs"
140
+ },
141
+ "./twilio": {
142
+ "types": "./dist/twilio.d.ts",
143
+ "import": "./dist/twilio.js",
144
+ "require": "./dist/twilio.cjs"
145
+ },
136
146
  "./http": {
137
147
  "types": "./dist/http.d.ts",
138
148
  "import": "./dist/http.js",
@@ -200,5 +210,5 @@
200
210
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
201
211
  },
202
212
  "types": "dist/index.d.ts",
203
- "version": "0.0.111"
213
+ "version": "0.0.114"
204
214
  }