@drawbridge/drawbridge-utils 0.0.125 → 0.0.127

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.
@@ -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",
@@ -815,6 +815,9 @@ var attentive_default2 = {
815
815
  title: "Attentive"
816
816
  };
817
817
 
818
+ // lib/connections/providers/drawbridge.js
819
+ var import_node_crypto2 = require("crypto");
820
+
818
821
  // lib/http.js
819
822
  var DEFAULT_TIMEOUT_MS = 15e3;
820
823
  var request = async ({
@@ -1007,14 +1010,14 @@ var contacts = {
1007
1010
 
1008
1011
  // lib/connections/icons/drawbridge.js
1009
1012
  var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1010
- <rect width="500" height="500" fill="#BAEC5F"/>
1013
+ <rect width="500" height="500" fill="#0D1314"/>
1011
1014
  <g clip-path="url(#clip0_2115_2832)">
1012
- <path d="M140.224 127.586L174.803 188.73V311.176L140 372.32L176.084 392.031L216.111 321.753V178.278L176.341 108L140.224 127.586Z" fill="#0D1314"/>
1013
- <path d="M360.002 127.523L323.694 108.282L284.949 178.498V321.596L322.924 391.749L359.394 372.79L326.225 311.52V188.73L360.002 127.523Z" fill="#0D1314"/>
1015
+ <path d="M153.198 142.241L183.69 196.065V303.852L153 357.676L184.82 375.028L220.116 313.163V186.865L185.046 125L153.198 142.241Z" fill="#BAEC5F"/>
1016
+ <path d="M347.002 142.186L314.984 125.249L280.818 187.058V313.025L314.306 374.779L346.465 358.091L317.216 304.156V196.066L347.002 142.186Z" fill="#BAEC5F"/>
1014
1017
  </g>
1015
1018
  <defs>
1016
1019
  <clipPath id="clip0_2115_2832">
1017
- <rect width="220" height="284" fill="white" transform="translate(140 108)"/>
1020
+ <rect width="194" height="250" fill="white" transform="translate(153 125)"/>
1018
1021
  </clipPath>
1019
1022
  </defs>
1020
1023
  </svg>`;
@@ -1641,6 +1644,8 @@ var channels = {
1641
1644
  };
1642
1645
 
1643
1646
  // lib/connections/providers/drawbridge.js
1647
+ var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
1648
+ var START_KEYWORDS = ["START", "UNSTOP", "YES"];
1644
1649
  var interpolate = (template, data2) => {
1645
1650
  if (!template) return template;
1646
1651
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (data2 == null ? void 0 : data2[key]) != null ? String(data2[key]) : "{{" + key + "}}");
@@ -1868,7 +1873,88 @@ var drawbridge_default2 = {
1868
1873
  };
1869
1874
  }
1870
1875
  },
1871
- inbound: false,
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
+ },
1872
1958
  lifecycle: false,
1873
1959
  resources: {
1874
1960
  audiences: false,
@@ -2047,6 +2133,18 @@ var drawbridge_default2 = {
2047
2133
  webhook: false
2048
2134
  },
2049
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
+ },
2050
2148
  // PRIVATE: never in the catalog, always available to the builder.
2051
2149
  private: true,
2052
2150
  // THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
@@ -2679,7 +2777,7 @@ var klaviyo_default2 = {
2679
2777
  };
2680
2778
 
2681
2779
  // lib/connections/providers/mailchimp.js
2682
- var import_node_crypto2 = require("crypto");
2780
+ var import_node_crypto3 = require("crypto");
2683
2781
 
2684
2782
  // lib/connections/icons/mailchimp.js
2685
2783
  var mailchimp_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -2711,7 +2809,7 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
2711
2809
  }
2712
2810
  return response.json();
2713
2811
  };
2714
- var subscriberHash = (email) => (0, import_node_crypto2.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
2812
+ var subscriberHash = (email) => (0, import_node_crypto3.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
2715
2813
  var mailchimp_default2 = {
2716
2814
  // OAUTH 2, authorization code. Every url below is quoted from
2717
2815
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -2998,7 +3096,7 @@ var mailchimp_default2 = {
2998
3096
  };
2999
3097
 
3000
3098
  // lib/connections/providers/shopify.js
3001
- var import_node_crypto4 = require("crypto");
3099
+ var import_node_crypto5 = require("crypto");
3002
3100
  var import_nanoid2 = require("nanoid");
3003
3101
 
3004
3102
  // lib/connections/icons/shopify.js
@@ -3009,7 +3107,7 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
3009
3107
  </svg>`;
3010
3108
 
3011
3109
  // lib/connections/inbound.js
3012
- var import_node_crypto3 = require("crypto");
3110
+ var import_node_crypto4 = require("crypto");
3013
3111
  var verifySignature = ({ body, descriptor, headers, secret }) => {
3014
3112
  if (!secret) {
3015
3113
  throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
@@ -3018,10 +3116,10 @@ var verifySignature = ({ body, descriptor, headers, secret }) => {
3018
3116
  if (!provided) {
3019
3117
  throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
3020
3118
  }
3021
- const digest = (0, import_node_crypto3.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
3119
+ const digest = (0, import_node_crypto4.createHmac)(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
3022
3120
  const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
3023
3121
  const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
3024
- if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto3.timingSafeEqual)(digestBuffer, providedBuffer)) {
3122
+ if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto4.timingSafeEqual)(digestBuffer, providedBuffer)) {
3025
3123
  throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
3026
3124
  }
3027
3125
  return JSON.parse(body.toString());
@@ -3760,7 +3858,7 @@ var shopify_default2 = {
3760
3858
  event: "shopify.register.webhooks"
3761
3859
  },
3762
3860
  name: "register",
3763
- options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto4.randomUUID)() },
3861
+ options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto5.randomUUID)() },
3764
3862
  queue: "connection"
3765
3863
  }],
