@drawbridge/drawbridge-utils 0.0.156 → 0.0.158
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/axios.cjs +5 -0
- package/dist/axios.d.cts +14 -0
- package/dist/axios.d.ts +14 -0
- package/dist/axios.js +5 -0
- package/dist/connections/index.cjs +263 -39
- package/dist/connections/index.d.cts +466 -82
- package/dist/connections/index.d.ts +466 -82
- package/dist/connections/index.js +262 -38
- package/dist/providers.cjs +263 -39
- package/dist/providers.js +262 -38
- package/dist/safe-http.cjs +9 -2
- package/dist/safe-http.d.cts +9 -2
- package/dist/safe-http.d.ts +9 -2
- package/dist/safe-http.js +9 -2
- package/dist/sendgrid.cjs +18 -1
- package/dist/sendgrid.d.cts +24 -1
- package/dist/sendgrid.d.ts +24 -1
- package/dist/sendgrid.js +18 -1
- package/package.json +1 -1
package/dist/axios.cjs
CHANGED
|
@@ -58,9 +58,14 @@ var isBlockedIPv4 = (ip) => {
|
|
|
58
58
|
if (a >= 224) return true;
|
|
59
59
|
return false;
|
|
60
60
|
};
|
|
61
|
+
var transition = new import_net.default.BlockList();
|
|
62
|
+
transition.addSubnet("64:ff9b::", 96, "ipv6");
|
|
63
|
+
transition.addSubnet("2002::", 16, "ipv6");
|
|
64
|
+
transition.addSubnet("2001::", 32, "ipv6");
|
|
61
65
|
var isBlockedIPv6 = (ip) => {
|
|
62
66
|
const lower = ip.toLowerCase();
|
|
63
67
|
if (lower === "::1" || lower === "::") return true;
|
|
68
|
+
if (transition.check(ip, "ipv6")) return true;
|
|
64
69
|
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
65
70
|
if (/^fe[89ab]/.test(lower)) return true;
|
|
66
71
|
if (lower.startsWith("ff")) return true;
|
package/dist/axios.d.cts
CHANGED
|
@@ -34,12 +34,26 @@ const isBlockedIPv4 = ( ip ) => {
|
|
|
34
34
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// The IPv6 transition mechanisms each carry a v4 address inside the v6 one and
|
|
38
|
+
// a gateway unwraps it, so 10.0.0.1 can arrive dressed as 64:ff9b::a00:1 and
|
|
39
|
+
// pass a check that only knows the plain ranges. No merchant endpoint lives on
|
|
40
|
+
// one — the public internet runs native v6 or v4 — so the three prefixes are
|
|
41
|
+
// refused whole rather than unpacked. BlockList understands every textual form,
|
|
42
|
+
// which the prefix comparisons below do not.
|
|
43
|
+
const transition = new net.BlockList();
|
|
44
|
+
|
|
45
|
+
transition.addSubnet( '64:ff9b::', 96, 'ipv6' );
|
|
46
|
+
transition.addSubnet( '2002::', 16, 'ipv6' );
|
|
47
|
+
transition.addSubnet( '2001::', 32, 'ipv6' );
|
|
48
|
+
|
|
37
49
|
const isBlockedIPv6 = ( ip ) => {
|
|
38
50
|
|
|
39
51
|
const lower = ip.toLowerCase();
|
|
40
52
|
|
|
41
53
|
if( lower === '::1' || lower === '::' ) return true;
|
|
42
54
|
|
|
55
|
+
if( transition.check( ip, 'ipv6' ) ) return true;
|
|
56
|
+
|
|
43
57
|
// fc00::/7 — unique local addresses
|
|
44
58
|
if( lower.startsWith( 'fc' ) || lower.startsWith( 'fd' ) ) return true;
|
|
45
59
|
|
package/dist/axios.d.ts
CHANGED
|
@@ -34,12 +34,26 @@ const isBlockedIPv4 = ( ip ) => {
|
|
|
34
34
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
+
// The IPv6 transition mechanisms each carry a v4 address inside the v6 one and
|
|
38
|
+
// a gateway unwraps it, so 10.0.0.1 can arrive dressed as 64:ff9b::a00:1 and
|
|
39
|
+
// pass a check that only knows the plain ranges. No merchant endpoint lives on
|
|
40
|
+
// one — the public internet runs native v6 or v4 — so the three prefixes are
|
|
41
|
+
// refused whole rather than unpacked. BlockList understands every textual form,
|
|
42
|
+
// which the prefix comparisons below do not.
|
|
43
|
+
const transition = new net.BlockList();
|
|
44
|
+
|
|
45
|
+
transition.addSubnet( '64:ff9b::', 96, 'ipv6' );
|
|
46
|
+
transition.addSubnet( '2002::', 16, 'ipv6' );
|
|
47
|
+
transition.addSubnet( '2001::', 32, 'ipv6' );
|
|
48
|
+
|
|
37
49
|
const isBlockedIPv6 = ( ip ) => {
|
|
38
50
|
|
|
39
51
|
const lower = ip.toLowerCase();
|
|
40
52
|
|
|
41
53
|
if( lower === '::1' || lower === '::' ) return true;
|
|
42
54
|
|
|
55
|
+
if( transition.check( ip, 'ipv6' ) ) return true;
|
|
56
|
+
|
|
43
57
|
// fc00::/7 — unique local addresses
|
|
44
58
|
if( lower.startsWith( 'fc' ) || lower.startsWith( 'fd' ) ) return true;
|
|
45
59
|
|
package/dist/axios.js
CHANGED
|
@@ -23,9 +23,14 @@ var isBlockedIPv4 = (ip) => {
|
|
|
23
23
|
if (a >= 224) return true;
|
|
24
24
|
return false;
|
|
25
25
|
};
|
|
26
|
+
var transition = new net.BlockList();
|
|
27
|
+
transition.addSubnet("64:ff9b::", 96, "ipv6");
|
|
28
|
+
transition.addSubnet("2002::", 16, "ipv6");
|
|
29
|
+
transition.addSubnet("2001::", 32, "ipv6");
|
|
26
30
|
var isBlockedIPv6 = (ip) => {
|
|
27
31
|
const lower = ip.toLowerCase();
|
|
28
32
|
if (lower === "::1" || lower === "::") return true;
|
|
33
|
+
if (transition.check(ip, "ipv6")) return true;
|
|
29
34
|
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
30
35
|
if (/^fe[89ab]/.test(lower)) return true;
|
|
31
36
|
if (lower.startsWith("ff")) return true;
|
|
@@ -931,7 +931,53 @@ var attentive_default2 = {
|
|
|
931
931
|
};
|
|
932
932
|
|
|
933
933
|
// lib/connections/providers/drawbridge.js
|
|
934
|
+
var import_node_crypto3 = require("crypto");
|
|
935
|
+
|
|
936
|
+
// lib/connections/inbound.js
|
|
934
937
|
var import_node_crypto2 = require("crypto");
|
|
938
|
+
var verifySignature = ({ body, descriptor, headers, secret }) => {
|
|
939
|
+
if (!secret) {
|
|
940
|
+
throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
|
|
941
|
+
}
|
|
942
|
+
const provided = headers[descriptor.headers.signature];
|
|
943
|
+
if (!provided) {
|
|
944
|
+
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
945
|
+
}
|
|
946
|
+
const digest = (0, import_node_crypto2.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
|
|
947
|
+
const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
|
|
948
|
+
const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
|
|
949
|
+
if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto2.timingSafeEqual)(digestBuffer, providedBuffer)) {
|
|
950
|
+
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
951
|
+
}
|
|
952
|
+
return JSON.parse(body.toString());
|
|
953
|
+
};
|
|
954
|
+
var REPLAY_TOLERANCE_SECONDS = 5 * 60;
|
|
955
|
+
var asPem = (key) => [
|
|
956
|
+
"-----BEGIN PUBLIC KEY-----",
|
|
957
|
+
...key.match(/.{1,64}/g) || [],
|
|
958
|
+
"-----END PUBLIC KEY-----"
|
|
959
|
+
].join("\n");
|
|
960
|
+
var verifyEcdsa = ({ body, descriptor, headers, secret }) => {
|
|
961
|
+
if (!secret) {
|
|
962
|
+
throw Object.assign(new Error("Missing webhook key: " + descriptor.signature.secret), { status: 500 });
|
|
963
|
+
}
|
|
964
|
+
const provided = headers[descriptor.headers.signature];
|
|
965
|
+
const timestamp = headers[descriptor.headers.timestamp];
|
|
966
|
+
if (!provided || !timestamp) {
|
|
967
|
+
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
968
|
+
}
|
|
969
|
+
const age = Math.abs(Math.floor(Date.now() / 1e3) - Number(timestamp));
|
|
970
|
+
if (!(age <= REPLAY_TOLERANCE_SECONDS)) {
|
|
971
|
+
throw Object.assign(new Error("Stale webhook signature"), { status: 401 });
|
|
972
|
+
}
|
|
973
|
+
const payload = Buffer.concat([Buffer.from(String(timestamp), "utf8"), body]);
|
|
974
|
+
const verified = (0, import_node_crypto2.createVerify)("sha256").update(payload).verify(asPem(secret), Buffer.from(provided, "base64"));
|
|
975
|
+
if (!verified) {
|
|
976
|
+
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
977
|
+
}
|
|
978
|
+
return JSON.parse(body.toString());
|
|
979
|
+
};
|
|
980
|
+
var readEventHeader = ({ descriptor, headers }) => headers[descriptor.headers.event];
|
|
935
981
|
|
|
936
982
|
// lib/http.js
|
|
937
983
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
@@ -1822,6 +1868,144 @@ var MINIMUM_ACTION_CENTS = Math.round(
|
|
|
1822
1868
|
// lib/connections/providers/drawbridge.js
|
|
1823
1869
|
var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
|
|
1824
1870
|
var START_KEYWORDS = ["START", "UNSTOP", "YES"];
|
|
1871
|
+
var sendgridInbound = {
|
|
1872
|
+
headers: {
|
|
1873
|
+
signature: "x-twilio-email-event-webhook-signature",
|
|
1874
|
+
timestamp: "x-twilio-email-event-webhook-timestamp"
|
|
1875
|
+
},
|
|
1876
|
+
signature: {
|
|
1877
|
+
secret: "SENDGRID_EVENT_WEBHOOK_KEY"
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
var REFUSALS = /* @__PURE__ */ new Set(["blocked", "bounce", "deferred", "dropped", "spamreport"]);
|
|
1881
|
+
var DELIVERY = {
|
|
1882
|
+
queued: 10,
|
|
1883
|
+
sent: 20,
|
|
1884
|
+
deferred: 30,
|
|
1885
|
+
blocked: 40,
|
|
1886
|
+
delivered: 50,
|
|
1887
|
+
bounced: 60,
|
|
1888
|
+
complained: 70
|
|
1889
|
+
};
|
|
1890
|
+
var fromSendgrid = (event) => {
|
|
1891
|
+
if ((event == null ? void 0 : event.event) === "bounce") return event.type === "blocked" ? "blocked" : "bounced";
|
|
1892
|
+
return {
|
|
1893
|
+
blocked: "blocked",
|
|
1894
|
+
deferred: "deferred",
|
|
1895
|
+
delivered: "delivered",
|
|
1896
|
+
dropped: "bounced",
|
|
1897
|
+
processed: "queued",
|
|
1898
|
+
spamreport: "complained"
|
|
1899
|
+
}[event == null ? void 0 : event.event] || null;
|
|
1900
|
+
};
|
|
1901
|
+
var fromTwilio = (status) => ({
|
|
1902
|
+
delivered: "delivered",
|
|
1903
|
+
failed: "bounced",
|
|
1904
|
+
queued: "queued",
|
|
1905
|
+
sending: "sent",
|
|
1906
|
+
sent: "sent",
|
|
1907
|
+
undelivered: "bounced"
|
|
1908
|
+
})[String(status || "").toLowerCase()] || null;
|
|
1909
|
+
var deliveryWrite = ({ at, code: code2, notification, permanent, provider, reason, status }) => {
|
|
1910
|
+
const rank = DELIVERY[status];
|
|
1911
|
+
if (!notification || !rank) return null;
|
|
1912
|
+
return {
|
|
1913
|
+
collection: "notification",
|
|
1914
|
+
data: {
|
|
1915
|
+
$set: {
|
|
1916
|
+
"meta.delivery": {
|
|
1917
|
+
at: at || /* @__PURE__ */ new Date(),
|
|
1918
|
+
...code2 !== void 0 && code2 !== null && { code: String(code2) },
|
|
1919
|
+
...permanent !== void 0 && { permanent },
|
|
1920
|
+
provider,
|
|
1921
|
+
rank,
|
|
1922
|
+
...reason && { reason: String(reason) },
|
|
1923
|
+
status
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
},
|
|
1927
|
+
operation: "update",
|
|
1928
|
+
query: {
|
|
1929
|
+
id: notification,
|
|
1930
|
+
// NEVER an upsert. A callback naming a notification we do not have is a
|
|
1931
|
+
// vendor talking about someone else's message, not a document to create.
|
|
1932
|
+
$or: [
|
|
1933
|
+
{ "meta.delivery.rank": { $exists: false } },
|
|
1934
|
+
{ "meta.delivery.rank": { $lt: rank } }
|
|
1935
|
+
]
|
|
1936
|
+
}
|
|
1937
|
+
};
|
|
1938
|
+
};
|
|
1939
|
+
var summariseEvents = (events) => {
|
|
1940
|
+
const list = Array.isArray(events) ? events : [];
|
|
1941
|
+
if (!list.length) return { message: "SendGrid delivered an empty event batch.", skipped: true };
|
|
1942
|
+
const counts = {};
|
|
1943
|
+
for (const { event } of list) {
|
|
1944
|
+
const name = event || "unknown";
|
|
1945
|
+
counts[name] = (counts[name] || 0) + 1;
|
|
1946
|
+
}
|
|
1947
|
+
const tally = Object.entries(counts).map(([name, count]) => count + " " + name).join(", ");
|
|
1948
|
+
const refused = list.filter(({ event }) => REFUSALS.has(event)).slice(0, 5).map(({ email, reason, response, status }) => email + " \u2014 " + (reason || response || status || "no reason given"));
|
|
1949
|
+
const writes = list.map((event) => {
|
|
1950
|
+
var _a;
|
|
1951
|
+
return deliveryWrite({
|
|
1952
|
+
at: event.timestamp ? new Date(event.timestamp * 1e3) : void 0,
|
|
1953
|
+
code: event.status,
|
|
1954
|
+
// Read BOTH shapes. SendGrid echoes custom_args as siblings of `email`
|
|
1955
|
+
// and `event`, which is why it warns about colliding with reserved
|
|
1956
|
+
// names — but this is the one thing in the design that cannot be proved
|
|
1957
|
+
// until a real event arrives, and reading the nested form too costs a
|
|
1958
|
+
// token and removes the only silent failure left here.
|
|
1959
|
+
notification: event.notification || ((_a = event.custom_args) == null ? void 0 : _a.notification),
|
|
1960
|
+
// Only a bounce carries the distinction, and only a real one is
|
|
1961
|
+
// permanent — `blocked` is this attempt refused, not this address dead.
|
|
1962
|
+
...event.event === "bounce" && { permanent: event.type !== "blocked" },
|
|
1963
|
+
provider: { slug: "sendgrid", id: event.sg_message_id || null },
|
|
1964
|
+
reason: event.reason || event.response,
|
|
1965
|
+
status: fromSendgrid(event)
|
|
1966
|
+
});
|
|
1967
|
+
}).filter(Boolean);
|
|
1968
|
+
return {
|
|
1969
|
+
message: [list.length + " SendGrid event(s): " + tally, ...refused].join(" | "),
|
|
1970
|
+
...writes.length ? { writes } : { skipped: true }
|
|
1971
|
+
};
|
|
1972
|
+
};
|
|
1973
|
+
var statusCallback = (data2) => {
|
|
1974
|
+
const status = fromTwilio((data2 == null ? void 0 : data2.MessageStatus) || (data2 == null ? void 0 : data2.SmsStatus));
|
|
1975
|
+
if (!status) return { message: "Twilio sent a status this does not map: " + ((data2 == null ? void 0 : data2.MessageStatus) || "none"), skipped: true };
|
|
1976
|
+
const rank = DELIVERY[status];
|
|
1977
|
+
const write = {
|
|
1978
|
+
collection: "notification",
|
|
1979
|
+
data: {
|
|
1980
|
+
$set: {
|
|
1981
|
+
"meta.delivery": {
|
|
1982
|
+
at: /* @__PURE__ */ new Date(),
|
|
1983
|
+
...(data2 == null ? void 0 : data2.ErrorCode) && { code: String(data2.ErrorCode) },
|
|
1984
|
+
// `failed` never left Twilio and `undelivered` was refused by the
|
|
1985
|
+
// carrier. Both are dead ends for this handset, so neither is
|
|
1986
|
+
// reported as a temporary condition.
|
|
1987
|
+
...status === "bounced" && { permanent: true },
|
|
1988
|
+
provider: { slug: "twilio", id: (data2 == null ? void 0 : data2.MessageSid) || null },
|
|
1989
|
+
rank,
|
|
1990
|
+
...(data2 == null ? void 0 : data2.ErrorMessage) && { reason: String(data2.ErrorMessage) },
|
|
1991
|
+
status
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
},
|
|
1995
|
+
operation: "update",
|
|
1996
|
+
query: {
|
|
1997
|
+
"meta.delivery.provider.id": data2 == null ? void 0 : data2.MessageSid,
|
|
1998
|
+
$or: [
|
|
1999
|
+
{ "meta.delivery.rank": { $exists: false } },
|
|
2000
|
+
{ "meta.delivery.rank": { $lt: rank } }
|
|
2001
|
+
]
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
return {
|
|
2005
|
+
message: "Twilio " + status + " for " + ((data2 == null ? void 0 : data2.MessageSid) || "an unnamed message") + ((data2 == null ? void 0 : data2.ErrorCode) ? " (" + data2.ErrorCode + ")" : ""),
|
|
2006
|
+
...(data2 == null ? void 0 : data2.MessageSid) ? { writes: [write] } : { skipped: true }
|
|
2007
|
+
};
|
|
2008
|
+
};
|
|
1825
2009
|
var interpolate = (template, data2) => {
|
|
1826
2010
|
if (!template) return template;
|
|
1827
2011
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (data2 == null ? void 0 : data2[key]) != null ? String(data2[key]) : "{{" + key + "}}");
|
|
@@ -2065,9 +2249,16 @@ var drawbridge_default2 = {
|
|
|
2065
2249
|
// other vendor, and the bespoke webhooks route + the handler that lived in
|
|
2066
2250
|
// sync's buffer table are gone.
|
|
2067
2251
|
inbound: {
|
|
2068
|
-
//
|
|
2069
|
-
//
|
|
2070
|
-
|
|
2252
|
+
// THE REGISTERED URL IS THE TOPIC. Neither vendor sends a topic header,
|
|
2253
|
+
// so the channel it arrived on names the event — `sms` is one inbound
|
|
2254
|
+
// message, `email` is a batch of SendGrid delivery events.
|
|
2255
|
+
//
|
|
2256
|
+
// Defaulted because the drain calls `event()` with nothing when it only
|
|
2257
|
+
// wants the SMS name.
|
|
2258
|
+
event: ({ channel } = {}) => ({
|
|
2259
|
+
email: "email.events",
|
|
2260
|
+
"sms-status": "sms.status"
|
|
2261
|
+
})[channel] || "message.inbound",
|
|
2071
2262
|
// STOP AND START, AS DESCRIBED WRITES. A withdrawn number is withdrawn
|
|
2072
2263
|
// for everyone — organization : null is the platform floor canSend()
|
|
2073
2264
|
// reads on every send path. $setOnInsert + upsert so webhook replays
|
|
@@ -2075,6 +2266,8 @@ var drawbridge_default2 = {
|
|
|
2075
2266
|
// organization } index instead of erroring.
|
|
2076
2267
|
process: ({ context }) => {
|
|
2077
2268
|
var _a, _b;
|
|
2269
|
+
if ((context == null ? void 0 : context.topic) === "email.events") return summariseEvents(context == null ? void 0 : context.data);
|
|
2270
|
+
if ((context == null ? void 0 : context.topic) === "sms.status") return statusCallback(context == null ? void 0 : context.data);
|
|
2078
2271
|
const keyword = String(((_a = context == null ? void 0 : context.data) == null ? void 0 : _a.Body) || "").trim().toUpperCase();
|
|
2079
2272
|
const address = toE164((_b = context == null ? void 0 : context.data) == null ? void 0 : _b.From);
|
|
2080
2273
|
if (!address) return { message: "Inbound SMS carried no usable sender number.", skipped: true };
|
|
@@ -2120,9 +2313,14 @@ var drawbridge_default2 = {
|
|
|
2120
2313
|
}
|
|
2121
2314
|
return { message: "A real reply, not a keyword \u2014 nothing to record.", skipped: true };
|
|
2122
2315
|
},
|
|
2123
|
-
|
|
2316
|
+
// A SendGrid batch carries many sg_message_ids and no single identity, so
|
|
2317
|
+
// it names no provider id. That means no dedupe key for a redelivery —
|
|
2318
|
+
// acceptable because SendGrid only retries a non-2xx, and the cost of a
|
|
2319
|
+
// duplicate here is a second buffer row rather than a repeated side
|
|
2320
|
+
// effect. Revisit if this ever writes suppressions.
|
|
2321
|
+
receive: ({ channel, payload }) => ({
|
|
2124
2322
|
data: payload,
|
|
2125
|
-
provider: { id: (payload == null ? void 0 : payload.MessageSid) || null }
|
|
2323
|
+
provider: { id: channel === "email" ? null : (payload == null ? void 0 : payload.MessageSid) || null }
|
|
2126
2324
|
}),
|
|
2127
2325
|
// TWILIO'S SCHEME, from docs.twilio.com/usage/security: take the full
|
|
2128
2326
|
// registered url, sort the POST parameters alphabetically (Unix-style,
|
|
@@ -2131,13 +2329,14 @@ var drawbridge_default2 = {
|
|
|
2131
2329
|
// X-Twilio-Signature. It signs the URL rather than the raw body, which
|
|
2132
2330
|
// is exactly why the shared body-HMAC verifier cannot cover it and this
|
|
2133
2331
|
// hook exists.
|
|
2134
|
-
verify: ({ body, headers, secret, url }) => {
|
|
2332
|
+
verify: ({ body, channel, headers, secret, url }) => {
|
|
2333
|
+
if (channel === "email") return verifyEcdsa({ body, descriptor: sendgridInbound, headers, secret });
|
|
2135
2334
|
if (!secret) throw Object.assign(new Error("Missing webhook secret: TWILIO_AUTH_TOKEN"), { status: 500 });
|
|
2136
2335
|
const params = new URLSearchParams(String(body || ""));
|
|
2137
2336
|
const signed = url + [...params.keys()].sort().map((key) => key + params.get(key)).join("");
|
|
2138
|
-
const expected = (0,
|
|
2337
|
+
const expected = (0, import_node_crypto3.createHmac)("sha1", secret).update(signed).digest("base64");
|
|
2139
2338
|
const provided = String((headers == null ? void 0 : headers["x-twilio-signature"]) || "");
|
|
2140
|
-
const matches = expected.length === provided.length && (0,
|
|
2339
|
+
const matches = expected.length === provided.length && (0, import_node_crypto3.timingSafeEqual)(Buffer.from(expected), Buffer.from(provided));
|
|
2141
2340
|
if (!matches) throw Object.assign(new Error("Invalid Twilio signature"), { status: 401 });
|
|
2142
2341
|
return Object.fromEntries(params);
|
|
2143
2342
|
}
|
|
@@ -2321,15 +2520,30 @@ var drawbridge_default2 = {
|
|
|
2321
2520
|
},
|
|
2322
2521
|
icon: drawbridge_default,
|
|
2323
2522
|
// WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
|
|
2324
|
-
// verifies
|
|
2325
|
-
//
|
|
2523
|
+
// verifies each channel. The route resolves the NAME to the stored value and
|
|
2524
|
+
// hands it to verify; this manifest never reads a store.
|
|
2525
|
+
//
|
|
2526
|
+
// `signature.channels` because this is the one manifest that receives from two
|
|
2527
|
+
// different vendors — Twilio on `sms`, SendGrid on `email` — which need
|
|
2528
|
+
// different credentials and share no scheme. A manifest whose channels all
|
|
2529
|
+
// verify the same way keeps declaring `signature` flat, and the route falls
|
|
2530
|
+
// back to it; Shopify's two channels do exactly that.
|
|
2326
2531
|
inbound: {
|
|
2327
2532
|
headers: {
|
|
2328
2533
|
id: "i-twilio-idempotency-token",
|
|
2329
2534
|
signature: "x-twilio-signature"
|
|
2330
2535
|
},
|
|
2331
2536
|
signature: {
|
|
2332
|
-
|
|
2537
|
+
channels: {
|
|
2538
|
+
email: sendgridInbound.signature,
|
|
2539
|
+
sms: { secret: "TWILIO_AUTH_TOKEN" },
|
|
2540
|
+
// Outbound delivery receipts, which Twilio signs exactly as it signs
|
|
2541
|
+
// an inbound message — same scheme, same credential, different url.
|
|
2542
|
+
// Separate channel because the payload is a MessageStatus rather than
|
|
2543
|
+
// a message, and conflating them would put a STOP keyword check on a
|
|
2544
|
+
// delivery receipt.
|
|
2545
|
+
"sms-status": { secret: "TWILIO_AUTH_TOKEN" }
|
|
2546
|
+
}
|
|
2333
2547
|
}
|
|
2334
2548
|
},
|
|
2335
2549
|
// PRIVATE: never in the catalog, always available to the builder.
|
|
@@ -2361,6 +2575,13 @@ var drawbridge_default2 = {
|
|
|
2361
2575
|
// Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
|
|
2362
2576
|
// either. Unset, it degrades to the account sender rather than
|
|
2363
2577
|
// refusing to start.
|
|
2578
|
+
// NOT redacted, because it is a PUBLIC key. Marking it secret would
|
|
2579
|
+
// be theatre, and it would stop an operator reading back the value
|
|
2580
|
+
// they need to compare against the SendGrid console.
|
|
2581
|
+
//
|
|
2582
|
+
// Optional: absent, the event webhook answers 500 on verify rather
|
|
2583
|
+
// than accepting unverified deliveries, and nothing else notices.
|
|
2584
|
+
{ input: "text", key: "eventKey", credential: "SENDGRID_EVENT_WEBHOOK_KEY", label: "Event webhook key", message: "SendGrid, Settings, Mail Settings, Event Webhook \u2014 the verification key shown once signature verification is enabled. Public, not a secret." },
|
|
2364
2585
|
{ input: "email", key: "leadSender", credential: "SENDGRID_SEND_FROM_ADDRESS", label: "Lead sender", message: "The default for lead-facing mail when a merchant has not verified their own domain." }
|
|
2365
2586
|
],
|
|
2366
2587
|
icon: sendgrid_default,
|
|
@@ -3107,7 +3328,7 @@ var klaviyo_default2 = {
|
|
|
3107
3328
|
};
|
|
3108
3329
|
|
|
3109
3330
|
// lib/connections/providers/mailchimp.js
|
|
3110
|
-
var
|
|
3331
|
+
var import_node_crypto4 = require("crypto");
|
|
3111
3332
|
|
|
3112
3333
|
// lib/connections/icons/mailchimp.js
|
|
3113
3334
|
var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -3139,7 +3360,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
3139
3360
|
}
|
|
3140
3361
|
return response.status === 204 ? null : response.json();
|
|
3141
3362
|
};
|
|
3142
|
-
var subscriberHash = (email) => (0,
|
|
3363
|
+
var subscriberHash = (email) => (0, import_node_crypto4.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
3143
3364
|
var mailchimp_default2 = {
|
|
3144
3365
|
// OAUTH 2, authorization code. Every url below is quoted from
|
|
3145
3366
|
// mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
|
|
@@ -3475,26 +3696,6 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
3475
3696
|
<path d="M300.325 382.771L368 365.96C368 365.96 338.904 169.185 338.689 167.892C338.473 166.599 337.396 165.737 336.318 165.737C335.24 165.737 316.274 165.306 316.274 165.306C316.274 165.306 304.636 154.099 300.325 149.788V382.771Z" fill="white"/>
|
|
3476
3697
|
</svg>`;
|
|
3477
3698
|
|
|
3478
|
-
// lib/connections/inbound.js
|
|
3479
|
-
var import_node_crypto4 = require("crypto");
|
|
3480
|
-
var verifySignature = ({ body, descriptor, headers, secret }) => {
|
|
3481
|
-
if (!secret) {
|
|
3482
|
-
throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
|
|
3483
|
-
}
|
|
3484
|
-
const provided = headers[descriptor.headers.signature];
|
|
3485
|
-
if (!provided) {
|
|
3486
|
-
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
3487
|
-
}
|
|
3488
|
-
const digest = (0, import_node_crypto4.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
|
|
3489
|
-
const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
|
|
3490
|
-
const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
|
|
3491
|
-
if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto4.timingSafeEqual)(digestBuffer, providedBuffer)) {
|
|
3492
|
-
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
3493
|
-
}
|
|
3494
|
-
return JSON.parse(body.toString());
|
|
3495
|
-
};
|
|
3496
|
-
var readEventHeader = ({ descriptor, headers }) => headers[descriptor.headers.event];
|
|
3497
|
-
|
|
3498
3699
|
// lib/email.js
|
|
3499
3700
|
var GMAIL_DOMAINS = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com"]);
|
|
3500
3701
|
var toCanonicalEmail = (value) => {
|
|
@@ -4867,9 +5068,14 @@ var isBlockedIPv4 = (ip) => {
|
|
|
4867
5068
|
if (a >= 224) return true;
|
|
4868
5069
|
return false;
|
|
4869
5070
|
};
|
|
5071
|
+
var transition = new import_net.default.BlockList();
|
|
5072
|
+
transition.addSubnet("64:ff9b::", 96, "ipv6");
|
|
5073
|
+
transition.addSubnet("2002::", 16, "ipv6");
|
|
5074
|
+
transition.addSubnet("2001::", 32, "ipv6");
|
|
4870
5075
|
var isBlockedIPv6 = (ip) => {
|
|
4871
5076
|
const lower = ip.toLowerCase();
|
|
4872
5077
|
if (lower === "::1" || lower === "::") return true;
|
|
5078
|
+
if (transition.check(ip, "ipv6")) return true;
|
|
4873
5079
|
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
4874
5080
|
if (/^fe[89ab]/.test(lower)) return true;
|
|
4875
5081
|
if (lower.startsWith("ff")) return true;
|
|
@@ -4928,12 +5134,13 @@ var pinnedAgent = async (url) => {
|
|
|
4928
5134
|
var safeRequest = async ({
|
|
4929
5135
|
body,
|
|
4930
5136
|
headers = {},
|
|
5137
|
+
maxContentLength,
|
|
4931
5138
|
method = "GET",
|
|
4932
5139
|
query,
|
|
4933
5140
|
timeout = DEFAULT_TIMEOUT_MS2,
|
|
4934
5141
|
type = "json",
|
|
4935
5142
|
url
|
|
4936
|
-
}) => {
|
|
5143
|
+
}, { transport = axios } = {}) => {
|
|
4937
5144
|
const full = new URL(url);
|
|
4938
5145
|
if (query) {
|
|
4939
5146
|
Object.entries(query).forEach(([key, value]) => full.searchParams.set(key, value));
|
|
@@ -4941,7 +5148,7 @@ var safeRequest = async ({
|
|
|
4941
5148
|
const { protocol, agent } = await pinnedAgent(full.toString());
|
|
4942
5149
|
const isForm = type === "form";
|
|
4943
5150
|
try {
|
|
4944
|
-
const response = await
|
|
5151
|
+
const response = await transport({
|
|
4945
5152
|
method,
|
|
4946
5153
|
url: full.toString(),
|
|
4947
5154
|
headers: {
|
|
@@ -4951,6 +5158,7 @@ var safeRequest = async ({
|
|
|
4951
5158
|
...body !== void 0 && {
|
|
4952
5159
|
data: isForm ? new URLSearchParams(body).toString() : body
|
|
4953
5160
|
},
|
|
5161
|
+
...maxContentLength !== void 0 && { maxContentLength },
|
|
4954
5162
|
timeout,
|
|
4955
5163
|
maxRedirects: 0,
|
|
4956
5164
|
httpAgent: protocol === "http:" ? agent : void 0,
|
|
@@ -4973,6 +5181,19 @@ var safeRequest = async ({
|
|
|
4973
5181
|
};
|
|
4974
5182
|
|
|
4975
5183
|
// lib/connections/providers/webhook.js
|
|
5184
|
+
var RESPONSE_LIMIT = 64 * 1024;
|
|
5185
|
+
var signature = ({ body, settings }) => {
|
|
5186
|
+
var _a;
|
|
5187
|
+
const timestamp = Math.floor(Date.now() / 1e3);
|
|
5188
|
+
const payload = timestamp + "." + JSON.stringify(body);
|
|
5189
|
+
const previous = ((_a = settings.previous) == null ? void 0 : _a.secret) && new Date(settings.previous.until) > /* @__PURE__ */ new Date() ? [settings.previous.secret] : [];
|
|
5190
|
+
return [
|
|
5191
|
+
"t=" + timestamp,
|
|
5192
|
+
...[settings.secret, ...previous].map(
|
|
5193
|
+
(secret) => "v1=" + import_node_crypto6.default.createHmac("sha256", secret).update(payload).digest("hex")
|
|
5194
|
+
)
|
|
5195
|
+
].join(",");
|
|
5196
|
+
};
|
|
4976
5197
|
var webhook_default = {
|
|
4977
5198
|
// Connecting GENERATES the secret rather than storing one the merchant typed,
|
|
4978
5199
|
// so the buttons say what actually happens.
|
|
@@ -5003,7 +5224,9 @@ var webhook_default = {
|
|
|
5003
5224
|
"Press Connect. Drawbridge generates a signing secret and shows it here.",
|
|
5004
5225
|
"Copy the secret into your own endpoint.",
|
|
5005
5226
|
"Add a Send webhook step to a workflow and put your endpoint URL on it. The URL belongs to the step rather than the connection, so one connection can serve several endpoints.",
|
|
5006
|
-
"On each request, compute HMAC-SHA256 of
|
|
5227
|
+
"On each request, read the X-Drawbridge-Signature header \u2014 `t=<unix seconds>,v1=<hex>` \u2014 and compute HMAC-SHA256 of `<t>.<raw request body>` with the secret. Use the raw bytes, not a re-serialised parse.",
|
|
5228
|
+
"Compare your result against each v1 value with a constant-time comparison, and reject any request whose timestamp is more than five minutes old.",
|
|
5229
|
+
"After you regenerate the secret, the previous one keeps signing for 24 hours (as a second v1), so update your endpoint within that window."
|
|
5007
5230
|
]
|
|
5008
5231
|
},
|
|
5009
5232
|
// Nothing to be exclusive with — there is no second webhook vendor, and a
|
|
@@ -5071,13 +5294,14 @@ var webhook_default = {
|
|
|
5071
5294
|
const { headers = {}, method = "POST", url } = step.settings || {};
|
|
5072
5295
|
const request2 = { method, url: url || null };
|
|
5073
5296
|
if (!url) return { message: "Outgoing webhook URL is not configured for this step.", request: request2, response: { skipped: true }, skipped: true };
|
|
5297
|
+
if (!/^https:\/\//i.test(url)) return { message: "Outgoing webhook URL must use https.", request: request2, response: { skipped: true }, skipped: true };
|
|
5074
5298
|
const body = lead || context;
|
|
5075
5299
|
request2.body = body;
|
|
5076
5300
|
const outgoing = { ...headers };
|
|
5077
5301
|
if (settings == null ? void 0 : settings.secret) {
|
|
5078
|
-
outgoing["X-Drawbridge-Signature"] =
|
|
5302
|
+
outgoing["X-Drawbridge-Signature"] = signature({ body, settings });
|
|
5079
5303
|
}
|
|
5080
|
-
const response = await send2({ body, headers: outgoing, method, url });
|
|
5304
|
+
const response = await send2({ body, headers: outgoing, maxContentLength: RESPONSE_LIMIT, method, url });
|
|
5081
5305
|
return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
|
|
5082
5306
|
}
|
|
5083
5307
|
}
|
|
@@ -5126,7 +5350,7 @@ var webhook_default = {
|
|
|
5126
5350
|
// and a task repeating it talks over the button.
|
|
5127
5351
|
tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
|
|
5128
5352
|
{
|
|
5129
|
-
message:
|
|
5353
|
+
message: 'Compute HMAC-SHA256( secret, t + "." + raw body ) and constant-time compare it against a v1 in the X-Drawbridge-Signature header; reject timestamps older than five minutes.',
|
|
5130
5354
|
title: "Requests must be verified",
|
|
5131
5355
|
type: "warning"
|
|
5132
5356
|
}
|