@drawbridge/drawbridge-utils 0.0.162 → 0.0.164
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 +105 -8
- package/dist/connections/index.d.cts +104 -4
- package/dist/connections/index.d.ts +104 -4
- package/dist/connections/index.js +105 -8
- package/dist/html.cjs +5 -2
- package/dist/html.d.cts +20 -1
- package/dist/html.d.ts +20 -1
- package/dist/html.js +3 -1
- package/dist/index.cjs +34 -1
- package/dist/index.d.cts +34 -1
- package/dist/index.d.ts +34 -1
- package/dist/index.js +34 -1
- package/dist/notification.cjs +84 -20
- package/dist/notification.d.cts +117 -34
- package/dist/notification.d.ts +117 -34
- package/dist/notification.js +82 -20
- package/dist/providers.cjs +137 -17
- package/dist/providers.d.cts +104 -12
- package/dist/providers.d.ts +104 -12
- package/dist/providers.js +133 -16
- package/package.json +1 -1
package/dist/notification.js
CHANGED
|
@@ -26,40 +26,102 @@ var phrasesIn = (value) => {
|
|
|
26
26
|
return PHRASES.filter((phrase) => lower.includes(phrase));
|
|
27
27
|
};
|
|
28
28
|
var list = (values) => values.map((value) => "\u201C" + value + "\u201D").join(", ");
|
|
29
|
-
var
|
|
29
|
+
var POINTS = {
|
|
30
|
+
brand: 0,
|
|
31
|
+
currency: 1,
|
|
32
|
+
emoji: 0.5,
|
|
33
|
+
length: 0.5,
|
|
34
|
+
phrase: 1,
|
|
35
|
+
punctuation: 1,
|
|
36
|
+
shouting: 1.5
|
|
37
|
+
};
|
|
38
|
+
var subjectRules = ({ brand, subject }) => {
|
|
30
39
|
const value = String(subject || "").trim();
|
|
31
40
|
if (!value) return [];
|
|
32
41
|
const found = phrasesIn(value);
|
|
33
42
|
const emojis = emojiCount(value);
|
|
34
43
|
return [
|
|
35
|
-
value.length > SUBJECT_DISPLAY_LIMIT &&
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
value.length > SUBJECT_DISPLAY_LIMIT && {
|
|
45
|
+
message: "Most email apps cut the subject around " + SUBJECT_DISPLAY_LIMIT + " characters \u2014 yours is " + value.length + ".",
|
|
46
|
+
points: POINTS.length,
|
|
47
|
+
rule: "length"
|
|
48
|
+
},
|
|
49
|
+
shouting(value) && {
|
|
50
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
51
|
+
points: POINTS.shouting,
|
|
52
|
+
rule: "shouting"
|
|
53
|
+
},
|
|
54
|
+
/[!?]{2,}/.test(value) && {
|
|
55
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
56
|
+
points: POINTS.punctuation,
|
|
57
|
+
rule: "punctuation"
|
|
58
|
+
},
|
|
59
|
+
emojis > 1 && {
|
|
60
|
+
message: "More than one emoji in a subject is a common spam signal.",
|
|
61
|
+
points: POINTS.emoji,
|
|
62
|
+
rule: "emoji"
|
|
63
|
+
},
|
|
64
|
+
/[$£€]\s?\d/.test(value) && {
|
|
65
|
+
message: "A currency amount in the subject is a common spam signal.",
|
|
66
|
+
points: POINTS.currency,
|
|
67
|
+
rule: "currency"
|
|
68
|
+
},
|
|
69
|
+
found.length > 0 && {
|
|
70
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
71
|
+
points: POINTS.phrase * found.length,
|
|
72
|
+
rule: "phrase"
|
|
73
|
+
},
|
|
74
|
+
brand && !value.toLowerCase().includes(String(brand).toLowerCase()) && {
|
|
75
|
+
message: "Nothing here says the mail is from " + brand + " \u2014 subjects that name the sender get opened more.",
|
|
76
|
+
points: POINTS.brand,
|
|
77
|
+
rule: "brand"
|
|
78
|
+
}
|
|
79
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "subject" }));
|
|
43
80
|
};
|
|
44
|
-
var
|
|
81
|
+
var messageRules = ({ message }) => {
|
|
45
82
|
const value = String(message || "").trim();
|
|
46
83
|
if (!value) return [];
|
|
47
84
|
const found = phrasesIn(value);
|
|
48
85
|
return [
|
|
49
|
-
value.length < MESSAGE_SHORT_LIMIT &&
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
86
|
+
value.length < MESSAGE_SHORT_LIMIT && {
|
|
87
|
+
message: "Very short messages read as a fragment, and bulk senders send exactly this shape.",
|
|
88
|
+
points: POINTS.length,
|
|
89
|
+
rule: "length"
|
|
90
|
+
},
|
|
91
|
+
shouting(value) && {
|
|
92
|
+
message: "Mostly capitals reads as shouting, and filters treat it that way too.",
|
|
93
|
+
points: POINTS.shouting,
|
|
94
|
+
rule: "shouting"
|
|
95
|
+
},
|
|
96
|
+
/[!?]{2,}/.test(value) && {
|
|
97
|
+
message: "Repeated exclamation or question marks are a common spam signal.",
|
|
98
|
+
points: POINTS.punctuation,
|
|
99
|
+
rule: "punctuation"
|
|
100
|
+
},
|
|
101
|
+
found.length > 0 && {
|
|
102
|
+
message: "Reads like a scam to filters and to people: " + list(found) + ".",
|
|
103
|
+
points: POINTS.phrase * found.length,
|
|
104
|
+
rule: "phrase"
|
|
105
|
+
}
|
|
106
|
+
].filter(Boolean).map((rule) => ({ ...rule, field: "message" }));
|
|
54
107
|
};
|
|
55
|
-
var
|
|
108
|
+
var notificationRules = ({
|
|
56
109
|
brand,
|
|
57
110
|
message,
|
|
58
111
|
subject
|
|
59
|
-
} = {}) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
112
|
+
} = {}) => [
|
|
113
|
+
...subjectRules({ brand, subject }),
|
|
114
|
+
...messageRules({ message })
|
|
115
|
+
];
|
|
116
|
+
var notificationWarnings = (input) => {
|
|
117
|
+
const rules = notificationRules(input);
|
|
118
|
+
return {
|
|
119
|
+
message: rules.filter((rule) => rule.field === "message").map((rule) => rule.message),
|
|
120
|
+
subject: rules.filter((rule) => rule.field === "subject").map((rule) => rule.message)
|
|
121
|
+
};
|
|
122
|
+
};
|
|
63
123
|
export {
|
|
124
|
+
POINTS,
|
|
125
|
+
notificationRules,
|
|
64
126
|
notificationWarnings
|
|
65
127
|
};
|
package/dist/providers.cjs
CHANGED
|
@@ -36,11 +36,14 @@ __export(providers_exports, {
|
|
|
36
36
|
providerEnvNames: () => providerEnvNames,
|
|
37
37
|
providerFields: () => providerFields,
|
|
38
38
|
providerMemo: () => providerMemo,
|
|
39
|
+
providerRow: () => providerRow,
|
|
39
40
|
providerSettings: () => providerSettings,
|
|
40
41
|
providerSlugs: () => providerSlugs,
|
|
41
42
|
saveProviderSettings: () => saveProviderSettings,
|
|
43
|
+
vendorEnabled: () => vendorEnabled,
|
|
42
44
|
vendorSettings: () => vendorSettings,
|
|
43
|
-
vendors: () => vendors
|
|
45
|
+
vendors: () => vendors,
|
|
46
|
+
vendorsEnabled: () => vendorsEnabled
|
|
44
47
|
});
|
|
45
48
|
module.exports = __toCommonJS(providers_exports);
|
|
46
49
|
|
|
@@ -540,10 +543,15 @@ var attentive_default2 = {
|
|
|
540
543
|
"Attentive accepts these updates and applies them in the background, so a contact appears in your segment shortly after the sync rather than the instant it runs."
|
|
541
544
|
],
|
|
542
545
|
excerpt: "Sync your Drawbridge contacts into an Attentive segment.",
|
|
546
|
+
// NAMES THE SEGMENT STEP, because `status` below gates on it: a grant with
|
|
547
|
+
// no segment chosen sits at Pending, and a guide that stops at the consent
|
|
548
|
+
// leaves a merchant looking at a connection they think is broken. Same
|
|
549
|
+
// omission Klaviyo's guide already carries a note about.
|
|
543
550
|
guide: [
|
|
544
551
|
"Press Connect. Drawbridge sends you to Attentive to approve access.",
|
|
545
552
|
"Sign in to Attentive if you are not already, and authorize the permissions listed.",
|
|
546
|
-
"You
|
|
553
|
+
"You come back here to choose which Attentive segment your contacts should sync into.",
|
|
554
|
+
"The connection shows Pending until you choose a segment, then Active."
|
|
547
555
|
]
|
|
548
556
|
},
|
|
549
557
|
// A contact destination, like Klaviyo and Mailchimp — a merchant could
|
|
@@ -806,6 +814,9 @@ var attentive_default2 = {
|
|
|
806
814
|
// merchant would recognise in a builder label.
|
|
807
815
|
key: "Sync contact to Attentive",
|
|
808
816
|
queue: "connection",
|
|
817
|
+
// Attentive applies the update in the background and returns no id we
|
|
818
|
+
// could hand on, so there is nothing for a later step to interpolate.
|
|
819
|
+
returns: [],
|
|
809
820
|
// Nothing for a merchant to configure on the step itself — the segment
|
|
810
821
|
// is chosen once on the connection. Declared empty rather than omitted,
|
|
811
822
|
// so "this step takes no settings" and "nobody thought about settings"
|
|
@@ -2033,13 +2044,18 @@ var drawbridge_default2 = {
|
|
|
2033
2044
|
request: request2,
|
|
2034
2045
|
response: { count, notified: recipients.length },
|
|
2035
2046
|
writes: recipients.map((member) => {
|
|
2036
|
-
var _a2, _b2;
|
|
2047
|
+
var _a2, _b2, _c2;
|
|
2037
2048
|
return queueNotification({
|
|
2038
2049
|
audience: "member",
|
|
2039
2050
|
message: interpolate((_a2 = step.settings) == null ? void 0 : _a2.message, values),
|
|
2040
2051
|
organization: workflow.organization,
|
|
2041
2052
|
send: { type: "email", email: member.email },
|
|
2042
|
-
|
|
2053
|
+
// The editor's plain rendering, interpolated the same way so
|
|
2054
|
+
// {{count}} resolves in the text part too. Spread rather than
|
|
2055
|
+
// written as null: a step saved before `text` existed has none,
|
|
2056
|
+
// and the send path strips the markup itself for those.
|
|
2057
|
+
...((_b2 = step.settings) == null ? void 0 : _b2.text) && { text: interpolate(step.settings.text, values) },
|
|
2058
|
+
title: interpolate((_c2 = step.settings) == null ? void 0 : _c2.subject, values),
|
|
2043
2059
|
workflow: workflow.id
|
|
2044
2060
|
});
|
|
2045
2061
|
})
|
|
@@ -2108,7 +2124,7 @@ var drawbridge_default2 = {
|
|
|
2108
2124
|
// unsubscribe token and the CAN-SPAM footer. The step's job is to say who
|
|
2109
2125
|
// and what, correctly, and to refuse early when it must not send at all.
|
|
2110
2126
|
send: async ({ context, step, workflow }, { canSend, read } = {}) => {
|
|
2111
|
-
var _a, _b;
|
|
2127
|
+
var _a, _b, _c;
|
|
2112
2128
|
const to = context == null ? void 0 : context.email;
|
|
2113
2129
|
if (!to) throw new Error("No email address on context (context.email is required)");
|
|
2114
2130
|
const request2 = { to };
|
|
@@ -2145,7 +2161,11 @@ var drawbridge_default2 = {
|
|
|
2145
2161
|
message: interpolate((_a = step.settings) == null ? void 0 : _a.message, context),
|
|
2146
2162
|
organization: workflow.organization,
|
|
2147
2163
|
send: { type: "email", email: to },
|
|
2148
|
-
|
|
2164
|
+
// The editor's plain rendering, so the text/plain part of this
|
|
2165
|
+
// email is what the merchant wrote rather than a regex's guess at
|
|
2166
|
+
// it. See the digest above for why it is spread, not nulled.
|
|
2167
|
+
...((_b = step.settings) == null ? void 0 : _b.text) && { text: interpolate(step.settings.text, context) },
|
|
2168
|
+
title: interpolate((_c = step.settings) == null ? void 0 : _c.subject, context),
|
|
2149
2169
|
workflow: workflow.id
|
|
2150
2170
|
})
|
|
2151
2171
|
]
|
|
@@ -2565,6 +2585,11 @@ var drawbridge_default2 = {
|
|
|
2565
2585
|
hook: "email.digest",
|
|
2566
2586
|
key: "Email \u2014 Digest",
|
|
2567
2587
|
queue: "notification",
|
|
2588
|
+
// Nothing for a later step to interpolate. Declared empty rather than
|
|
2589
|
+
// omitted, so "this step leaves nothing behind" and "nobody thought
|
|
2590
|
+
// about it" stay different statements — the second is what emptied the
|
|
2591
|
+
// builder's Variables menu for every step in the product.
|
|
2592
|
+
returns: [],
|
|
2568
2593
|
settings: {
|
|
2569
2594
|
// The organization OWNER is always a recipient, resolved by the
|
|
2570
2595
|
// hook, so this is additional recipients rather than the list. It
|
|
@@ -2573,7 +2598,22 @@ var drawbridge_default2 = {
|
|
|
2573
2598
|
// pick and could never save the step.
|
|
2574
2599
|
members: { of: "string", type: "array" },
|
|
2575
2600
|
message: { max: 2e3, required: true, type: "string" },
|
|
2576
|
-
|
|
2601
|
+
// THE LAST SPAM CHECK, stored beside the copy it measured. Shape is
|
|
2602
|
+
// pinned no further than the pair that proves the match, which is the
|
|
2603
|
+
// only part the api reads; the rest is SpamAssassin's own output and
|
|
2604
|
+
// is stored as it arrives. Same declaration the campaign's draw
|
|
2605
|
+
// notification validates against.
|
|
2606
|
+
//
|
|
2607
|
+
// Undeclared, this was silently dropped at submit — the settings
|
|
2608
|
+
// validator is noUnknown().strict(), so the dashboard's step form
|
|
2609
|
+
// mapped the key away rather than 400 — and every open of a workflow
|
|
2610
|
+
// step re-ran a check against a free service to be told what the last
|
|
2611
|
+
// open had already been told.
|
|
2612
|
+
score: { shape: { message: { type: "string" }, subject: { type: "string" } }, type: "object" },
|
|
2613
|
+
subject: { max: 150, required: true, type: "string" },
|
|
2614
|
+
// The editor's plain rendering of `message`, written beside it rather
|
|
2615
|
+
// than reconstructed by running a regex over the markup at send time.
|
|
2616
|
+
text: { max: 2e3, type: "string" }
|
|
2577
2617
|
},
|
|
2578
2618
|
triggers: ["schedule.day", "schedule.week", "schedule.month"],
|
|
2579
2619
|
usage: { actions: 0 }
|
|
@@ -2594,6 +2634,7 @@ var drawbridge_default2 = {
|
|
|
2594
2634
|
hook: "email.notify",
|
|
2595
2635
|
key: "Email \u2014 Notification",
|
|
2596
2636
|
queue: "notification",
|
|
2637
|
+
returns: [],
|
|
2597
2638
|
settings: {
|
|
2598
2639
|
members: { of: "string", type: "array" },
|
|
2599
2640
|
message: { max: 2e3, type: "string" },
|
|
@@ -2610,9 +2651,14 @@ var drawbridge_default2 = {
|
|
|
2610
2651
|
hook: "email.send",
|
|
2611
2652
|
key: "Email \u2014 Send email",
|
|
2612
2653
|
queue: "notification",
|
|
2654
|
+
returns: [],
|
|
2613
2655
|
settings: {
|
|
2614
2656
|
message: { max: 2e3, required: true, type: "string" },
|
|
2615
|
-
|
|
2657
|
+
// See email.digest above — the last spam check, and the editor's
|
|
2658
|
+
// plain rendering of the message.
|
|
2659
|
+
score: { shape: { message: { type: "string" }, subject: { type: "string" } }, type: "object" },
|
|
2660
|
+
subject: { max: 150, required: true, type: "string" },
|
|
2661
|
+
text: { max: 2e3, type: "string" }
|
|
2616
2662
|
},
|
|
2617
2663
|
triggers: ["lead.insert"],
|
|
2618
2664
|
// ONE SOURCE FOR THE PRICE. lib/pricing.js is the index of every
|
|
@@ -3211,6 +3257,13 @@ var klaviyo_default2 = {
|
|
|
3211
3257
|
// than a label that could be any of their Klaviyo accounts.
|
|
3212
3258
|
key: "Sync contact to " + (((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.account) || "Klaviyo"),
|
|
3213
3259
|
queue: "connection",
|
|
3260
|
+
// The key the hook puts in `context`, which the runner merges into the
|
|
3261
|
+
// run so a later step can interpolate {{klaviyoProfileId}}. Declared
|
|
3262
|
+
// beside the hook that writes it — see the note on Shopify's
|
|
3263
|
+
// commerce.code, which is where this came back from.
|
|
3264
|
+
returns: [
|
|
3265
|
+
{ key: "klaviyoProfileId", label: "Klaviyo Profile ID" }
|
|
3266
|
+
],
|
|
3214
3267
|
// Nothing for a merchant to configure on the step itself — the list
|
|
3215
3268
|
// is chosen once on the connection. Declared empty rather than
|
|
3216
3269
|
// omitted, so "this step takes no settings" and "nobody thought about
|
|
@@ -3326,10 +3379,15 @@ var mailchimp_default2 = {
|
|
|
3326
3379
|
"Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
|
|
3327
3380
|
],
|
|
3328
3381
|
excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
|
|
3382
|
+
// NAMES THE AUDIENCE STEP, because `status` below gates on it: a grant with
|
|
3383
|
+
// no audience chosen sits at Pending, and a guide that stops at the consent
|
|
3384
|
+
// leaves a merchant looking at a connection they think is broken.
|
|
3329
3385
|
guide: [
|
|
3330
3386
|
"Press Connect. Drawbridge sends you to Mailchimp to approve access.",
|
|
3331
3387
|
"Sign in to Mailchimp if you are not already, and choose the account to connect.",
|
|
3332
|
-
"You come back here to pick the audience your contacts should sync into."
|
|
3388
|
+
"You come back here to pick the audience your contacts should sync into.",
|
|
3389
|
+
"The connection shows Pending until you pick an audience, then Active.",
|
|
3390
|
+
"You can remove Drawbridge at any time from the Authorized Apps page in your Mailchimp account."
|
|
3333
3391
|
]
|
|
3334
3392
|
},
|
|
3335
3393
|
// Mailchimp and SendGrid shared a group while they were SENDERS, where an org
|
|
@@ -3571,6 +3629,11 @@ var mailchimp_default2 = {
|
|
|
3571
3629
|
// than showing a string like a1b2c3d4e5.
|
|
3572
3630
|
key: "Sync contact to Mailchimp",
|
|
3573
3631
|
queue: "connection",
|
|
3632
|
+
// The key the hook puts in `context`, for a later step to interpolate
|
|
3633
|
+
// as {{mailchimpMemberId}}. Same reasoning as Klaviyo's.
|
|
3634
|
+
returns: [
|
|
3635
|
+
{ key: "mailchimpMemberId", label: "Mailchimp Member ID" }
|
|
3636
|
+
],
|
|
3574
3637
|
// Nothing for a merchant to configure on the step itself — the audience
|
|
3575
3638
|
// is chosen once on the connection. Declared empty rather than omitted,
|
|
3576
3639
|
// so "this step takes no settings" and "nobody thought about settings"
|
|
@@ -4245,8 +4308,8 @@ var shopify_default2 = {
|
|
|
4245
4308
|
}
|
|
4246
4309
|
}
|
|
4247
4310
|
const billable = (org == null ? void 0 : org.billingProvider) === "shopify" && fee > 0 && !backfill;
|
|
4248
|
-
const
|
|
4249
|
-
const { orderEventHandle } = (
|
|
4311
|
+
const providerRow2 = billable ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
4312
|
+
const { orderEventHandle } = (providerRow2 == null ? void 0 : providerRow2.settings) ? decrypt(providerRow2.settings) : {};
|
|
4250
4313
|
const handle = typeof orderEventHandle === "string" && slugify(orderEventHandle) || ORDER_EVENT_HANDLE;
|
|
4251
4314
|
if (billable && !((_f = connection2 == null ? void 0 : connection2.source) == null ? void 0 : _f.id)) {
|
|
4252
4315
|
(_g = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _g.call(logger2, new Error(
|
|
@@ -4851,6 +4914,24 @@ var shopify_default2 = {
|
|
|
4851
4914
|
hook: "commerce.code",
|
|
4852
4915
|
key: "Issue a discount code",
|
|
4853
4916
|
queue: "connection",
|
|
4917
|
+
// WHAT THIS STEP LEAVES BEHIND FOR THE ONES AFTER IT, and the only
|
|
4918
|
+
// reason the builder can offer a Variables menu.
|
|
4919
|
+
//
|
|
4920
|
+
// These are the exact keys the hook puts in `context` — the runner
|
|
4921
|
+
// merges that into the run's context, and a later step interpolates
|
|
4922
|
+
// {{shopifyDiscountCode}} out of it. Declared beside the hook that
|
|
4923
|
+
// writes them so the two cannot drift; there is nowhere else that
|
|
4924
|
+
// knows both.
|
|
4925
|
+
//
|
|
4926
|
+
// They existed as a hand-written list in the api's workflow catalog
|
|
4927
|
+
// until it was derived from these manifests, and the derivation
|
|
4928
|
+
// hardcoded `returns : []` for every step. Nothing has offered a
|
|
4929
|
+
// variable since — an email step after a discount step had no way to
|
|
4930
|
+
// name the code it was supposed to send.
|
|
4931
|
+
returns: [
|
|
4932
|
+
{ key: "shopifyDiscountCode", label: "Shopify Discount Code" },
|
|
4933
|
+
{ key: "shopifyDiscountId", label: "Shopify Discount ID" }
|
|
4934
|
+
],
|
|
4854
4935
|
settings: {
|
|
4855
4936
|
discount: {
|
|
4856
4937
|
required: true,
|
|
@@ -4869,6 +4950,12 @@ var shopify_default2 = {
|
|
|
4869
4950
|
hook: "commerce.customer",
|
|
4870
4951
|
key: "Create customer",
|
|
4871
4952
|
queue: "connection",
|
|
4953
|
+
// Written on every path the hook can take — created, reused from an
|
|
4954
|
+
// earlier step, or reused from the lead — so a later step can rely on
|
|
4955
|
+
// it being there whenever this one succeeded.
|
|
4956
|
+
returns: [
|
|
4957
|
+
{ key: "shopifyCustomerId", label: "Shopify Customer ID" }
|
|
4958
|
+
],
|
|
4872
4959
|
settings: {},
|
|
4873
4960
|
triggers: ["lead.insert"],
|
|
4874
4961
|
usage: { actions: 1 }
|
|
@@ -5266,6 +5353,13 @@ var webhook_default = {
|
|
|
5266
5353
|
hook: "webhook.send",
|
|
5267
5354
|
key: "Send webhook",
|
|
5268
5355
|
queue: "webhook",
|
|
5356
|
+
// The receiver's response is buffered, not merged into the run — a
|
|
5357
|
+
// receiver owes us a status, not a document — so there is nothing here
|
|
5358
|
+
// for a later step to interpolate. Declared empty rather than omitted:
|
|
5359
|
+
// "leaves nothing behind" and "nobody thought about it" are different
|
|
5360
|
+
// statements, and the second one is what emptied the builder's
|
|
5361
|
+
// Variables menu for every step in the product.
|
|
5362
|
+
returns: [],
|
|
5269
5363
|
settings: {
|
|
5270
5364
|
url: { format: "url", required: true, type: "string" }
|
|
5271
5365
|
},
|
|
@@ -5559,6 +5653,12 @@ var publicConnectionKeys = Object.freeze([
|
|
|
5559
5653
|
// excerpt, guide, and any vendor redirect copy.
|
|
5560
5654
|
"content",
|
|
5561
5655
|
"createdAt",
|
|
5656
|
+
// THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
|
|
5657
|
+
// paused — kept, configured, and not run — as distinct from a vendor the
|
|
5658
|
+
// platform switched off (which the api overlays as an error). Absent means
|
|
5659
|
+
// on; the dashboard renders Paused from it and the sync gates refuse a paused
|
|
5660
|
+
// connection the way they refuse a missing one.
|
|
5661
|
+
"enabled",
|
|
5562
5662
|
// The connection DOCUMENT's own errors array — scope-drift entries written by
|
|
5563
5663
|
// drawbridge-sync. NOT the manifest's error copy, which is content.errors:
|
|
5564
5664
|
// the document is spread OVER the resolved manifest downstream, so the two
|
|
@@ -5599,17 +5699,25 @@ var mask = (value) => {
|
|
|
5599
5699
|
var providerMemo = /* @__PURE__ */ new Map();
|
|
5600
5700
|
var MEMO_TTL_MS = 60 * 1e3;
|
|
5601
5701
|
var clearProviderMemo = () => providerMemo.clear();
|
|
5602
|
-
var
|
|
5702
|
+
var providerRow = async ({ controller, slug: slug2 }) => {
|
|
5603
5703
|
const memoized = providerMemo.get(slug2);
|
|
5604
5704
|
if (memoized && Date.now() - memoized.at < MEMO_TTL_MS) return memoized.value;
|
|
5605
5705
|
const row = await controller.get({
|
|
5606
5706
|
collection: "provider",
|
|
5607
5707
|
query: { slug: slug2 }
|
|
5608
5708
|
});
|
|
5609
|
-
const value =
|
|
5709
|
+
const value = {
|
|
5710
|
+
enabled: (row == null ? void 0 : row.enabled) !== false,
|
|
5711
|
+
settings: (row == null ? void 0 : row.settings) ? decrypt(row.settings) : {}
|
|
5712
|
+
};
|
|
5610
5713
|
providerMemo.set(slug2, { at: Date.now(), value });
|
|
5611
5714
|
return value;
|
|
5612
5715
|
};
|
|
5716
|
+
var providerSettings = async ({ controller, includeDisabled = false, slug: slug2 }) => {
|
|
5717
|
+
const row = await providerRow({ controller, slug: slug2 });
|
|
5718
|
+
return row.enabled || includeDisabled ? row.settings : {};
|
|
5719
|
+
};
|
|
5720
|
+
var vendorEnabled = async ({ controller, vendor }) => (await providerRow({ controller, slug: vendor })).enabled;
|
|
5613
5721
|
var vendorSettings = async ({ controller, slug: slug2 }) => {
|
|
5614
5722
|
var _a;
|
|
5615
5723
|
const declared = Object.hasOwn(connections, slug2) ? Object.keys(((_a = connections[slug2].provider) == null ? void 0 : _a.vendors) || {}) : [];
|
|
@@ -5619,7 +5727,15 @@ var vendorSettings = async ({ controller, slug: slug2 }) => {
|
|
|
5619
5727
|
}
|
|
5620
5728
|
return merged;
|
|
5621
5729
|
};
|
|
5622
|
-
var
|
|
5730
|
+
var vendorsEnabled = async ({ controller, slug: slug2 }) => {
|
|
5731
|
+
var _a;
|
|
5732
|
+
const declared = Object.hasOwn(connections, slug2) ? Object.keys(((_a = connections[slug2].provider) == null ? void 0 : _a.vendors) || {}) : [];
|
|
5733
|
+
for (const vendor of declared) {
|
|
5734
|
+
if (!await vendorEnabled({ controller, vendor })) return false;
|
|
5735
|
+
}
|
|
5736
|
+
return true;
|
|
5737
|
+
};
|
|
5738
|
+
var saveProviderSettings = async ({ authenticated, clear, controller, enabled, settings, slug: slug2 }) => {
|
|
5623
5739
|
const fields2 = providerFields(slug2);
|
|
5624
5740
|
if (!fields2.length) return null;
|
|
5625
5741
|
const existing = await controller.get({
|
|
@@ -5641,6 +5757,7 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
|
|
|
5641
5757
|
collection: "provider",
|
|
5642
5758
|
data: {
|
|
5643
5759
|
$set: {
|
|
5760
|
+
...enabled !== void 0 && { enabled: Boolean(enabled) },
|
|
5644
5761
|
settings: encrypt(merged)
|
|
5645
5762
|
}
|
|
5646
5763
|
},
|
|
@@ -5655,11 +5772,11 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
|
|
|
5655
5772
|
var providerEnvNames = () => new Set(
|
|
5656
5773
|
providerSlugs().flatMap((slug2) => providerFields(slug2)).map((field2) => field2.credential).filter(Boolean)
|
|
5657
5774
|
);
|
|
5658
|
-
var providerCredentials = async ({ controller }) => {
|
|
5775
|
+
var providerCredentials = async ({ controller, includeDisabled = false }) => {
|
|
5659
5776
|
const credentials2 = {};
|
|
5660
5777
|
for (const slug2 of providerSlugs()) {
|
|
5661
5778
|
try {
|
|
5662
|
-
const settings = await providerSettings({ controller, slug: slug2 });
|
|
5779
|
+
const settings = await providerSettings({ controller, includeDisabled, slug: slug2 });
|
|
5663
5780
|
for (const field2 of providerFields(slug2)) {
|
|
5664
5781
|
const value = settings == null ? void 0 : settings[field2.key];
|
|
5665
5782
|
if (field2.credential && value) credentials2[field2.credential] = value;
|
|
@@ -5679,9 +5796,12 @@ var providerCredentials = async ({ controller }) => {
|
|
|
5679
5796
|
providerEnvNames,
|
|
5680
5797
|
providerFields,
|
|
5681
5798
|
providerMemo,
|
|
5799
|
+
providerRow,
|
|
5682
5800
|
providerSettings,
|
|
5683
5801
|
providerSlugs,
|
|
5684
5802
|
saveProviderSettings,
|
|
5803
|
+
vendorEnabled,
|
|
5685
5804
|
vendorSettings,
|
|
5686
|
-
vendors
|
|
5805
|
+
vendors,
|
|
5806
|
+
vendorsEnabled
|
|
5687
5807
|
});
|
package/dist/providers.d.cts
CHANGED
|
@@ -128,8 +128,9 @@ const mask = ( value ) => {
|
|
|
128
128
|
// safe. Per-process state cannot be stale for anyone but the process holding it,
|
|
129
129
|
// and it expires on its own.
|
|
130
130
|
//
|
|
131
|
-
// slug -> { at, value }, where value is
|
|
132
|
-
//
|
|
131
|
+
// slug -> { at, value }, where value is { enabled, settings } — the decrypted
|
|
132
|
+
// settings and whether the row is switched on. Exported only so tests can age an
|
|
133
|
+
// entry without sleeping; nothing else should read it.
|
|
133
134
|
const providerMemo = new Map();
|
|
134
135
|
|
|
135
136
|
// SIXTY SECONDS, chosen for the human rather than for the load. The read is one
|
|
@@ -143,16 +144,35 @@ const MEMO_TTL_MS = 60 * 1000;
|
|
|
143
144
|
// For tests. A save clears its own slug; this clears everything.
|
|
144
145
|
const clearProviderMemo = () => providerMemo.clear();
|
|
145
146
|
|
|
146
|
-
//
|
|
147
|
-
// OAuth callback and every
|
|
147
|
+
// A VENDOR, as { enabled, settings } — the switch and the credentials, from one
|
|
148
|
+
// read. Memoized, because this is on the path of every OAuth callback and every
|
|
149
|
+
// send, so a guard costs a Map lookup rather than a document and a decrypt.
|
|
148
150
|
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
+
// THIS IS THE ROOT CHECK. There is no separate is-it-enabled function, because
|
|
152
|
+
// one question with two ways to ask it is two things to keep agreeing:
|
|
153
|
+
//
|
|
154
|
+
// const { enabled, settings } = await providerRow({ controller, slug : 'sendgrid' });
|
|
155
|
+
// if( ! enabled || ! settings.apiKey ) return;
|
|
156
|
+
//
|
|
157
|
+
// The switch is answered here and the caller names the credentials it needs.
|
|
158
|
+
// Which credentials matter is the FEATURE's question, not the vendor's, and
|
|
159
|
+
// folding the two together answers it wrong in both directions: `isLive` judges
|
|
160
|
+
// the fields a vendor marks `required`, so a send needing SendGrid's optional
|
|
161
|
+
// eventKey would pass a combined check and still fail, while one needing nothing
|
|
162
|
+
// but eventKey would be refused because accountSender happens to be blank. A
|
|
163
|
+
// vendor has one switch and many features.
|
|
164
|
+
//
|
|
165
|
+
// An unconfigured vendor answers enabled with no settings rather than throwing:
|
|
166
|
+
// the admin list has to render the row that lets someone fix it.
|
|
167
|
+
//
|
|
168
|
+
// ABSENT MEANS ON. Every row that predates the switch has no `enabled` field,
|
|
169
|
+
// and reading that as off would take every vendor down at once — so the flag is
|
|
170
|
+
// only ever believed when it says false.
|
|
151
171
|
//
|
|
152
172
|
// The memoized object is handed to every reader inside the window rather than
|
|
153
173
|
// copied per read — no caller mutates its credentials, and defending against one
|
|
154
174
|
// that does not exist would cost a clone on every send.
|
|
155
|
-
const
|
|
175
|
+
const providerRow = async ({ controller, slug }) => {
|
|
156
176
|
|
|
157
177
|
const memoized = providerMemo.get( slug );
|
|
158
178
|
|
|
@@ -163,7 +183,10 @@ const providerSettings = async ({ controller, slug }) => {
|
|
|
163
183
|
query : { slug }
|
|
164
184
|
});
|
|
165
185
|
|
|
166
|
-
const value =
|
|
186
|
+
const value = {
|
|
187
|
+
enabled : row?.enabled !== false,
|
|
188
|
+
settings : row?.settings ? decrypt( row.settings ) : {}
|
|
189
|
+
};
|
|
167
190
|
|
|
168
191
|
providerMemo.set( slug, { at : Date.now(), value });
|
|
169
192
|
|
|
@@ -171,6 +194,39 @@ const providerSettings = async ({ controller, slug }) => {
|
|
|
171
194
|
|
|
172
195
|
};
|
|
173
196
|
|
|
197
|
+
// A vendor's credentials, or nothing at all if the vendor is switched off.
|
|
198
|
+
//
|
|
199
|
+
// SWITCHED OFF READS AS UNCONFIGURED, and that is the whole design. Every
|
|
200
|
+
// consumer already handles a vendor with no credentials — availableConnections
|
|
201
|
+
// drops it from the catalogue, isLive reports not live, a send has nothing to
|
|
202
|
+
// send with — so turning one off needs no second code path anywhere. The keys
|
|
203
|
+
// stay on the row, which is the point: this is the switch you reach for instead
|
|
204
|
+
// of deleting a credential you will have to find again.
|
|
205
|
+
//
|
|
206
|
+
// `includeDisabled` is for the ADMIN SCREEN alone. That page exists to show what
|
|
207
|
+
// is stored and to switch it back on, and it cannot do either if the credentials
|
|
208
|
+
// disappear from its own view the moment they are turned off.
|
|
209
|
+
const providerSettings = async ({ controller, includeDisabled = false, slug }) => {
|
|
210
|
+
|
|
211
|
+
const row = await providerRow({ controller, slug });
|
|
212
|
+
|
|
213
|
+
return ( row.enabled || includeDisabled ) ? row.settings : {};
|
|
214
|
+
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// IS THIS VENDOR SWITCHED ON? The primitive both halves of the gate are built
|
|
218
|
+
// from, named for what it takes: a VENDOR slug (sendgrid, twilio, klaviyo), not
|
|
219
|
+
// a connection slug. The `provider` collection is one row per vendor, so this is
|
|
220
|
+
// that row's switch and nothing else.
|
|
221
|
+
//
|
|
222
|
+
// Its parameter is `vendor` rather than `slug` on purpose. vendorsEnabled below
|
|
223
|
+
// takes a CONNECTION slug and composes this across everything that connection
|
|
224
|
+
// spends, and the two are a letter apart — the argument name is what makes which
|
|
225
|
+
// one you are calling obvious at the call site.
|
|
226
|
+
const vendorEnabled = async ({ controller, vendor }) => (
|
|
227
|
+
( await providerRow({ controller, slug : vendor }) ).enabled
|
|
228
|
+
);
|
|
229
|
+
|
|
174
230
|
// EVERYTHING A CONNECTION SPENDS, merged across the vendors its manifest
|
|
175
231
|
// declares. `drawbridge` reads SendGrid, Twilio and HubSpot as one object, the
|
|
176
232
|
// way it did when they sat on one row — the row split, the readers did not.
|
|
@@ -194,6 +250,30 @@ const vendorSettings = async ({ controller, slug }) => {
|
|
|
194
250
|
|
|
195
251
|
};
|
|
196
252
|
|
|
253
|
+
// EVERY VENDOR A CONNECTION SPENDS, SWITCHED ON.
|
|
254
|
+
//
|
|
255
|
+
// The connection-level answer to the provider-level question, and the two are
|
|
256
|
+
// not one-to-one in either direction: `drawbridge` spends SendGrid, Twilio and
|
|
257
|
+
// HubSpot, so switching any one of them off makes that connection unusable,
|
|
258
|
+
// while `webhook` spends no vendor at all and can never be switched off.
|
|
259
|
+
//
|
|
260
|
+
// Same walk as vendorSettings, which is deliberate — a connection that reads
|
|
261
|
+
// three rows as one settings object has to answer the switch across the same
|
|
262
|
+
// three, or the two would disagree about which vendors a connection depends on.
|
|
263
|
+
const vendorsEnabled = async ({ controller, slug }) => {
|
|
264
|
+
|
|
265
|
+
const declared = Object.hasOwn( connections, slug ) ? Object.keys( connections[ slug ].provider?.vendors || {} ) : [];
|
|
266
|
+
|
|
267
|
+
for( const vendor of declared ){
|
|
268
|
+
|
|
269
|
+
if( ! await vendorEnabled({ controller, vendor }) ) return false;
|
|
270
|
+
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return true;
|
|
274
|
+
|
|
275
|
+
};
|
|
276
|
+
|
|
197
277
|
// Write a vendor's credentials.
|
|
198
278
|
//
|
|
199
279
|
// ONLY THE DECLARED FIELDS. The body arrives over the wire, so anything in it
|
|
@@ -203,7 +283,11 @@ const vendorSettings = async ({ controller, slug }) => {
|
|
|
203
283
|
// BLANK MEANS KEEP. A redacted field is never returned by the GET, so an
|
|
204
284
|
// unchanged form posts it back empty; treating that as a clear would wipe a
|
|
205
285
|
// credential every time someone edited the field next to it.
|
|
206
|
-
|
|
286
|
+
// SWITCHING OFF IS A SAVE, not its own route. The toggle sits in the same drawer
|
|
287
|
+
// as the credentials and travels with them, so an admin who turns a vendor off
|
|
288
|
+
// and corrects a key does it in one gesture. Omitted leaves the row's current
|
|
289
|
+
// answer alone, the same way a blank credential does.
|
|
290
|
+
const saveProviderSettings = async ({ authenticated, clear, controller, enabled, settings, slug }) => {
|
|
207
291
|
|
|
208
292
|
const fields = providerFields( slug );
|
|
209
293
|
|
|
@@ -263,6 +347,7 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
|
|
|
263
347
|
collection : 'provider',
|
|
264
348
|
data : {
|
|
265
349
|
$set : {
|
|
350
|
+
...( enabled !== undefined && { enabled : Boolean( enabled ) }),
|
|
266
351
|
settings : encrypt( merged )
|
|
267
352
|
}
|
|
268
353
|
},
|
|
@@ -309,7 +394,14 @@ const providerEnvNames = () => new Set(
|
|
|
309
394
|
//
|
|
310
395
|
// A field with no value is OMITTED rather than set empty, so `requires` sees
|
|
311
396
|
// the same absence it would for an unset variable.
|
|
312
|
-
|
|
397
|
+
//
|
|
398
|
+
// `includeDisabled` is for VERIFYING, not spending. Switching a vendor off means
|
|
399
|
+
// we stop sending through it; it does not mean a message signed with its secret
|
|
400
|
+
// stopped being genuine. Delivery events for mail already sent still arrive, and
|
|
401
|
+
// a STOP reply is an obligation whether or not we are currently sending — so
|
|
402
|
+
// the inbound routes read the secret with the switch ignored, and everything
|
|
403
|
+
// that decides what a merchant can connect or a step can run reads it honoured.
|
|
404
|
+
const providerCredentials = async ({ controller, includeDisabled = false }) => {
|
|
313
405
|
|
|
314
406
|
const credentials = {};
|
|
315
407
|
|
|
@@ -326,7 +418,7 @@ const providerCredentials = async ({ controller }) => {
|
|
|
326
418
|
// which is exactly where someone goes to fix it.
|
|
327
419
|
try {
|
|
328
420
|
|
|
329
|
-
const settings = await providerSettings({ controller, slug });
|
|
421
|
+
const settings = await providerSettings({ controller, includeDisabled, slug });
|
|
330
422
|
|
|
331
423
|
for( const field of providerFields( slug ) ){
|
|
332
424
|
|
|
@@ -348,4 +440,4 @@ const providerCredentials = async ({ controller }) => {
|
|
|
348
440
|
|
|
349
441
|
};
|
|
350
442
|
|
|
351
|
-
export { clearProviderMemo, isLive, mask, providerCredentials, providerEnvNames, providerFields, providerMemo, providerSettings, providerSlugs, saveProviderSettings, vendorSettings, vendors };
|
|
443
|
+
export { clearProviderMemo, isLive, mask, providerCredentials, providerEnvNames, providerFields, providerMemo, providerRow, providerSettings, providerSlugs, saveProviderSettings, vendorEnabled, vendorSettings, vendors, vendorsEnabled };
|