3766
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.",
@@ -4047,7 +4145,7 @@ var shopify_default2 = {
4047
4145
  };
4048
4146
 
4049
4147
  // lib/connections/providers/webhook.js
4050
- var import_node_crypto5 = __toESM(require("crypto"), 1);
4148
+ var import_node_crypto6 = __toESM(require("crypto"), 1);
4051
4149
 
4052
4150
  // lib/safe-http.js
4053
4151
  var import_dns2 = __toESM(require("dns"), 1);
@@ -4279,7 +4377,7 @@ var webhook_default = {
4279
4377
  request2.body = body;
4280
4378
  const outgoing = { ...headers };
4281
4379
  if (settings == null ? void 0 : settings.secret) {
4282
- outgoing["X-Drawbridge-Signature"] = "sha256=" + import_node_crypto5.default.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4380
+ outgoing["X-Drawbridge-Signature"] = "sha256=" + import_node_crypto6.default.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4283
4381
  }
4284
4382
  const response = await send2({ body, headers: outgoing, method, url });
4285
4383
  return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
@@ -4353,7 +4451,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
4353
4451
  ([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
4354
4452
  );
4355
4453
  var build = (manifest) => {
4356
- 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;
4357
4455
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
4358
4456
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
4359
4457
  if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
@@ -4432,16 +4530,16 @@ var build = (manifest) => {
4432
4530
  );
4433
4531
  }
4434
4532
  }
4435
- if (implemented(manifest.hooks, "inbound.event") && !((_l = (_k = manifest.inbound) == null ? void 0 : _k.headers) == null ? void 0 : _l.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)) {
4436
4534
  throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
4437
4535
  }
4438
- if (implemented(manifest.hooks, "inbound.verify") && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.signature)) {
4536
+ if (implemented(manifest.hooks, "inbound.verify") && !((_p = (_o = manifest.inbound) == null ? void 0 : _o.headers) == null ? void 0 : _p.signature)) {
4439
4537
  throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
4440
4538
  }
4441
4539
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
4442
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");
4443
4541
  }
