@drawbridge/drawbridge-utils 0.0.124 → 0.0.126
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 +138 -25
- package/dist/connections/index.d.cts +171 -15
- package/dist/connections/index.d.ts +171 -15
- package/dist/connections/index.js +132 -19
- package/dist/providers.cjs +135 -22
- package/dist/providers.js +129 -16
- package/package.json +1 -1
package/dist/providers.cjs
CHANGED
|
@@ -221,7 +221,7 @@ var HOOKS = Object.freeze({
|
|
|
221
221
|
])
|
|
222
222
|
});
|
|
223
223
|
var HOOK_EFFECTS = Object.freeze(["enqueues", "events", "writes"]);
|
|
224
|
-
var WRITE_OPERATIONS = Object.freeze(["create", "update"]);
|
|
224
|
+
var WRITE_OPERATIONS = Object.freeze(["create", "delete", "update"]);
|
|
225
225
|
var HOOK_PROPS = Object.freeze([
|
|
226
226
|
"channel",
|
|
227
227
|
"clientId",
|
|
@@ -799,9 +799,13 @@ var attentive_default2 = {
|
|
|
799
799
|
}
|
|
800
800
|
},
|
|
801
801
|
// WHY, in the merchant's words, and what to do about it.
|
|
802
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
803
|
+
// is reconnecting — prompting "choose a segment" there asks the merchant to
|
|
804
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
805
|
+
// moment: the grant is good and the segment is the missing half.
|
|
802
806
|
tasks: (data2) => {
|
|
803
807
|
var _a;
|
|
804
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
|
|
808
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
|
|
805
809
|
{
|
|
806
810
|
message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
|
|
807
811
|
title: "Choose a segment"
|
|
@@ -811,6 +815,9 @@ var attentive_default2 = {
|
|
|
811
815
|
title: "Attentive"
|
|
812
816
|
};
|
|
813
817
|
|
|
818
|
+
// lib/connections/providers/drawbridge.js
|
|
819
|
+
var import_node_crypto2 = require("crypto");
|
|
820
|
+
|
|
814
821
|
// lib/http.js
|
|
815
822
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
816
823
|
var request = async ({
|
|
@@ -1637,6 +1644,8 @@ var channels = {
|
|
|
1637
1644
|
};
|
|
1638
1645
|
|
|
1639
1646
|
// lib/connections/providers/drawbridge.js
|
|
1647
|
+
var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
|
|
1648
|
+
var START_KEYWORDS = ["START", "UNSTOP", "YES"];
|
|
1640
1649
|
var interpolate = (template, data2) => {
|
|
1641
1650
|
if (!template) return template;
|
|
1642
1651
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (data2 == null ? void 0 : data2[key]) != null ? String(data2[key]) : "{{" + key + "}}");
|
|
@@ -1864,7 +1873,88 @@ var drawbridge_default2 = {
|
|
|
1864
1873
|
};
|
|
1865
1874
|
}
|
|
1866
1875
|
},
|
|
1867
|
-
|
|
1876
|
+
// INBOUND SMS to the platform number. Twilio's webhook, verified and
|
|
1877
|
+
// processed HERE — the manifest owns its own inbound, the same as every
|
|
1878
|
+
// other vendor, and the bespoke webhooks route + the handler that lived in
|
|
1879
|
+
// sync's buffer table are gone.
|
|
1880
|
+
inbound: {
|
|
1881
|
+
// EVERY POST ON THIS CHANNEL IS AN INBOUND MESSAGE. Twilio sends no
|
|
1882
|
+
// topic header — the registered url IS the topic.
|
|
1883
|
+
event: () => "message.inbound",
|
|
1884
|
+
// STOP AND START, AS DESCRIBED WRITES. A withdrawn number is withdrawn
|
|
1885
|
+
// for everyone — organization : null is the platform floor canSend()
|
|
1886
|
+
// reads on every send path. $setOnInsert + upsert so webhook replays
|
|
1887
|
+
// and repeat STOPs collapse onto the unique { channel, address,
|
|
1888
|
+
// organization } index instead of erroring.
|
|
1889
|
+
process: ({ context }) => {
|
|
1890
|
+
var _a, _b;
|
|
1891
|
+
const keyword = String(((_a = context == null ? void 0 : context.data) == null ? void 0 : _a.Body) || "").trim().toUpperCase();
|
|
1892
|
+
const address = toE164((_b = context == null ? void 0 : context.data) == null ? void 0 : _b.From);
|
|
1893
|
+
if (!address) return { message: "Inbound SMS carried no usable sender number.", skipped: true };
|
|
1894
|
+
if (STOP_KEYWORDS.includes(keyword)) {
|
|
1895
|
+
return {
|
|
1896
|
+
message: "STOP recorded \u2014 " + address + " is suppressed on SMS everywhere.",
|
|
1897
|
+
request: { keyword },
|
|
1898
|
+
writes: [{
|
|
1899
|
+
collection: "suppression",
|
|
1900
|
+
data: {
|
|
1901
|
+
$setOnInsert: {
|
|
1902
|
+
address,
|
|
1903
|
+
channel: "sms",
|
|
1904
|
+
organization: null,
|
|
1905
|
+
reason: keyword,
|
|
1906
|
+
source: "stop"
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
operation: "update",
|
|
1910
|
+
options: { upsert: true },
|
|
1911
|
+
query: {
|
|
1912
|
+
address,
|
|
1913
|
+
channel: "sms",
|
|
1914
|
+
organization: null
|
|
1915
|
+
}
|
|
1916
|
+
}]
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
if (START_KEYWORDS.includes(keyword)) {
|
|
1920
|
+
return {
|
|
1921
|
+
message: "START recorded \u2014 " + address + " can receive SMS again.",
|
|
1922
|
+
request: { keyword },
|
|
1923
|
+
writes: [{
|
|
1924
|
+
collection: "suppression",
|
|
1925
|
+
operation: "delete",
|
|
1926
|
+
query: {
|
|
1927
|
+
address,
|
|
1928
|
+
channel: "sms",
|
|
1929
|
+
organization: null
|
|
1930
|
+
}
|
|
1931
|
+
}]
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
return { message: "A real reply, not a keyword \u2014 nothing to record.", skipped: true };
|
|
1935
|
+
},
|
|
1936
|
+
receive: ({ payload }) => ({
|
|
1937
|
+
data: payload,
|
|
1938
|
+
provider: { id: (payload == null ? void 0 : payload.MessageSid) || null }
|
|
1939
|
+
}),
|
|
1940
|
+
// TWILIO'S SCHEME, from docs.twilio.com/usage/security: take the full
|
|
1941
|
+
// registered url, sort the POST parameters alphabetically (Unix-style,
|
|
1942
|
+
// case-sensitive), append each name and value with no delimiters, sign
|
|
1943
|
+
// with HMAC-SHA1 keyed by the AuthToken, base64 — compared against
|
|
1944
|
+
// X-Twilio-Signature. It signs the URL rather than the raw body, which
|
|
1945
|
+
// is exactly why the shared body-HMAC verifier cannot cover it and this
|
|
1946
|
+
// hook exists.
|
|
1947
|
+
verify: ({ body, headers, secret, url }) => {
|
|
1948
|
+
if (!secret) throw Object.assign(new Error("Missing webhook secret: TWILIO_AUTH_TOKEN"), { status: 500 });
|
|
1949
|
+
const params = new URLSearchParams(String(body || ""));
|
|
1950
|
+
const signed = url + [...params.keys()].sort().map((key) => key + params.get(key)).join("");
|
|
1951
|
+
const expected = (0, import_node_crypto2.createHmac)("sha1", secret).update(signed).digest("base64");
|
|
1952
|
+
const provided = String((headers == null ? void 0 : headers["x-twilio-signature"]) || "");
|
|
1953
|
+
const matches = expected.length === provided.length && (0, import_node_crypto2.timingSafeEqual)(Buffer.from(expected), Buffer.from(provided));
|
|
1954
|
+
if (!matches) throw Object.assign(new Error("Invalid Twilio signature"), { status: 401 });
|
|
1955
|
+
return Object.fromEntries(params);
|
|
1956
|
+
}
|
|
1957
|
+
},
|
|
1868
1958
|
lifecycle: false,
|
|
1869
1959
|
resources: {
|
|
1870
1960
|
audiences: false,
|
|
@@ -2043,6 +2133,18 @@ var drawbridge_default2 = {
|
|
|
2043
2133
|
webhook: false
|
|
2044
2134
|
},
|
|
2045
2135
|
icon: drawbridge_default,
|
|
2136
|
+
// WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
|
|
2137
|
+
// verifies it. `secret` names the drawbridge provider's smsToken — the route
|
|
2138
|
+
// resolves the name to the stored value and hands it to verify.
|
|
2139
|
+
inbound: {
|
|
2140
|
+
headers: {
|
|
2141
|
+
id: "i-twilio-idempotency-token",
|
|
2142
|
+
signature: "x-twilio-signature"
|
|
2143
|
+
},
|
|
2144
|
+
signature: {
|
|
2145
|
+
secret: "TWILIO_AUTH_TOKEN"
|
|
2146
|
+
}
|
|
2147
|
+
},
|
|
2046
2148
|
// PRIVATE: never in the catalog, always available to the builder.
|
|
2047
2149
|
private: true,
|
|
2048
2150
|
// THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
|
|
@@ -2658,9 +2760,13 @@ var klaviyo_default2 = {
|
|
|
2658
2760
|
}
|
|
2659
2761
|
},
|
|
2660
2762
|
// WHY, in the merchant's words, and what to do about it.
|
|
2763
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
2764
|
+
// is reconnecting — prompting "choose a list" there asks the merchant to
|
|
2765
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
2766
|
+
// moment: the grant is good and the list is the missing half.
|
|
2661
2767
|
tasks: (data2) => {
|
|
2662
2768
|
var _a;
|
|
2663
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
|
|
2769
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
|
|
2664
2770
|
{
|
|
2665
2771
|
message: "Choose which Klaviyo list your contacts should sync into. Until you do, nothing is being synced.",
|
|
2666
2772
|
title: "Choose a list"
|
|
@@ -2671,7 +2777,7 @@ var klaviyo_default2 = {
|
|
|
2671
2777
|
};
|
|
2672
2778
|
|
|
2673
2779
|
// lib/connections/providers/mailchimp.js
|
|
2674
|
-
var
|
|
2780
|
+
var import_node_crypto3 = require("crypto");
|
|
2675
2781
|
|
|
2676
2782
|
// lib/connections/icons/mailchimp.js
|
|
2677
2783
|
var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
@@ -2703,7 +2809,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
|
|
|
2703
2809
|
}
|
|
2704
2810
|
return response.json();
|
|
2705
2811
|
};
|
|
2706
|
-
var subscriberHash = (email) => (0,
|
|
2812
|
+
var subscriberHash = (email) => (0, import_node_crypto3.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
|
|
2707
2813
|
var mailchimp_default2 = {
|
|
2708
2814
|
// OAUTH 2, authorization code. Every url below is quoted from
|
|
2709
2815
|
// mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
|
|
@@ -2745,7 +2851,7 @@ var mailchimp_default2 = {
|
|
|
2745
2851
|
content: {
|
|
2746
2852
|
confirm: "Disconnecting removes Drawbridge's stored Mailchimp access. You can also remove Drawbridge from the Authorized Apps page in your Mailchimp account. Your contacts stay in both Drawbridge and Mailchimp \u2014 neither list is deleted.",
|
|
2747
2853
|
description: [
|
|
2748
|
-
"Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under
|
|
2854
|
+
"Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under Messaging in your organization settings puts your own brand in the from line.",
|
|
2749
2855
|
"Connecting Mailchimp lets Drawbridge sync the contacts your campaigns collect into a Mailchimp audience, so the people who enter a giveaway can be marketed to alongside the rest of your list.",
|
|
2750
2856
|
"Anyone who has opted out in Drawbridge is synced as unsubscribed rather than omitted, so a person who asked not to be contacted stays suppressed in both systems instead of quietly reappearing.",
|
|
2751
2857
|
"Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
|
|
@@ -2973,9 +3079,13 @@ var mailchimp_default2 = {
|
|
|
2973
3079
|
}
|
|
2974
3080
|
},
|
|
2975
3081
|
// WHY, in the merchant's words, and what to do about it.
|
|
3082
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
3083
|
+
// is reconnecting — prompting "choose a audience" there asks the merchant to
|
|
3084
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
3085
|
+
// moment: the grant is good and the audience is the missing half.
|
|
2976
3086
|
tasks: (data2) => {
|
|
2977
3087
|
var _a;
|
|
2978
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
|
|
3088
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
|
|
2979
3089
|
{
|
|
2980
3090
|
message: "Choose which Mailchimp audience your contacts should sync into. Until you do, nothing is being synced.",
|
|
2981
3091
|
title: "Choose an audience"
|
|
@@ -2986,7 +3096,7 @@ var mailchimp_default2 = {
|
|
|
2986
3096
|
};
|
|
2987
3097
|
|
|
2988
3098
|
// lib/connections/providers/shopify.js
|
|
2989
|
-
var
|
|
3099
|
+
var import_node_crypto5 = require("crypto");
|
|
2990
3100
|
var import_nanoid2 = require("nanoid");
|
|
2991
3101
|
|
|
2992
3102
|
// lib/connections/icons/shopify.js
|
|
@@ -2997,7 +3107,7 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
2997
3107
|
</svg>`;
|
|
2998
3108
|
|
|
2999
3109
|
// lib/connections/inbound.js
|
|
3000
|
-
var
|
|
3110
|
+
var import_node_crypto4 = require("crypto");
|
|
3001
3111
|
var verifySignature = ({ body, descriptor, headers, secret }) => {
|
|
3002
3112
|
if (!secret) {
|
|
3003
3113
|
throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
|
|
@@ -3006,10 +3116,10 @@ var verifySignature = ({ body, descriptor, headers, secret }) => {
|
|
|
3006
3116
|
if (!provided) {
|
|
3007
3117
|
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
3008
3118
|
}
|
|
3009
|
-
const digest = (0,
|
|
3119
|
+
const digest = (0, import_node_crypto4.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
|
|
3010
3120
|
const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
|
|
3011
3121
|
const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
|
|
3012
|
-
if (digestBuffer.length !== providedBuffer.length || !(0,
|
|
3122
|
+
if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto4.timingSafeEqual)(digestBuffer, providedBuffer)) {
|
|
3013
3123
|
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
3014
3124
|
}
|
|
3015
3125
|
return JSON.parse(body.toString());
|
|
@@ -3748,7 +3858,7 @@ var shopify_default2 = {
|
|
|
3748
3858
|
event: "shopify.register.webhooks"
|
|
3749
3859
|
},
|
|
3750
3860
|
name: "register",
|
|
3751
|
-
options: { jobId: "connection.update.register." + workflow.connection + "." + (0,
|
|
3861
|
+
options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto5.randomUUID)() },
|
|
3752
3862
|
queue: "connection"
|
|
3753
3863
|
}],
|
|
3754
3864
|
message: (scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.",
|
|
@@ -4035,7 +4145,7 @@ var shopify_default2 = {
|
|
|
4035
4145
|
};
|
|
4036
4146
|
|
|
4037
4147
|
// lib/connections/providers/webhook.js
|
|
4038
|
-
var
|
|
4148
|
+
var import_node_crypto6 = __toESM(require("crypto"), 1);
|
|
4039
4149
|
|
|
4040
4150
|
// lib/safe-http.js
|
|
4041
4151
|
var import_dns2 = __toESM(require("dns"), 1);
|
|
@@ -4267,7 +4377,7 @@ var webhook_default = {
|
|
|
4267
4377
|
request2.body = body;
|
|
4268
4378
|
const outgoing = { ...headers };
|
|
4269
4379
|
if (settings == null ? void 0 : settings.secret) {
|
|
4270
|
-
outgoing["X-Drawbridge-Signature"] = "sha256=" +
|
|
4380
|
+
outgoing["X-Drawbridge-Signature"] = "sha256=" + import_node_crypto6.default.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
|
|
4271
4381
|
}
|
|
4272
4382
|
const response = await send2({ body, headers: outgoing, method, url });
|
|
4273
4383
|
return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
|
|
@@ -4313,7 +4423,10 @@ var webhook_default = {
|
|
|
4313
4423
|
// pressing Connect will do; afterwards it states the verification the
|
|
4314
4424
|
// merchant's own endpoint has to perform, because a signed payload nobody
|
|
4315
4425
|
// checks is an unsigned payload.
|
|
4316
|
-
|
|
4426
|
+
// The connect prompt only where connecting is not already the card's whole
|
|
4427
|
+
// story: a disconnected or errored document renders a Connect action itself,
|
|
4428
|
+
// and a task repeating it talks over the button.
|
|
4429
|
+
tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
|
|
4317
4430
|
{
|
|
4318
4431
|
message: "Compute HMAC-SHA256( secret, body ) and compare against the X-Drawbridge-Signature header to confirm each payload.",
|
|
4319
4432
|
title: "Requests must be verified",
|
|
@@ -4338,7 +4451,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
|
|
|
4338
4451
|
([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
|
|
4339
4452
|
);
|
|
4340
4453
|
var build = (manifest) => {
|
|
4341
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
|
|
4454
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
|
|
4342
4455
|
if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
|
|
4343
4456
|
if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
|
|
4344
4457
|
if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
|
|
@@ -4417,16 +4530,16 @@ var build = (manifest) => {
|
|
|
4417
4530
|
);
|
|
4418
4531
|
}
|
|
4419
4532
|
}
|
|
4420
|
-
if (implemented(manifest.hooks, "inbound.event") &&
|
|
4533
|
+
if (implemented(manifest.hooks, "inbound.event") && typeof ((_l = (_k = manifest.hooks) == null ? void 0 : _k.inbound) == null ? void 0 : _l.event) !== "function" && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.event)) {
|
|
4421
4534
|
throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
|
|
4422
4535
|
}
|
|
4423
|
-
if (implemented(manifest.hooks, "inbound.verify") && !((
|
|
4536
|
+
if (implemented(manifest.hooks, "inbound.verify") && !((_p = (_o = manifest.inbound) == null ? void 0 : _o.headers) == null ? void 0 : _p.signature)) {
|
|
4424
4537
|
throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
|
|
4425
4538
|
}
|
|
4426
4539
|
if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
|
|
4427
4540
|
throw new Error(manifest.slug + " must declare status( data ) \u2014 return null to accept the connection's own status, or { message, status } to override it");
|
|
4428
4541
|
}
|
|
4429
|
-
if (!Array.isArray((
|
|
4542
|
+
if (!Array.isArray((_q = manifest == null ? void 0 : manifest.content) == null ? void 0 : _q.guide) || !manifest.content.guide.length) {
|
|
4430
4543
|
throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
|
|
4431
4544
|
}
|
|
4432
4545
|
if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
|
|
@@ -4438,8 +4551,8 @@ var build = (manifest) => {
|
|
|
4438
4551
|
}
|
|
4439
4552
|
for (const [domain, verbs] of Object.entries(HOOKS)) {
|
|
4440
4553
|
for (const verb of verbs) {
|
|
4441
|
-
const hook = (
|
|
4442
|
-
if (((
|
|
4554
|
+
const hook = (_s = (_r = manifest.hooks) == null ? void 0 : _r[domain]) == null ? void 0 : _s[verb];
|
|
4555
|
+
if (((_t = manifest.hooks) == null ? void 0 : _t[domain]) === false) continue;
|
|
4443
4556
|
if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
|
|
4444
4557
|
throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
|
|
4445
4558
|
}
|
package/dist/providers.js
CHANGED
|
@@ -177,7 +177,7 @@ var HOOKS = Object.freeze({
|
|
|
177
177
|
])
|
|
178
178
|
});
|
|
179
179
|
var HOOK_EFFECTS = Object.freeze(["enqueues", "events", "writes"]);
|
|
180
|
-
var WRITE_OPERATIONS = Object.freeze(["create", "update"]);
|
|
180
|
+
var WRITE_OPERATIONS = Object.freeze(["create", "delete", "update"]);
|
|
181
181
|
var HOOK_PROPS = Object.freeze([
|
|
182
182
|
"channel",
|
|
183
183
|
"clientId",
|
|
@@ -755,9 +755,13 @@ var attentive_default2 = {
|
|
|
755
755
|
}
|
|
756
756
|
},
|
|
757
757
|
// WHY, in the merchant's words, and what to do about it.
|
|
758
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
759
|
+
// is reconnecting — prompting "choose a segment" there asks the merchant to
|
|
760
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
761
|
+
// moment: the grant is good and the segment is the missing half.
|
|
758
762
|
tasks: (data2) => {
|
|
759
763
|
var _a;
|
|
760
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
|
|
764
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
|
|
761
765
|
{
|
|
762
766
|
message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
|
|
763
767
|
title: "Choose a segment"
|
|
@@ -767,6 +771,9 @@ var attentive_default2 = {
|
|
|
767
771
|
title: "Attentive"
|
|
768
772
|
};
|
|
769
773
|
|
|
774
|
+
// lib/connections/providers/drawbridge.js
|
|
775
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
776
|
+
|
|
770
777
|
// lib/http.js
|
|
771
778
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
772
779
|
var request = async ({
|
|
@@ -1593,6 +1600,8 @@ var channels = {
|
|
|
1593
1600
|
};
|
|
1594
1601
|
|
|
1595
1602
|
// lib/connections/providers/drawbridge.js
|
|
1603
|
+
var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
|
|
1604
|
+
var START_KEYWORDS = ["START", "UNSTOP", "YES"];
|
|
1596
1605
|
var interpolate = (template, data2) => {
|
|
1597
1606
|
if (!template) return template;
|
|
1598
1607
|
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (data2 == null ? void 0 : data2[key]) != null ? String(data2[key]) : "{{" + key + "}}");
|
|
@@ -1820,7 +1829,88 @@ var drawbridge_default2 = {
|
|
|
1820
1829
|
};
|
|
1821
1830
|
}
|
|
1822
1831
|
},
|
|
1823
|
-
|
|
1832
|
+
// INBOUND SMS to the platform number. Twilio's webhook, verified and
|
|
1833
|
+
// processed HERE — the manifest owns its own inbound, the same as every
|
|
1834
|
+
// other vendor, and the bespoke webhooks route + the handler that lived in
|
|
1835
|
+
// sync's buffer table are gone.
|
|
1836
|
+
inbound: {
|
|
1837
|
+
// EVERY POST ON THIS CHANNEL IS AN INBOUND MESSAGE. Twilio sends no
|
|
1838
|
+
// topic header — the registered url IS the topic.
|
|
1839
|
+
event: () => "message.inbound",
|
|
1840
|
+
// STOP AND START, AS DESCRIBED WRITES. A withdrawn number is withdrawn
|
|
1841
|
+
// for everyone — organization : null is the platform floor canSend()
|
|
1842
|
+
// reads on every send path. $setOnInsert + upsert so webhook replays
|
|
1843
|
+
// and repeat STOPs collapse onto the unique { channel, address,
|
|
1844
|
+
// organization } index instead of erroring.
|
|
1845
|
+
process: ({ context }) => {
|
|
1846
|
+
var _a, _b;
|
|
1847
|
+
const keyword = String(((_a = context == null ? void 0 : context.data) == null ? void 0 : _a.Body) || "").trim().toUpperCase();
|
|
1848
|
+
const address = toE164((_b = context == null ? void 0 : context.data) == null ? void 0 : _b.From);
|
|
1849
|
+
if (!address) return { message: "Inbound SMS carried no usable sender number.", skipped: true };
|
|
1850
|
+
if (STOP_KEYWORDS.includes(keyword)) {
|
|
1851
|
+
return {
|
|
1852
|
+
message: "STOP recorded \u2014 " + address + " is suppressed on SMS everywhere.",
|
|
1853
|
+
request: { keyword },
|
|
1854
|
+
writes: [{
|
|
1855
|
+
collection: "suppression",
|
|
1856
|
+
data: {
|
|
1857
|
+
$setOnInsert: {
|
|
1858
|
+
address,
|
|
1859
|
+
channel: "sms",
|
|
1860
|
+
organization: null,
|
|
1861
|
+
reason: keyword,
|
|
1862
|
+
source: "stop"
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1865
|
+
operation: "update",
|
|
1866
|
+
options: { upsert: true },
|
|
1867
|
+
query: {
|
|
1868
|
+
address,
|
|
1869
|
+
channel: "sms",
|
|
1870
|
+
organization: null
|
|
1871
|
+
}
|
|
1872
|
+
}]
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
if (START_KEYWORDS.includes(keyword)) {
|
|
1876
|
+
return {
|
|
1877
|
+
message: "START recorded \u2014 " + address + " can receive SMS again.",
|
|
1878
|
+
request: { keyword },
|
|
1879
|
+
writes: [{
|
|
1880
|
+
collection: "suppression",
|
|
1881
|
+
operation: "delete",
|
|
1882
|
+
query: {
|
|
1883
|
+
address,
|
|
1884
|
+
channel: "sms",
|
|
1885
|
+
organization: null
|
|
1886
|
+
}
|
|
1887
|
+
}]
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
return { message: "A real reply, not a keyword \u2014 nothing to record.", skipped: true };
|
|
1891
|
+
},
|
|
1892
|
+
receive: ({ payload }) => ({
|
|
1893
|
+
data: payload,
|
|
1894
|
+
provider: { id: (payload == null ? void 0 : payload.MessageSid) || null }
|
|
1895
|
+
}),
|
|
1896
|
+
// TWILIO'S SCHEME, from docs.twilio.com/usage/security: take the full
|
|
1897
|
+
// registered url, sort the POST parameters alphabetically (Unix-style,
|
|
1898
|
+
// case-sensitive), append each name and value with no delimiters, sign
|
|
1899
|
+
// with HMAC-SHA1 keyed by the AuthToken, base64 — compared against
|
|
1900
|
+
// X-Twilio-Signature. It signs the URL rather than the raw body, which
|
|
1901
|
+
// is exactly why the shared body-HMAC verifier cannot cover it and this
|
|
1902
|
+
// hook exists.
|
|
1903
|
+
verify: ({ body, headers, secret, url }) => {
|
|
1904
|
+
if (!secret) throw Object.assign(new Error("Missing webhook secret: TWILIO_AUTH_TOKEN"), { status: 500 });
|
|
1905
|
+
const params = new URLSearchParams(String(body || ""));
|
|
1906
|
+
const signed = url + [...params.keys()].sort().map((key) => key + params.get(key)).join("");
|
|
1907
|
+
const expected = createHmac("sha1", secret).update(signed).digest("base64");
|
|
1908
|
+
const provided = String((headers == null ? void 0 : headers["x-twilio-signature"]) || "");
|
|
1909
|
+
const matches = expected.length === provided.length && timingSafeEqual(Buffer.from(expected), Buffer.from(provided));
|
|
1910
|
+
if (!matches) throw Object.assign(new Error("Invalid Twilio signature"), { status: 401 });
|
|
1911
|
+
return Object.fromEntries(params);
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1824
1914
|
lifecycle: false,
|
|
1825
1915
|
resources: {
|
|
1826
1916
|
audiences: false,
|
|
@@ -1999,6 +2089,18 @@ var drawbridge_default2 = {
|
|
|
1999
2089
|
webhook: false
|
|
2000
2090
|
},
|
|
2001
2091
|
icon: drawbridge_default,
|
|
2092
|
+
// WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
|
|
2093
|
+
// verifies it. `secret` names the drawbridge provider's smsToken — the route
|
|
2094
|
+
// resolves the name to the stored value and hands it to verify.
|
|
2095
|
+
inbound: {
|
|
2096
|
+
headers: {
|
|
2097
|
+
id: "i-twilio-idempotency-token",
|
|
2098
|
+
signature: "x-twilio-signature"
|
|
2099
|
+
},
|
|
2100
|
+
signature: {
|
|
2101
|
+
secret: "TWILIO_AUTH_TOKEN"
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2002
2104
|
// PRIVATE: never in the catalog, always available to the builder.
|
|
2003
2105
|
private: true,
|
|
2004
2106
|
// THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
|
|
@@ -2614,9 +2716,13 @@ var klaviyo_default2 = {
|
|
|
2614
2716
|
}
|
|
2615
2717
|
},
|
|
2616
2718
|
// WHY, in the merchant's words, and what to do about it.
|
|
2719
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
2720
|
+
// is reconnecting — prompting "choose a list" there asks the merchant to
|
|
2721
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
2722
|
+
// moment: the grant is good and the list is the missing half.
|
|
2617
2723
|
tasks: (data2) => {
|
|
2618
2724
|
var _a;
|
|
2619
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
|
|
2725
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.list) ? [] : [
|
|
2620
2726
|
{
|
|
2621
2727
|
message: "Choose which Klaviyo list your contacts should sync into. Until you do, nothing is being synced.",
|
|
2622
2728
|
title: "Choose a list"
|
|
@@ -2701,7 +2807,7 @@ var mailchimp_default2 = {
|
|
|
2701
2807
|
content: {
|
|
2702
2808
|
confirm: "Disconnecting removes Drawbridge's stored Mailchimp access. You can also remove Drawbridge from the Authorized Apps page in your Mailchimp account. Your contacts stay in both Drawbridge and Mailchimp \u2014 neither list is deleted.",
|
|
2703
2809
|
description: [
|
|
2704
|
-
"Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under
|
|
2810
|
+
"Drawbridge no longer sends email through Mailchimp. Notification email now sends from Drawbridge itself, and verifying a domain under Messaging in your organization settings puts your own brand in the from line.",
|
|
2705
2811
|
"Connecting Mailchimp lets Drawbridge sync the contacts your campaigns collect into a Mailchimp audience, so the people who enter a giveaway can be marketed to alongside the rest of your list.",
|
|
2706
2812
|
"Anyone who has opted out in Drawbridge is synced as unsubscribed rather than omitted, so a person who asked not to be contacted stays suppressed in both systems instead of quietly reappearing.",
|
|
2707
2813
|
"Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
|
|
@@ -2929,9 +3035,13 @@ var mailchimp_default2 = {
|
|
|
2929
3035
|
}
|
|
2930
3036
|
},
|
|
2931
3037
|
// WHY, in the merchant's words, and what to do about it.
|
|
3038
|
+
// ONLY FOR A LIVE GRANT. A disconnected or errored connection's next step
|
|
3039
|
+
// is reconnecting — prompting "choose a audience" there asks the merchant to
|
|
3040
|
+
// configure a grant that no longer exists. `pending` is exactly this task's
|
|
3041
|
+
// moment: the grant is good and the audience is the missing half.
|
|
2932
3042
|
tasks: (data2) => {
|
|
2933
3043
|
var _a;
|
|
2934
|
-
return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
|
|
3044
|
+
return !["active", "pending"].includes(data2 == null ? void 0 : data2.status) || ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.audience) ? [] : [
|
|
2935
3045
|
{
|
|
2936
3046
|
message: "Choose which Mailchimp audience your contacts should sync into. Until you do, nothing is being synced.",
|
|
2937
3047
|
title: "Choose an audience"
|
|
@@ -2953,7 +3063,7 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
2953
3063
|
</svg>`;
|
|
2954
3064
|
|
|
2955
3065
|
// lib/connections/inbound.js
|
|
2956
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
3066
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2957
3067
|
var verifySignature = ({ body, descriptor, headers, secret }) => {
|
|
2958
3068
|
if (!secret) {
|
|
2959
3069
|
throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
|
|
@@ -2962,10 +3072,10 @@ var verifySignature = ({ body, descriptor, headers, secret }) => {
|
|
|
2962
3072
|
if (!provided) {
|
|
2963
3073
|
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
2964
3074
|
}
|
|
2965
|
-
const digest =
|
|
3075
|
+
const digest = createHmac2(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
|
|
2966
3076
|
const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
|
|
2967
3077
|
const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
|
|
2968
|
-
if (digestBuffer.length !== providedBuffer.length || !
|
|
3078
|
+
if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual2(digestBuffer, providedBuffer)) {
|
|
2969
3079
|
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
2970
3080
|
}
|
|
2971
3081
|
return JSON.parse(body.toString());
|
|
@@ -4269,7 +4379,10 @@ var webhook_default = {
|
|
|
4269
4379
|
// pressing Connect will do; afterwards it states the verification the
|
|
4270
4380
|
// merchant's own endpoint has to perform, because a signed payload nobody
|
|
4271
4381
|
// checks is an unsigned payload.
|
|
4272
|
-
|
|
4382
|
+
// The connect prompt only where connecting is not already the card's whole
|
|
4383
|
+
// story: a disconnected or errored document renders a Connect action itself,
|
|
4384
|
+
// and a task repeating it talks over the button.
|
|
4385
|
+
tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
|
|
4273
4386
|
{
|
|
4274
4387
|
message: "Compute HMAC-SHA256( secret, body ) and compare against the X-Drawbridge-Signature header to confirm each payload.",
|
|
4275
4388
|
title: "Requests must be verified",
|
|
@@ -4294,7 +4407,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
|
|
|
4294
4407
|
([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
|
|
4295
4408
|
);
|
|
4296
4409
|
var build = (manifest) => {
|
|
4297
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r;
|
|
4410
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
|
|
4298
4411
|
if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
|
|
4299
4412
|
if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
|
|
4300
4413
|
if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
|
|
@@ -4373,16 +4486,16 @@ var build = (manifest) => {
|
|
|
4373
4486
|
);
|
|
4374
4487
|
}
|
|
4375
4488
|
}
|
|
4376
|
-
if (implemented(manifest.hooks, "inbound.event") &&
|
|
4489
|
+
if (implemented(manifest.hooks, "inbound.event") && typeof ((_l = (_k = manifest.hooks) == null ? void 0 : _k.inbound) == null ? void 0 : _l.event) !== "function" && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.event)) {
|
|
4377
4490
|
throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
|
|
4378
4491
|
}
|
|
4379
|
-
if (implemented(manifest.hooks, "inbound.verify") && !((
|
|
4492
|
+
if (implemented(manifest.hooks, "inbound.verify") && !((_p = (_o = manifest.inbound) == null ? void 0 : _o.headers) == null ? void 0 : _p.signature)) {
|
|
4380
4493
|
throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
|
|
4381
4494
|
}
|
|
4382
4495
|
if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
|
|
4383
4496
|
throw new Error(manifest.slug + " must declare status( data ) \u2014 return null to accept the connection's own status, or { message, status } to override it");
|
|
4384
4497
|
}
|
|
4385
|
-
if (!Array.isArray((
|
|
4498
|
+
if (!Array.isArray((_q = manifest == null ? void 0 : manifest.content) == null ? void 0 : _q.guide) || !manifest.content.guide.length) {
|
|
4386
4499
|
throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
|
|
4387
4500
|
}
|
|
4388
4501
|
if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
|
|
@@ -4394,8 +4507,8 @@ var build = (manifest) => {
|
|
|
4394
4507
|
}
|
|
4395
4508
|
for (const [domain, verbs] of Object.entries(HOOKS)) {
|
|
4396
4509
|
for (const verb of verbs) {
|
|
4397
|
-
const hook = (
|
|
4398
|
-
if (((
|
|
4510
|
+
const hook = (_s = (_r = manifest.hooks) == null ? void 0 : _r[domain]) == null ? void 0 : _s[verb];
|
|
4511
|
+
if (((_t = manifest.hooks) == null ? void 0 : _t[domain]) === false) continue;
|
|
4399
4512
|
if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
|
|
4400
4513
|
throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
|
|
4401
4514
|
}
|
package/package.json
CHANGED