4444
- if (!Array.isArray((_o = manifest == null ? void 0 : manifest.content) == null ? void 0 : _o.guide) || !manifest.content.guide.length) {
4542
+ if (!Array.isArray((_q = manifest == null ? void 0 : manifest.content) == null ? void 0 : _q.guide) || !manifest.content.guide.length) {
4445
4543
  throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
4446
4544
  }
4447
4545
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
@@ -4453,8 +4551,8 @@ var build = (manifest) => {
4453
4551
  }
4454
4552
  for (const [domain, verbs] of Object.entries(HOOKS)) {
4455
4553
  for (const verb of verbs) {
4456
- const hook = (_q = (_p = manifest.hooks) == null ? void 0 : _p[domain]) == null ? void 0 : _q[verb];
4457
- if (((_r = manifest.hooks) == null ? void 0 : _r[domain]) === false) continue;
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;
4458
4556
  if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
4459
4557
  throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
4460
4558
  }
@@ -4642,9 +4740,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
4642
4740
  query: { slug }
4643
4741
  });
4644
4742
  const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
4743
+ const pasted = (value) => typeof value === "string" ? value.trim().replace(/^['"]+|['"]+$/g, "") : value;
4645
4744
  const merged = mergeSettings({
4646
4745
  existing: stored,
4647
- incoming: Object.fromEntries(fields2.map((field2) => [field2.key, settings == null ? void 0 : settings[field2.key]]))
4746
+ incoming: Object.fromEntries(fields2.map((field2) => [field2.key, pasted(settings == null ? void 0 : settings[field2.key])]))
4648
4747
  });
4649
4748
  for (const key of Array.isArray(clear) ? clear : []) {
4650
4749
  if (fields2.some((field2) => field2.key === key)) delete merged[key];
@@ -191,6 +191,15 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
191
191
 
192
192
  const stored = existing?.settings ? decrypt( existing.settings ) : {};
193
193
 
194
+ // A CREDENTIAL ARRIVES BY PASTE, and a paste from a shell line or a password
195
+ // manager brings wrapping quotes and whitespace with it. One stray trailing
196
+ // `'` on a SendGrid key turned every sign-in email into a silent 401 — no
197
+ // legitimate credential begins or ends with a quote or a space, so stripping
198
+ // them here loses nothing and ends the failure mode for every field at once.
199
+ const pasted = ( value ) => typeof value === 'string'
200
+ ? value.trim().replace( /^['"]+|['"]+$/g, '' )
201
+ : value;
202
+
194
203
  // mergeSettings is the blank-keeps rule, already used for merchant
195
204
  // connection settings in route/organization-connection.js — same behaviour,
196
205
  // one implementation. The declared-field allowlist is the part that is ours:
@@ -198,7 +207,7 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
198
207
  // not declare reaching the document.
199
208
  const merged = mergeSettings({
200
209
  existing : stored,
201
- incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, settings?.[ field.key ] ] ) )
210
+ incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, pasted( settings?.[ field.key ] ) ] ) )
202
211
  });
203
212
 
204
213
  // CLEARING IS EXPLICIT, because blank already means keep and the two cannot
@@ -191,6 +191,15 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
191
191
 
192
192
  const stored = existing?.settings ? decrypt( existing.settings ) : {};
193
193
 
194
+ // A CREDENTIAL ARRIVES BY PASTE, and a paste from a shell line or a password
195
+ // manager brings wrapping quotes and whitespace with it. One stray trailing
196
+ // `'` on a SendGrid key turned every sign-in email into a silent 401 — no
197
+ // legitimate credential begins or ends with a quote or a space, so stripping
198
+ // them here loses nothing and ends the failure mode for every field at once.
199
+ const pasted = ( value ) => typeof value === 'string'
200
+ ? value.trim().replace( /^['"]+|['"]+$/g, '' )
201
+ : value;
202
+
194
203
  // mergeSettings is the blank-keeps rule, already used for merchant
195
204
  // connection settings in route/organization-connection.js — same behaviour,
196
205
  // one implementation. The declared-field allowlist is the part that is ours:
@@ -198,7 +207,7 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
198
207
  // not declare reaching the document.
199
208
  const merged = mergeSettings({
200
209
  existing : stored,
201
- incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, settings?.[ field.key ] ] ) )
210
+ incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, pasted( settings?.[ field.key ] ) ] ) )
202
211
  });
203
212
 
204
213
  // CLEARING IS EXPLICIT, because blank already means keep and the two cannot
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",
@@ -771,6 +771,9 @@ var attentive_default2 = {
771
771
  title: "Attentive"
772
772
  };
773
773
 
774
+ // lib/connections/providers/drawbridge.js
775
+ import { createHmac, timingSafeEqual } from "crypto";
776
+
774
777
  // lib/http.js
775
778
  var DEFAULT_TIMEOUT_MS = 15e3;
776
779
  var request = async ({
@@ -963,14 +966,14 @@ var contacts = {
963
966
 
964
967
  // lib/connections/icons/drawbridge.js
965
968
  var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
966
- <rect width="500" height="500" fill="#BAEC5F"/>
969
+ <rect width="500" height="500" fill="#0D1314"/>
967
970
  <g clip-path="url(#clip0_2115_2832)">
968
- <path d="M140.224 127.586L174.803 188.73V311.176L140 372.32L176.084 392.031L216.111 321.753V178.278L176.341 108L140.224 127.586Z" fill="#0D1314"/>
969
- <path d="M360.002 127.523L323.694 108.282L284.949 178.498V321.596L322.924 391.749L359.394 372.79L326.225 311.52V188.73L360.002 127.523Z" fill="#0D1314"/>
971
+ <path d="M153.198 142.241L183.69 196.065V303.852L153 357.676L184.82 375.028L220.116 313.163V186.865L185.046 125L153.198 142.241Z" fill="#BAEC5F"/>
972
+ <path d="M347.002 142.186L314.984 125.249L280.818 187.058V313.025L314.306 374.779L346.465 358.091L317.216 304.156V196.066L347.002 142.186Z" fill="#BAEC5F"/>
970
973
  </g>
971
974
  <defs>
972
975
  <clipPath id="clip0_2115_2832">
973
- <rect width="220" height="284" fill="white" transform="translate(140 108)"/>
976
+ <rect width="194" height="250" fill="white" transform="translate(153 125)"/>
974
977
  </clipPath>
975
978
  </defs>
976
979
  </svg>`;
@@ -1597,6 +1600,8 @@ var channels = {
1597
1600
  };
1598
1601
 
1599
1602
  // lib/connections/providers/drawbridge.js
1603
+ var STOP_KEYWORDS = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
1604
+ var START_KEYWORDS = ["START", "UNSTOP", "YES"];
1600
1605
  var interpolate = (template, data2) => {
1601
1606
  if (!template) return template;
1602
1607
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => (data2 == null ? void 0 : data2[key]) != null ? String(data2[key]) : "{{" + key + "}}");
@@ -1824,7 +1829,88 @@ var drawbridge_default2 = {
1824
1829
  };
1825
1830
  }
1826
1831
  },
1827
- inbound: false,
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
+ },
1828
1914
  lifecycle: false,
1829
1915
  resources: {
1830
1916
  audiences: false,
@@ -2003,6 +2089,18 @@ var drawbridge_default2 = {
2003
2089
  webhook: false
2004
2090
  },
2005
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
+ },
2006
2104
  // PRIVATE: never in the catalog, always available to the builder.
2007
2105
  private: true,
2008
2106
  // THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
@@ -2965,7 +3063,7 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
2965
3063
  </svg>`;
2966
3064
 
2967
3065
  // lib/connections/inbound.js
2968
- import { createHmac, timingSafeEqual } from "crypto";
3066
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
2969
3067
  var verifySignature = ({ body, descriptor, headers, secret }) => {
2970
3068
  if (!secret) {
2971
3069
  throw Object.assign(new Error("Missing webhook secret: " + descriptor.signature.secret), { status: 500 });
@@ -2974,10 +3072,10 @@ var verifySignature = ({ body, descriptor, headers, secret }) => {
2974
3072
  if (!provided) {
2975
3073
  throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
2976
3074
  }
2977
- const digest = createHmac(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
3075
+ const digest = createHmac2(descriptor.signature.algorithm, secret).update(body).digest(descriptor.signature.encoding);
2978
3076
  const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
2979
3077
  const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
2980
- if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual(digestBuffer, providedBuffer)) {
3078
+ if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual2(digestBuffer, providedBuffer)) {
2981
3079
  throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
2982
3080
  }
2983
3081
  return JSON.parse(body.toString());
@@ -4309,7 +4407,7 @@ var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
4309
4407
  ([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
4310
4408
  );
4311
4409
  var build = (manifest) => {
4312
- 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;
4313
4411
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
4314
4412
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
4315
4413
  if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
@@ -4388,16 +4486,16 @@ var build = (manifest) => {
4388
4486
  );
4389
4487
  }
4390
4488
  }
4391
- if (implemented(manifest.hooks, "inbound.event") && !((_l = (_k = manifest.inbound) == null ? void 0 : _k.headers) == null ? void 0 : _l.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)) {
4392
4490
  throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
4393
4491
  }
4394
- if (implemented(manifest.hooks, "inbound.verify") && !((_n = (_m = manifest.inbound) == null ? void 0 : _m.headers) == null ? void 0 : _n.signature)) {
4492
+ if (implemented(manifest.hooks, "inbound.verify") && !((_p = (_o = manifest.inbound) == null ? void 0 : _o.headers) == null ? void 0 : _p.signature)) {
4395
4493
  throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
4396
4494
  }
4397
4495
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
4398
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");
4399
4497
  }
4400
- if (!Array.isArray((_o = manifest == null ? void 0 : manifest.content) == null ? void 0 : _o.guide) || !manifest.content.guide.length) {
4498
+ if (!Array.isArray((_q = manifest == null ? void 0 : manifest.content) == null ? void 0 : _q.guide) || !manifest.content.guide.length) {
4401
4499
  throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
4402
4500
  }
4403
4501
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
@@ -4409,8 +4507,8 @@ var build = (manifest) => {
4409
4507
  }
4410
4508
  for (const [domain, verbs] of Object.entries(HOOKS)) {
4411
4509
  for (const verb of verbs) {
4412
- const hook = (_q = (_p = manifest.hooks) == null ? void 0 : _p[domain]) == null ? void 0 : _q[verb];
4413
- if (((_r = manifest.hooks) == null ? void 0 : _r[domain]) === false) continue;
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;
4414
4512
  if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
4415
4513
  throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
4416
4514
  }
@@ -4598,9 +4696,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
4598
4696
  query: { slug }
4599
4697
  });
4600
4698
  const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
4699
+ const pasted = (value) => typeof value === "string" ? value.trim().replace(/^['"]+|['"]+$/g, "") : value;
4601
4700
  const merged = mergeSettings({
4602
4701
  existing: stored,
4603
- incoming: Object.fromEntries(fields2.map((field2) => [field2.key, settings == null ? void 0 : settings[field2.key]]))
4702
+ incoming: Object.fromEntries(fields2.map((field2) => [field2.key, pasted(settings == null ? void 0 : settings[field2.key])]))
4604
4703
  });
4605
4704
  for (const key of Array.isArray(clear) ? clear : []) {
4606
4705
  if (fields2.some((field2) => field2.key === key)) delete merged[key];
package/package.json CHANGED
@@ -215,5 +215,5 @@
215
215
  "test": ". \"$HOME/.nvm/nvm.sh\" && nvm use && node --test"
216
216
  },
217
217
  "types": "dist/index.d.ts",
218
- "version": "0.0.125"
218
+ "version": "0.0.127"
219
219
  }