@drawbridge/drawbridge-utils 0.0.174 → 0.0.175

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.
@@ -810,7 +810,7 @@ const accessToken = async ({
810
810
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
811
811
  // lib/ directly. A bare .svg import would need a bundler loader and force the
812
812
  // tests through dist.
813
- var icon$4 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
813
+ var icon$7 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
814
814
  <rect width="500" height="500" fill="#FFD967"/>
815
815
  <path d="M239.369 127.404C260.863 122.013 284.964 132.569 295.576 152.039C321.796 201.286 347.928 250.567 374.131 299.825C380.149 311.989 381.244 326.564 376.733 339.41C371.446 355.097 358.039 367.69 342.107 372.089C326.274 376.67 308.351 372.871 295.741 362.255C288.865 356.892 284.161 349.359 280.354 341.629C261.869 306.95 243.478 272.217 225.071 237.501C214.409 218.614 187.221 212.656 169.622 225.315C168.104 227.309 164.159 226.857 164.72 223.796C176.911 202.569 189.361 181.497 201.662 160.331C205.183 154.428 208.214 148.129 212.967 143.091C219.844 135.272 229.263 129.782 239.374 127.404" fill="#1E1C1C"/>
816
816
  <path d="M166.04 261.805C180.228 259.107 195.528 261.893 207.581 269.971C218.512 277.079 226.908 288.136 230.604 300.657C234.835 314.103 233.614 329.124 227.485 341.788C220.097 356.875 205.782 368.543 189.317 372.089C173.957 375.652 157.089 372.386 144.304 363.103C132.971 355.295 124.912 342.989 121.891 329.581C118.943 316.636 120.725 302.656 127.002 290.938C134.699 275.951 149.503 264.933 166.046 261.811" fill="#1E1C1C"/>
@@ -1469,27 +1469,7 @@ var attentive = {
1469
1469
  webhook : false
1470
1470
 
1471
1471
  },
1472
- icon: icon$4,
1473
- // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
1474
- // what an admin types on the provider screen, and the only declaration of it.
1475
- // It lives beside `requires`, which names the same variables: the manifest
1476
- // says what it needs and this says how someone supplies it, so a credential
1477
- // cannot be required by a vendor that offers nowhere to enter it.
1478
- //
1479
- // `redact` marks a secret — never returned by the api, and blank on save means
1480
- // keep the stored value. `required` drives the live check.
1481
- provider : {
1482
- vendors : {
1483
- attentive : {
1484
- fields : [
1485
- { input : 'text', key : 'clientId', credential : 'ATTENTIVE_OAUTH_CLIENT_ID', label : 'Client ID', message : 'From our app\'s registration in the Attentive developer portal.', required : true },
1486
- { input : 'password', key : 'clientSecret', credential : 'ATTENTIVE_OAUTH_CLIENT_SECRET', label : 'Client secret', message : 'Beside the Client ID in the Attentive developer portal.', redact : true, required : true }
1487
- ],
1488
- icon: icon$4,
1489
- name : 'Attentive'
1490
- }
1491
- }
1492
- },
1472
+ icon: icon$7,
1493
1473
  requires : [
1494
1474
  'ATTENTIVE_OAUTH_CLIENT_ID',
1495
1475
  'ATTENTIVE_OAUTH_CLIENT_SECRET'
@@ -1593,6 +1573,11 @@ var attentive = {
1593
1573
  }
1594
1574
  ]
1595
1575
  ),
1576
+ // THE VENDORS THIS CONNECTION SPENDS, by slug. Each is declared once in
1577
+ // lib/connections/vendors/, and named here by whoever uses it — the block that
1578
+ // used to sit here declared them inline, so two manifests naming one vendor had
1579
+ // to be reconciled at import by mergeVendors().
1580
+ vendors : [ 'attentive' ],
1596
1581
  title : 'Attentive',
1597
1582
 
1598
1583
  // ATTENTIVE'S SEGMENTS INDEX. There is no documented per-segment url, and the
@@ -2069,7 +2054,7 @@ const contacts = {
2069
2054
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
2070
2055
  // lib/ directly. A bare .svg import would need a bundler loader and force the
2071
2056
  // tests onto dist/, which is a worse trade than one line of wrapper.
2072
- var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2057
+ var icon$6 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2073
2058
  <rect width="500" height="500" fill="#0D1314"/>
2074
2059
  <g clip-path="url(#clip0_2115_2832)">
2075
2060
  <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"/>
@@ -2090,7 +2075,7 @@ var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
2090
2075
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
2091
2076
  // lib/ directly. A bare .svg import would need a bundler loader and force the
2092
2077
  // tests onto dist/, which is a worse trade than one line of wrapper.
2093
- var hubspotIcon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2078
+ var icon$5 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2094
2079
  <rect width="500" height="500" fill="#FF7A59"/>
2095
2080
  <circle cx="290" cy="290" r="78" stroke="white" stroke-width="30"/>
2096
2081
  <circle cx="290" cy="290" r="18" fill="white"/>
@@ -2108,7 +2093,7 @@ var hubspotIcon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="non
2108
2093
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
2109
2094
  // lib/ directly. A bare .svg import would need a bundler loader and force the
2110
2095
  // tests onto dist/, which is a worse trade than one line of wrapper.
2111
- var sendgridIcon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2096
+ var icon$4 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2112
2097
  <rect width="500" height="500" fill="#F1FAFF"/>
2113
2098
  <g clip-path="url(#clip0_1011_2743)">
2114
2099
  <path d="M366.676 134.534V289.172H289.203V366.487H134.258L134.258 289.171L134.256 289.172V211.851H211.729V134.534H366.676Z" fill="#9DD6E3"/>
@@ -2131,7 +2116,7 @@ var sendgridIcon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="no
2131
2116
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
2132
2117
  // lib/ directly. A bare .svg import would need a bundler loader and force the
2133
2118
  // tests onto dist/, which is a worse trade than one line of wrapper.
2134
- var twilioIcon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2119
+ var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2135
2120
  <rect width="500" height="500" fill="#F22F46"/>
2136
2121
  <path d="M250 134.954C186.341 134.954 134.954 186.341 134.954 250C134.954 313.659 186.341 365.047 250 365.047C313.659 365.047 365.047 313.659 365.047 250C365.047 186.341 313.659 134.954 250 134.954ZM250 334.368C203.215 334.368 165.633 296.786 165.633 250C165.633 203.215 203.215 165.633 250 165.633C296.786 165.633 334.368 203.215 334.368 250C334.368 296.786 296.786 334.368 250 334.368ZM302.155 221.622C302.155 234.661 291.417 245.399 278.378 245.399C265.34 245.399 254.602 234.661 254.602 221.622C254.602 208.584 265.34 197.846 278.378 197.846C291.417 197.846 302.155 208.584 302.155 221.622ZM302.155 278.378C302.155 291.417 291.417 302.155 278.378 302.155C265.34 302.155 254.602 291.417 254.602 278.378C254.602 265.34 265.34 254.602 278.378 254.602C291.417 254.602 302.155 265.34 302.155 278.378ZM245.399 278.378C245.399 291.417 234.661 302.155 221.622 302.155C208.584 302.155 197.846 291.417 197.846 278.378C197.846 265.34 208.584 254.602 221.622 254.602C234.661 254.602 245.399 265.34 245.399 278.378ZM245.399 221.622C245.399 234.661 234.661 245.399 221.622 245.399C208.584 245.399 197.846 234.661 197.846 221.622C197.846 208.584 208.584 197.846 221.622 197.846C234.661 197.846 245.399 208.584 245.399 221.622Z" fill="white"/>
2137
2122
  </svg>`;
@@ -3183,7 +3168,7 @@ var drawbridge = {
3183
3168
  },
3184
3169
  webhook : false
3185
3170
  },
3186
- icon: icon$3,
3171
+ icon: icon$6,
3187
3172
 
3188
3173
  // WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
3189
3174
  // verifies each channel. The route resolves the NAME to the stored value and
@@ -3214,70 +3199,6 @@ var drawbridge = {
3214
3199
  },
3215
3200
  // PRIVATE: never in the catalog, always available to the builder.
3216
3201
  private : true,
3217
- // THE PLATFORM'S OWN SENDING CREDENTIALS — SendGrid, Twilio, and the internal
3218
- // HubSpot portal. No merchant ever sees these; they are what an admin types on
3219
- // the provider screen so that Drawbridge itself can send.
3220
- //
3221
- // They belong on THIS manifest because this is the connection that sends: the
3222
- // email, sms and segment hooks below are the only things that spend them, and
3223
- // a private connection is still where a vendor fact lives. They are THREE
3224
- // VENDORS, not one, and each is its own row and its own admin card — so
3225
- // SendGrid reads live on a deployment that never configured Twilio, instead
3226
- // of one card for all three that could only ever be as live as its weakest.
3227
- //
3228
- // UNLIKE every public vendor, none of these appear in `requires` — see the
3229
- // comment there. Availability and configuration are different questions, and a
3230
- // missing CRM token must not take every base workflow step away.
3231
- //
3232
- // ponytail: the field keys keep the names they had on the shared row
3233
- // (smsSid, hubspotToken) so the readers destructuring vendorSettings() did
3234
- // not move. Rename them to sid/token when those readers are next touched.
3235
- provider : {
3236
- vendors : {
3237
- sendgrid : {
3238
- fields : [
3239
- { input : 'email', key : 'accountSender', credential : 'SENDGRID_FROM_ADDRESS', label : 'Account sender', message : 'Verification codes and security alerts send from here.', required : true },
3240
- { input : 'password', key : 'apiKey', credential : 'SENDGRID_API_KEY', label : 'SendGrid API key', message : 'SendGrid dashboard, Settings, API Keys — full access for sends.', redact : true, required : true },
3241
- // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
3242
- // either. Unset, it degrades to the account sender rather than
3243
- // refusing to start.
3244
- // NOT redacted, because it is a PUBLIC key. Marking it secret would
3245
- // be theatre, and it would stop an operator reading back the value
3246
- // they need to compare against the SendGrid console.
3247
- //
3248
- // Optional: absent, the event webhook answers 500 on verify rather
3249
- // than accepting unverified deliveries, and nothing else notices.
3250
- { input : 'text', key : 'eventKey', credential : 'SENDGRID_EVENT_WEBHOOK_KEY', label : 'Event webhook key', message : 'SendGrid, Settings, Mail Settings, Event Webhook — the verification key shown once signature verification is enabled. Public, not a secret.' },
3251
- { input : 'email', key : 'leadSender', credential : 'SENDGRID_SEND_FROM_ADDRESS', label : 'Lead sender', message : 'The default for lead-facing mail when a merchant has not verified their own domain.' }
3252
- ],
3253
- icon : sendgridIcon,
3254
- name : 'SendGrid'
3255
- },
3256
- // Missing, SMS is LOCKED DOWN rather than broken: every send path
3257
- // refuses cleanly (queue/notification.js cancels, the otc stream skips
3258
- // and logs), the number-purchase routes deny, and identityDefaults.sms
3259
- // hides the org's SMS settings entirely. The card reads not live, which
3260
- // is the truth about Twilio and says nothing about email.
3261
- twilio : {
3262
- fields : [
3263
- { input : 'text', key : 'smsFrom', credential : 'TWILIO_ACCOUNT_FROM', label : 'SMS number', message : 'Without Twilio credentials, SMS is disabled across the platform.', required : true },
3264
- { input : 'password', key : 'smsSid', credential : 'TWILIO_ACCOUNT_SID', label : 'Twilio account SID', message : 'Twilio console home — the AC… identifier on the account dashboard.', redact : true, required : true },
3265
- { input : 'password', key : 'smsToken', credential : 'TWILIO_AUTH_TOKEN', label : 'Twilio auth token', message : 'Beside the account SID on the Twilio console dashboard.', redact : true, required : true }
3266
- ],
3267
- icon : twilioIcon,
3268
- name : 'Twilio'
3269
- },
3270
- // The CRM sync is best-effort internal tooling and no-ops without a
3271
- // token. Not live here costs nothing a merchant can see.
3272
- hubspot : {
3273
- fields : [
3274
- { input : 'password', key : 'hubspotToken', credential : 'HUBSPOT_ACCESS_TOKEN', label : 'HubSpot access token', message : 'Drawbridge\'s own CRM portal. Internal — no merchant sees this.', redact : true, required : true }
3275
- ],
3276
- icon : hubspotIcon,
3277
- name : 'HubSpot'
3278
- }
3279
- }
3280
- },
3281
3202
  // NOTHING, and HUBSPOT_ACCESS_TOKEN in particular must not be here.
3282
3203
  //
3283
3204
  // `requires` gates AVAILABILITY: a name in it that is unset removes the whole
@@ -3488,6 +3409,16 @@ var drawbridge = {
3488
3409
 
3489
3410
  },
3490
3411
  tasks : () => [],
3412
+ // THE VENDORS THIS CONNECTION SPENDS, by slug. Each is declared once in
3413
+ // lib/connections/vendors/, and named here by whoever uses it — the block that
3414
+ // used to sit here declared them inline, so two manifests naming one vendor had
3415
+ // to be reconciled at import by mergeVendors().
3416
+ //
3417
+ // THREE, and they have nothing to do with each other: drawbridge is the
3418
+ // platform's own sender and spends SendGrid for email, Twilio for SMS and
3419
+ // HubSpot for CRM. They were bundled under one manifest only because one
3420
+ // connection happens to use all three.
3421
+ vendors : [ 'sendgrid', 'twilio', 'hubspot' ],
3491
3422
  title : 'Drawbridge'
3492
3423
  };
3493
3424
 
@@ -4461,24 +4392,6 @@ var klaviyo = {
4461
4392
 
4462
4393
  },
4463
4394
  icon: icon$2,
4464
- // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
4465
- // what an admin types on the provider screen. Declared here rather than in a
4466
- // table in lib/providers.js, so a vendor's credentials sit beside the
4467
- // `requires` that names the same variables. Keyed by the VENDOR that holds
4468
- // them — here the vendor and the connection are the same thing, which is not
4469
- // true of every manifest.
4470
- provider : {
4471
- vendors : {
4472
- klaviyo : {
4473
- fields : [
4474
- { input : 'text', key : 'clientId', credential : 'KLAVIYO_OAUTH_CLIENT_ID', label : 'Client ID', message : 'From our app in the Klaviyo developer portal (developers.klaviyo.com).', required : true },
4475
- { input : 'password', key : 'clientSecret', credential : 'KLAVIYO_OAUTH_CLIENT_SECRET', label : 'Client secret', message : 'Beside the Client ID on the Klaviyo app.', redact : true, required : true }
4476
- ],
4477
- icon: icon$2,
4478
- name : 'Klaviyo'
4479
- }
4480
- }
4481
- },
4482
4395
  requires : [
4483
4396
  'KLAVIYO_OAUTH_CLIENT_ID',
4484
4397
  'KLAVIYO_OAUTH_CLIENT_SECRET'
@@ -4639,6 +4552,11 @@ var klaviyo = {
4639
4552
 
4640
4553
  },
4641
4554
 
4555
+ // THE VENDORS THIS CONNECTION SPENDS, by slug. Each is declared once in
4556
+ // lib/connections/vendors/, and named here by whoever uses it — the block that
4557
+ // used to sit here declared them inline, so two manifests naming one vendor had
4558
+ // to be reconciled at import by mergeVendors().
4559
+ vendors : [ 'klaviyo' ],
4642
4560
  title : 'Klaviyo',
4643
4561
 
4644
4562
  // KLAVIYO PUBLISHES NO DASHBOARD URLS in its API reference. What is on record
@@ -5374,21 +5292,6 @@ var mailchimp = {
5374
5292
  webhook : false
5375
5293
  },
5376
5294
  icon: icon$1,
5377
- // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
5378
- // what an admin types on the provider screen, beside the `requires` naming the
5379
- // same variables.
5380
- provider : {
5381
- vendors : {
5382
- mailchimp : {
5383
- fields : [
5384
- { input : 'text', key : 'clientId', credential : 'MAILCHIMP_OAUTH_CLIENT_ID', label : 'Client ID', message : 'From our registered app — Mailchimp account, Extras, Registered apps.', required : true },
5385
- { input : 'password', key : 'clientSecret', credential : 'MAILCHIMP_OAUTH_CLIENT_SECRET', label : 'Client secret', message : 'Beside the Client ID on the registered app.', redact : true, required : true }
5386
- ],
5387
- icon: icon$1,
5388
- name : 'Mailchimp'
5389
- }
5390
- }
5391
- },
5392
5295
  // The OAuth client this deployment registered. Without both, the vendor drops
5393
5296
  // out of availableConnections rather than offering a Connect button that
5394
5297
  // cannot complete.
@@ -5505,6 +5408,11 @@ var mailchimp = {
5505
5408
  }
5506
5409
  ]
5507
5410
  ),
5411
+ // THE VENDORS THIS CONNECTION SPENDS, by slug. Each is declared once in
5412
+ // lib/connections/vendors/, and named here by whoever uses it — the block that
5413
+ // used to sit here declared them inline, so two manifests naming one vendor had
5414
+ // to be reconciled at import by mergeVendors().
5415
+ vendors : [ 'mailchimp' ],
5508
5416
  title : 'Mailchimp',
5509
5417
  // THE MERCHANT'S OWN ADMIN. Mailchimp's list schema states the shape outright:
5510
5418
  // the web_id field is "The ID used in the Mailchimp web application. View this
@@ -7304,41 +7212,6 @@ var shopify = {
7304
7212
  },
7305
7213
  icon,
7306
7214
  inbound,
7307
- // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
7308
- // what an admin types on the provider screen. The four names below are exactly
7309
- // what `requires` gates on, which is the point of declaring them together: a
7310
- // name required by the manifest and enterable nowhere is a vendor that can
7311
- // never go live from the admin screen.
7312
- provider : {
7313
- vendors : {
7314
- shopify : {
7315
- fields : [
7316
- { input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', message : 'The app\'s Client ID — Dev Dashboard, the app, Settings.', required : true },
7317
- { input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', message : 'The app\'s Client secret, beside the Client ID in the app\'s settings.', redact : true, required : true },
7318
- { input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', message : 'The app\'s URL handle (for example drawbridge-development), from the app\'s configuration.', required : true },
7319
- { input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', message : 'Where "View on the Shopify App Store" sends merchants — the public listing, or the Dev Dashboard install link for the dev app.', required : true },
7320
- // The usage meter's event handle, when the plan config's meter ever
7321
- // changes: stored value overrides the built-in default
7322
- // (events.order.handle) at the mint point, and `format : 'slug'`
7323
- // normalizes it on save — a handle can carry no spaces or capitals,
7324
- // and classification against it is case-sensitive. Optional: empty
7325
- // means the default, and the sender still refuses any handle that
7326
- // disagrees with the transaction it minted.
7327
- { format : 'slug', input : 'text', key : 'orderEventHandle', label : 'Order event handle', message : 'Must match the usage meter\'s Handle field on the plan exactly — lowercase and hyphens. Leave empty to use drawbridge-orders.', required : false, setting : true },
7328
- // The Partner API credential set — the ONE place meter billing is
7329
- // observable (CHARGE_USAGE accrual events). Org id and numeric app
7330
- // id come from the Partner Dashboard; the token from Settings →
7331
- // Partner API clients (Manage-apps permission). All optional: the
7332
- // accrual probe simply stays off until they exist.
7333
- { input : 'text', key : 'partnerOrgId', label : 'Partner organization ID', message : 'The number in the Partner Dashboard URL — partners.shopify.com/<this number>/… — NOT the admin organization_id from the app listing link.', required : false, setting : true },
7334
- { input : 'text', key : 'partnerAppId', label : 'Partner app ID (numeric)', message : 'The numeric id in the app\'s Partner Dashboard URL — not the Client ID.', required : false, setting : true },
7335
- { input : 'password', key : 'partnerToken', label : 'Partner API token', message : 'Partner Dashboard, Settings, Partner API clients — create one with Manage apps and paste the prtapi_ token.', redact : true, required : false, setting : true }
7336
- ],
7337
- icon,
7338
- name : 'Shopify'
7339
- }
7340
- }
7341
- },
7342
7215
  // A pre-launch integration: it only surfaces once the App Store listing
7343
7216
  // exists and the app is fully configured. Requiring all four means it can
7344
7217
  // never render half-configured — and absence of any one excludes the
@@ -7557,6 +7430,13 @@ var shopify = {
7557
7430
  : []
7558
7431
  )
7559
7432
  ],
7433
+ // THE VENDORS THIS CONNECTION SPENDS, by slug. Declared once in
7434
+ // lib/connections/vendors/shopify.js, and named here by whoever uses it.
7435
+ //
7436
+ // SHOPIFY_API_SECRET is spent by drawbridge-shopify-app and drawbridge-webhooks
7437
+ // too, which is the clearest case for why a vendor is not owned by whichever
7438
+ // connection happened to declare it first.
7439
+ vendors : [ 'shopify' ],
7560
7440
  title : 'Shopify',
7561
7441
  // THE VENDOR'S OWN ADMIN, one function per thing worth linking to. It lives
7562
7442
  // here rather than at the top level so a second link (a product, an order)
@@ -7763,7 +7643,7 @@ var webhook = {
7763
7643
  // Borrowed: this is the Drawbridge mark, because Webhooks has none of its own.
7764
7644
  // It is the one card that reads wrong — our logo among vendor logos — and it
7765
7645
  // wants a mark of its own when there is one.
7766
- icon: icon$3,
7646
+ icon: icon$6,
7767
7647
  // Gated on the encryption secret: without it the signing secret could not be
7768
7648
  // stored safely, so the connection must not be offered at all.
7769
7649
  requires : [ 'ENCRYPT_CONNECTION_SECRET' ],
@@ -7829,9 +7709,481 @@ var webhook = {
7829
7709
  }
7830
7710
  ]
7831
7711
  ),
7712
+ // THE VENDORS THIS CONNECTION SPENDS, by slug. Each is declared once in
7713
+ // lib/connections/vendors/, and named here by whoever uses it — the block that
7714
+ // used to sit here declared them inline, so two manifests naming one vendor had
7715
+ // to be reconciled at import by mergeVendors().
7716
+ //
7717
+ // Empty: webhook has no third party behind it — nothing to authenticate
7718
+ // against and nothing for an admin to type.
7719
+ vendors : [],
7832
7720
  title : 'Webhooks'
7833
7721
  };
7834
7722
 
7723
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR ATTENTIVE, and where the facts about it came
7724
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
7725
+ // in the `provider` collection holding our keys for it, and a connection names
7726
+ // the ones it spends.
7727
+ //
7728
+ // This lived inside attentive.js until 2026-09-13, for a reason that no longer
7729
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
7730
+ // variables". `requires` derives from the `credential` keys below now, so the
7731
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
7732
+ // throw-guards whose only job was reconciling two manifests that described one
7733
+ // vendor differently. Declared once, none of that can happen.
7734
+ var attentiveVendor = {
7735
+
7736
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
7737
+ // field falls back to, and is the only place that name is written now — it used
7738
+ // to appear three times per manifest.
7739
+ fields : [
7740
+ {
7741
+ credential : 'ATTENTIVE_OAUTH_CLIENT_ID',
7742
+ input : 'text',
7743
+ key : 'clientId',
7744
+ label : 'Client ID',
7745
+ message : 'From our app\'s registration in the Attentive developer portal.',
7746
+ required : true
7747
+ },
7748
+ {
7749
+ credential : 'ATTENTIVE_OAUTH_CLIENT_SECRET',
7750
+ input : 'password',
7751
+ key : 'clientSecret',
7752
+ label : 'Client secret',
7753
+ message : 'Beside the Client ID in the Attentive developer portal.',
7754
+ redact : true,
7755
+ required : true
7756
+ }
7757
+ ],
7758
+
7759
+ // The same mark the connection carries. One vendor, one icon — mergeVendors
7760
+ // used to throw when two manifests disagreed about this.
7761
+ icon: icon$7,
7762
+
7763
+ name : 'Attentive',
7764
+
7765
+ slug : 'attentive',
7766
+
7767
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
7768
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
7769
+ // answered two of them with an object keyed by vendor while the other four
7770
+ // answered with a string — two incompatible shapes for one key, and the shape
7771
+ // was saying where the data belonged.
7772
+ urls : {
7773
+ api : 'https://docs.attentive.com/reference/listsegments',
7774
+ dashboard : 'https://docs.attentive.com/docs/segments',
7775
+ scopes : 'https://docs.attentive.com/docs/authentication'
7776
+ }
7777
+
7778
+ };
7779
+
7780
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR HUBSPOT, and where the facts about it came
7781
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
7782
+ // in the `provider` collection holding our keys for it, and a connection names
7783
+ // the ones it spends.
7784
+ //
7785
+ // This lived inside drawbridge.js until 2026-09-13, for a reason that no longer
7786
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
7787
+ // variables". `requires` derives from the `credential` keys below now, so the
7788
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
7789
+ // throw-guards whose only job was reconciling two manifests that described one
7790
+ // vendor differently. Declared once, none of that can happen.
7791
+ var hubspotVendor = {
7792
+
7793
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
7794
+ // field falls back to, and is the only place that name is written now — it used
7795
+ // to appear three times per manifest.
7796
+ fields : [
7797
+ {
7798
+ credential : 'HUBSPOT_ACCESS_TOKEN',
7799
+ input : 'password',
7800
+ key : 'hubspotToken',
7801
+ label : 'HubSpot access token',
7802
+ message : 'Drawbridge\'s own CRM portal. Internal — no merchant sees this.',
7803
+ redact : true,
7804
+ required : true
7805
+ }
7806
+ ],
7807
+
7808
+ // The vendor's own mark rather than the connection's: drawbridge fronts three
7809
+ // vendors and none of them is Drawbridge.
7810
+ icon: icon$5,
7811
+
7812
+ name : 'HubSpot',
7813
+
7814
+ slug : 'hubspot',
7815
+
7816
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
7817
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
7818
+ // answered two of them with an object keyed by vendor while the other four
7819
+ // answered with a string — two incompatible shapes for one key, and the shape
7820
+ // was saying where the data belonged.
7821
+ urls : {
7822
+ api : 'https://developers.hubspot.com/docs/reference/api/crm/objects/contacts',
7823
+ dashboard : 'https://knowledge.hubspot.com/contacts/create-contacts',
7824
+ scopes : false
7825
+ }
7826
+
7827
+ };
7828
+
7829
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR KLAVIYO, and where the facts about it came
7830
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
7831
+ // in the `provider` collection holding our keys for it, and a connection names
7832
+ // the ones it spends.
7833
+ //
7834
+ // This lived inside klaviyo.js until 2026-09-13, for a reason that no longer
7835
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
7836
+ // variables". `requires` derives from the `credential` keys below now, so the
7837
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
7838
+ // throw-guards whose only job was reconciling two manifests that described one
7839
+ // vendor differently. Declared once, none of that can happen.
7840
+ var klaviyoVendor = {
7841
+
7842
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
7843
+ // field falls back to, and is the only place that name is written now — it used
7844
+ // to appear three times per manifest.
7845
+ fields : [
7846
+ {
7847
+ credential : 'KLAVIYO_OAUTH_CLIENT_ID',
7848
+ input : 'text',
7849
+ key : 'clientId',
7850
+ label : 'Client ID',
7851
+ message : 'From our app in the Klaviyo developer portal (developers.klaviyo.com).',
7852
+ required : true
7853
+ },
7854
+ {
7855
+ credential : 'KLAVIYO_OAUTH_CLIENT_SECRET',
7856
+ input : 'password',
7857
+ key : 'clientSecret',
7858
+ label : 'Client secret',
7859
+ message : 'Beside the Client ID on the Klaviyo app.',
7860
+ redact : true,
7861
+ required : true
7862
+ }
7863
+ ],
7864
+
7865
+ // The same mark the connection carries. One vendor, one icon — mergeVendors
7866
+ // used to throw when two manifests disagreed about this.
7867
+ icon: icon$2,
7868
+
7869
+ name : 'Klaviyo',
7870
+
7871
+ slug : 'klaviyo',
7872
+
7873
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
7874
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
7875
+ // answered two of them with an object keyed by vendor while the other four
7876
+ // answered with a string — two incompatible shapes for one key, and the shape
7877
+ // was saying where the data belonged.
7878
+ urls : {
7879
+ api : 'https://developers.klaviyo.com/en/reference/api_overview',
7880
+ dashboard : 'https://help.klaviyo.com/hc/en-us/articles/115005078647',
7881
+ scopes : 'https://developers.klaviyo.com/en/docs/authenticate_'
7882
+ }
7883
+
7884
+ };
7885
+
7886
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR MAILCHIMP, and where the facts about it came
7887
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
7888
+ // in the `provider` collection holding our keys for it, and a connection names
7889
+ // the ones it spends.
7890
+ //
7891
+ // This lived inside mailchimp.js until 2026-09-13, for a reason that no longer
7892
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
7893
+ // variables". `requires` derives from the `credential` keys below now, so the
7894
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
7895
+ // throw-guards whose only job was reconciling two manifests that described one
7896
+ // vendor differently. Declared once, none of that can happen.
7897
+ var mailchimpVendor = {
7898
+
7899
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
7900
+ // field falls back to, and is the only place that name is written now — it used
7901
+ // to appear three times per manifest.
7902
+ fields : [
7903
+ {
7904
+ credential : 'MAILCHIMP_OAUTH_CLIENT_ID',
7905
+ input : 'text',
7906
+ key : 'clientId',
7907
+ label : 'Client ID',
7908
+ message : 'From our registered app — Mailchimp account, Extras, Registered apps.',
7909
+ required : true
7910
+ },
7911
+ {
7912
+ credential : 'MAILCHIMP_OAUTH_CLIENT_SECRET',
7913
+ input : 'password',
7914
+ key : 'clientSecret',
7915
+ label : 'Client secret',
7916
+ message : 'Beside the Client ID on the registered app.',
7917
+ redact : true,
7918
+ required : true
7919
+ }
7920
+ ],
7921
+
7922
+ // The same mark the connection carries. One vendor, one icon — mergeVendors
7923
+ // used to throw when two manifests disagreed about this.
7924
+ icon: icon$1,
7925
+
7926
+ name : 'Mailchimp',
7927
+
7928
+ slug : 'mailchimp',
7929
+
7930
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
7931
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
7932
+ // answered two of them with an object keyed by vendor while the other four
7933
+ // answered with a string — two incompatible shapes for one key, and the shape
7934
+ // was saying where the data belonged.
7935
+ urls : {
7936
+ api : 'https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/',
7937
+ dashboard : 'https://mailchimp.com/help/manage-tags/',
7938
+ scopes : false
7939
+ }
7940
+
7941
+ };
7942
+
7943
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR SENDGRID, and where the facts about it came
7944
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
7945
+ // in the `provider` collection holding our keys for it, and a connection names
7946
+ // the ones it spends.
7947
+ //
7948
+ // This lived inside drawbridge.js until 2026-09-13, for a reason that no longer
7949
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
7950
+ // variables". `requires` derives from the `credential` keys below now, so the
7951
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
7952
+ // throw-guards whose only job was reconciling two manifests that described one
7953
+ // vendor differently. Declared once, none of that can happen.
7954
+ var sendgridVendor = {
7955
+
7956
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
7957
+ // field falls back to, and is the only place that name is written now — it used
7958
+ // to appear three times per manifest.
7959
+ fields : [
7960
+ {
7961
+ credential : 'SENDGRID_FROM_ADDRESS',
7962
+ input : 'email',
7963
+ key : 'accountSender',
7964
+ label : 'Account sender',
7965
+ message : 'Verification codes and security alerts send from here.',
7966
+ required : true
7967
+ },
7968
+ {
7969
+ credential : 'SENDGRID_API_KEY',
7970
+ input : 'password',
7971
+ key : 'apiKey',
7972
+ label : 'SendGrid API key',
7973
+ message : 'SendGrid dashboard, Settings, API Keys — full access for sends.',
7974
+ redact : true,
7975
+ required : true
7976
+ },
7977
+ {
7978
+ credential : 'SENDGRID_EVENT_WEBHOOK_KEY',
7979
+ input : 'text',
7980
+ key : 'eventKey',
7981
+ label : 'Event webhook key',
7982
+ message : 'SendGrid, Settings, Mail Settings, Event Webhook — the verification key shown once signature verification is enabled. Public, not a secret.'
7983
+ },
7984
+ {
7985
+ credential : 'SENDGRID_SEND_FROM_ADDRESS',
7986
+ input : 'email',
7987
+ key : 'leadSender',
7988
+ label : 'Lead sender',
7989
+ message : 'The default for lead-facing mail when a merchant has not verified their own domain.'
7990
+ }
7991
+ ],
7992
+
7993
+ // The vendor's own mark rather than the connection's: drawbridge fronts three
7994
+ // vendors and none of them is Drawbridge.
7995
+ icon: icon$4,
7996
+
7997
+ name : 'SendGrid',
7998
+
7999
+ slug : 'sendgrid',
8000
+
8001
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
8002
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
8003
+ // answered two of them with an object keyed by vendor while the other four
8004
+ // answered with a string — two incompatible shapes for one key, and the shape
8005
+ // was saying where the data belonged.
8006
+ urls : {
8007
+ api : 'https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send',
8008
+ dashboard : 'https://www.twilio.com/docs/sendgrid/ui/analytics-and-reporting/email-activity-feed',
8009
+ scopes : false
8010
+ }
8011
+
8012
+ };
8013
+
8014
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR SHOPIFY, and where the facts about it came
8015
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
8016
+ // in the `provider` collection holding our keys for it, and a connection names
8017
+ // the ones it spends.
8018
+ //
8019
+ // This lived inside shopify.js until 2026-09-13, for a reason that no longer
8020
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
8021
+ // variables". `requires` derives from the `credential` keys below now, so the
8022
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
8023
+ // throw-guards whose only job was reconciling two manifests that described one
8024
+ // vendor differently. Declared once, none of that can happen.
8025
+ var shopifyVendor = {
8026
+
8027
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
8028
+ // field falls back to, and is the only place that name is written now — it used
8029
+ // to appear three times per manifest.
8030
+ fields : [
8031
+ {
8032
+ credential : 'SHOPIFY_API_KEY',
8033
+ input : 'text',
8034
+ key : 'apiKey',
8035
+ label : 'API key',
8036
+ message : 'The app\'s Client ID — Dev Dashboard, the app, Settings.',
8037
+ required : true
8038
+ },
8039
+ {
8040
+ credential : 'SHOPIFY_API_SECRET',
8041
+ input : 'password',
8042
+ key : 'apiSecret',
8043
+ label : 'API secret',
8044
+ message : 'The app\'s Client secret, beside the Client ID in the app\'s settings.',
8045
+ redact : true,
8046
+ required : true
8047
+ },
8048
+ {
8049
+ credential : 'SHOPIFY_APP_HANDLE',
8050
+ input : 'text',
8051
+ key : 'appHandle',
8052
+ label : 'App handle',
8053
+ message : 'The app\'s URL handle (for example drawbridge-development), from the app\'s configuration.',
8054
+ required : true
8055
+ },
8056
+ {
8057
+ credential : 'SHOPIFY_APP_LISTING_URL',
8058
+ input : 'text',
8059
+ key : 'listingUrl',
8060
+ label : 'App listing URL',
8061
+ message : 'Where "View on the Shopify App Store" sends merchants — the public listing, or the Dev Dashboard install link for the dev app.',
8062
+ required : true
8063
+ },
8064
+ {
8065
+ format : 'slug',
8066
+ input : 'text',
8067
+ key : 'orderEventHandle',
8068
+ label : 'Order event handle',
8069
+ message : 'Must match the usage meter\'s Handle field on the plan exactly — lowercase and hyphens. Leave empty to use drawbridge-orders.',
8070
+ required : false,
8071
+ setting : true
8072
+ },
8073
+ {
8074
+ input : 'text',
8075
+ key : 'partnerOrgId',
8076
+ label : 'Partner organization ID',
8077
+ message : 'The number in the Partner Dashboard URL — partners.shopify.com/<this number>/… — NOT the admin organization_id from the app listing link.',
8078
+ required : false,
8079
+ setting : true
8080
+ },
8081
+ {
8082
+ input : 'text',
8083
+ key : 'partnerAppId',
8084
+ label : 'Partner app ID (numeric)',
8085
+ message : 'The numeric id in the app\'s Partner Dashboard URL — not the Client ID.',
8086
+ required : false,
8087
+ setting : true
8088
+ },
8089
+ {
8090
+ input : 'password',
8091
+ key : 'partnerToken',
8092
+ label : 'Partner API token',
8093
+ message : 'Partner Dashboard, Settings, Partner API clients — create one with Manage apps and paste the prtapi_ token.',
8094
+ redact : true,
8095
+ required : false,
8096
+ setting : true
8097
+ }
8098
+ ],
8099
+
8100
+ // The same mark the connection carries. One vendor, one icon — mergeVendors
8101
+ // used to throw when two manifests disagreed about this.
8102
+ icon,
8103
+
8104
+ name : 'Shopify',
8105
+
8106
+ slug : 'shopify',
8107
+
8108
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
8109
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
8110
+ // answered two of them with an object keyed by vendor while the other four
8111
+ // answered with a string — two incompatible shapes for one key, and the shape
8112
+ // was saying where the data belonged.
8113
+ urls : {
8114
+ api : 'https://shopify.dev/docs/api/admin-graphql',
8115
+ dashboard : 'https://help.shopify.com/en/manual/apps',
8116
+ scopes : 'https://shopify.dev/docs/api/usage/access-scopes'
8117
+ }
8118
+
8119
+ };
8120
+
8121
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR TWILIO, and where the facts about it came
8122
+ // from. One file per vendor, because a vendor is NOT a connection: it is the row
8123
+ // in the `provider` collection holding our keys for it, and a connection names
8124
+ // the ones it spends.
8125
+ //
8126
+ // This lived inside drawbridge.js until 2026-09-13, for a reason that no longer
8127
+ // holds — "so a vendor's credentials sit beside the `requires` that names the same
8128
+ // variables". `requires` derives from the `credential` keys below now, so the
8129
+ // proximity bought nothing, and the nesting cost mergeVendors(): 66 lines and four
8130
+ // throw-guards whose only job was reconciling two manifests that described one
8131
+ // vendor differently. Declared once, none of that can happen.
8132
+ var twilioVendor = {
8133
+
8134
+ // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
8135
+ // field falls back to, and is the only place that name is written now — it used
8136
+ // to appear three times per manifest.
8137
+ fields : [
8138
+ {
8139
+ credential : 'TWILIO_ACCOUNT_FROM',
8140
+ input : 'text',
8141
+ key : 'smsFrom',
8142
+ label : 'SMS number',
8143
+ message : 'Without Twilio credentials, SMS is disabled across the platform.',
8144
+ required : true
8145
+ },
8146
+ {
8147
+ credential : 'TWILIO_ACCOUNT_SID',
8148
+ input : 'password',
8149
+ key : 'smsSid',
8150
+ label : 'Twilio account SID',
8151
+ message : 'Twilio console home — the AC… identifier on the account dashboard.',
8152
+ redact : true,
8153
+ required : true
8154
+ },
8155
+ {
8156
+ credential : 'TWILIO_AUTH_TOKEN',
8157
+ input : 'password',
8158
+ key : 'smsToken',
8159
+ label : 'Twilio auth token',
8160
+ message : 'Beside the account SID on the Twilio console dashboard.',
8161
+ redact : true,
8162
+ required : true
8163
+ }
8164
+ ],
8165
+
8166
+ // The vendor's own mark rather than the connection's: drawbridge fronts three
8167
+ // vendors and none of them is Drawbridge.
8168
+ icon: icon$3,
8169
+
8170
+ name : 'Twilio',
8171
+
8172
+ slug : 'twilio',
8173
+
8174
+ // WHERE THE FACTS CAME FROM. Was review.api / .dashboard / .scopes on the
8175
+ // manifest. They are per-vendor documentation, which is exactly why drawbridge
8176
+ // answered two of them with an object keyed by vendor while the other four
8177
+ // answered with a string — two incompatible shapes for one key, and the shape
8178
+ // was saying where the data belonged.
8179
+ urls : {
8180
+ api : 'https://www.twilio.com/docs/messaging/api/message-resource',
8181
+ dashboard : 'https://www.twilio.com/docs/messaging/guides/debugging-tools',
8182
+ scopes : false
8183
+ }
8184
+
8185
+ };
8186
+
7835
8187
  // Every connection, one file each, and this is the whole registry.
7836
8188
  //
7837
8189
  // It lives in utils rather than in the api because BOTH sides need it and they
@@ -7909,6 +8261,81 @@ const checkIcon = ( owner, icon ) => {
7909
8261
 
7910
8262
  };
7911
8263
 
8264
+ // KEYED BY SLUG, from the files themselves. This was mergeVendors( connections ) —
8265
+ // 66 lines walking every manifest's provider.vendors and reconciling them, with
8266
+ // four throw-guards for two manifests naming one vendor differently, differing on
8267
+ // its icon, declaring one of its fields two ways, or claiming the same credential.
8268
+ // None of those can happen when a vendor is declared once, so all of it is gone.
8269
+ const vendors = Object.freeze( Object.fromEntries(
8270
+ [ attentiveVendor, hubspotVendor, klaviyoVendor, mailchimpVendor, sendgridVendor, shopifyVendor, twilioVendor ]
8271
+ .map( ( vendor ) => [ vendor.slug, vendor ] )
8272
+ ) );
8273
+
8274
+ // WHAT A VENDOR MUST DECLARE. Thrown at import, the same way build() refuses a
8275
+ // manifest — exported so the refusal itself is testable rather than only
8276
+ // observable by breaking a real vendor file.
8277
+ //
8278
+ // This ran inside build() over each manifest's provider.vendors until 2026-09-13,
8279
+ // which meant a vendor declared by two manifests was validated twice and could
8280
+ // disagree with itself between them. Declared once, checked once.
8281
+ //
8282
+ // A field is checked harder than a merchant field: it must be editable — an admin
8283
+ // cannot type into a read-only descriptor, and a credential nobody can enter is a
8284
+ // vendor that can never go live — and a `password` MUST be redacted. That last one
8285
+ // is a SECURITY BOUNDARY rather than tidiness: providerFields is what the api's
8286
+ // redaction derives from, so a password field that forgot `redact : true` is a
8287
+ // platform secret handed back over the wire to every admin screen that asks.
8288
+ const buildVendor = ( vendor ) => {
8289
+
8290
+ if( ! vendor?.slug ) throw new Error( 'A vendor needs a slug' );
8291
+
8292
+ if( ! vendor.name ) throw new Error( 'vendor ' + vendor.slug + ' has no name' );
8293
+
8294
+ // The admin card shows the vendor's own mark, which is not always the
8295
+ // connection's — SendGrid's card is not the Drawbridge logo.
8296
+ checkIcon( 'vendor ' + vendor.slug, vendor.icon );
8297
+
8298
+ if( ! Array.isArray( vendor.fields ) || ! vendor.fields.length ){
8299
+
8300
+ throw new Error( 'vendor ' + vendor.slug + ' has no fields — a vendor nothing is typed into holds nothing' );
8301
+
8302
+ }
8303
+
8304
+ for( const field of vendor.fields ){
8305
+
8306
+ if( ! field?.key || ! field?.label ){
8307
+
8308
+ throw new Error( 'vendor ' + vendor.slug + ' declares a field with no key or label' );
8309
+
8310
+ }
8311
+
8312
+ if( ! INPUTS.includes( field.input ) ){
8313
+
8314
+ throw new Error( vendor.slug + '.' + field.key + ' needs an input the admin form can render — one of ' + INPUTS.join( ', ' ) );
8315
+
8316
+ }
8317
+
8318
+ if( field.input === 'password' && ! field.redact ){
8319
+
8320
+ throw new Error( vendor.slug + '.' + field.key + ' is a password and must declare redact : true — the api would hand the value back' );
8321
+
8322
+ }
8323
+
8324
+ }
8325
+
8326
+ return Object.freeze( vendor );
8327
+
8328
+ };
8329
+
8330
+ for( const [ slug, vendor ] of Object.entries( vendors ) ){
8331
+
8332
+ if( vendor.slug !== slug ) throw new Error( 'vendor file ' + slug + ' declares slug ' + vendor.slug );
8333
+
8334
+ buildVendor( vendor );
8335
+
8336
+ }
8337
+
8338
+
7912
8339
  // WHAT A CONNECTION MUST DECLARE. Thrown at import rather than discovered by a
7913
8340
  // merchant looking at a broken card, or by a workflow that accepted a step it
7914
8341
  // could never run.
@@ -8008,75 +8435,44 @@ const build = ( manifest ) => {
8008
8435
 
8009
8436
  }
8010
8437
 
8011
- // DRAWBRIDGE'S OWN CREDENTIALS, declared under the VENDOR that holds them.
8012
- //
8013
- // A provider is a vendor Drawbridge has an account with; a connection is a
8014
- // thing a merchant turns on. They are not one-to-one in either direction:
8015
- // `drawbridge` spends SendGrid, Twilio and HubSpot, and a future Google row
8016
- // would be spent by analytics, geocoding and whatever comes after. So a
8017
- // manifest declares the fields IT spends, subkeyed by vendor, and mergeVendors
8018
- // below unions every manifest's declarations into one vendor each. Nobody
8019
- // owns a vendor — removing a manifest removes its fields and nothing else.
8020
- //
8021
- // A connection with no third party behind it — webhook — declares no block at
8022
- // all, which is what keeps it off the provider screen.
8438
+ // THE ICON RIDES WITH THE MANIFEST, so a vendor cannot name an asset nobody
8439
+ // added — which is what the old arrangement allowed, with the markup in one
8440
+ // repo and the file in another.
8441
+ checkIcon( manifest.slug, manifest?.icon );
8442
+
8443
+ // WHICH VENDORS THIS CONNECTION SPENDS, and they have to exist.
8023
8444
  //
8024
- // Every field is checked harder than a merchant field: it must be editable
8025
- // (an admin cannot type into a read-only descriptor, and a credential nobody
8026
- // can enter is a vendor that can never go live), and a `password` MUST be
8027
- // redacted. That last one is a SECURITY BOUNDARY rather than tidiness —
8028
- // providerFields is what the api's redaction derives from, so a password field
8029
- // that forgot `redact : true` is a platform secret handed back over the wire
8030
- // to every admin screen that asks.
8031
- if( manifest.provider?.fields ){
8445
+ // This used to be the whole vendor declaration, nested here name, icon,
8446
+ // fields and all which is why mergeVendors() existed to reconcile two
8447
+ // manifests describing one vendor. The vendors are their own files now and are
8448
+ // checked above; a manifest names slugs.
8449
+ if( ! Array.isArray( manifest.vendors ) ){
8032
8450
 
8033
- throw new Error( manifest.slug + ' declares provider.fieldsfields live under provider.vendors.<vendor> now, keyed by the vendor that holds them' );
8451
+ throw new Error( manifest.slug + ' must declare vendors an array of vendor slugs, empty when it spends none' );
8034
8452
 
8035
8453
  }
8036
8454
 
8037
8455
  const declaredKeys = new Set();
8038
8456
 
8039
- for( const [ vendor, declared ] of Object.entries( manifest.provider?.vendors || {} ) ){
8457
+ for( const slug of manifest.vendors ){
8040
8458
 
8041
- if( ! declared?.name ) throw new Error( manifest.slug + ' declares vendor ' + vendor + ' with no name' );
8459
+ if( ! Object.hasOwn( vendors, slug ) ){
8042
8460
 
8043
- // The admin card shows the vendor's own mark, which is not always the
8044
- // connection's — SendGrid's card is not the Drawbridge logo.
8045
- checkIcon( manifest.slug + ' vendor ' + vendor, declared.icon );
8046
-
8047
- if( ! Array.isArray( declared.fields ) || ! declared.fields.length ){
8048
-
8049
- throw new Error( manifest.slug + ' declares vendor ' + vendor + ' with no fields — a vendor nothing is typed into holds nothing' );
8461
+ throw new Error( manifest.slug + ' spends vendor ' + slug + ', which has no file in lib/connections/vendors' );
8050
8462
 
8051
8463
  }
8052
8464
 
8053
- for( const field of declared.fields ){
8054
-
8055
- if( ! field?.key || ! field?.label ){
8056
-
8057
- throw new Error( manifest.slug + ' declares a provider field with no key or label' );
8058
-
8059
- }
8060
-
8061
- if( ! INPUTS.includes( field.input ) ){
8062
-
8063
- throw new Error( manifest.slug + '.provider.' + field.key + ' needs an input the admin form can render — one of ' + INPUTS.join( ', ' ) );
8064
-
8065
- }
8066
-
8067
- if( field.input === 'password' && ! field.redact ){
8068
-
8069
- throw new Error( manifest.slug + '.provider.' + field.key + ' is a password and must declare redact : true — the api would hand the value back' );
8070
-
8071
- }
8465
+ // ONE KEY PER CONNECTION across its vendors. vendorSettings() hands a
8466
+ // connection every vendor's settings merged into one object, so two vendors
8467
+ // on one manifest sharing a key would be one value silently winning over the
8468
+ // other. Checked here rather than on the vendor, because it is a fact about
8469
+ // the COMBINATION a connection spends: sendgrid and twilio are each fine, and
8470
+ // only drawbridge naming both makes a shared key a problem.
8471
+ for( const field of vendors[ slug ].fields ){
8072
8472
 
8073
- // ONE KEY PER CONNECTION across its vendors. vendorSettings() hands a
8074
- // connection every vendor's settings merged into one object, so two
8075
- // vendors on one manifest sharing a key would be one value silently
8076
- // winning over the other.
8077
8473
  if( declaredKeys.has( field.key ) ){
8078
8474
 
8079
- throw new Error( manifest.slug + ' declares ' + field.key + ' under two vendors — vendorSettings() merges them and one would win' );
8475
+ throw new Error( manifest.slug + ' spends ' + field.key + ' under two vendors — vendorSettings() merges them and one would win' );
8080
8476
 
8081
8477
  }
8082
8478
 
@@ -8086,11 +8482,6 @@ const build = ( manifest ) => {
8086
8482
 
8087
8483
  }
8088
8484
 
8089
- // THE ICON RIDES WITH THE MANIFEST, so a vendor cannot name an asset nobody
8090
- // added — which is what the old arrangement allowed, with the markup in one
8091
- // repo and the file in another.
8092
- checkIcon( manifest.slug, manifest?.icon );
8093
-
8094
8485
  // `group` does the actual grouping on the connections page, and `category`
8095
8486
  // was a second word for the same fact — so it is gone rather than kept as an
8096
8487
  // alias nobody could tell apart from this one.
@@ -8367,96 +8758,6 @@ const connections = Object.freeze({
8367
8758
  webhook : build( webhook )
8368
8759
  });
8369
8760
 
8370
- // A field declaration, with its keys in a fixed order, so two manifests that
8371
- // declare the same field can be compared as strings whatever order they wrote
8372
- // the keys in.
8373
- const canonical = ( field ) => JSON.stringify( Object.fromEntries( Object.entries( field ).sort() ) );
8374
-
8375
- // DRAWBRIDGE'S OWN VENDOR ACCOUNTS, one entry per vendor, unioned from every
8376
- // manifest that spends one. This is what the provider screen lists and what
8377
- // the `provider` collection is keyed by: one row per key here.
8378
- //
8379
- // Exported as a function over a catalog, and not only as the merged constant,
8380
- // so the guards can be exercised against a catalog that breaks them — the real
8381
- // one is built to pass.
8382
- //
8383
- // THREE GUARDS, and each one is a way two files could disagree about one fact:
8384
- // the vendor's name and icon the same vendor drawn two ways is two cards
8385
- // a field two manifests both the same key declared differently is a form
8386
- // declare that renders whichever manifest loaded last
8387
- // a credential name the join between `requires` and a row — two
8388
- // fields answering one name is one value silently
8389
- // winning, which is exactly the drift a row per
8390
- // vendor exists to end
8391
- const mergeVendors = ( catalog ) => {
8392
-
8393
- const merged = {};
8394
- const claimed = new Map();
8395
-
8396
- for( const [ slug, manifest ] of Object.entries( catalog ) ){
8397
-
8398
- for( const [ vendor, declared ] of Object.entries( manifest.provider?.vendors || {} ) ){
8399
-
8400
- const held = merged[ vendor ] || ( merged[ vendor ] = { fields : [], icon : declared.icon, name : declared.name } );
8401
-
8402
- if( held.name !== declared.name ){
8403
-
8404
- throw new Error( slug + ' names vendor ' + vendor + ' "' + declared.name + '" but another manifest names it "' + held.name + '"' );
8405
-
8406
- }
8407
-
8408
- if( held.icon !== declared.icon ){
8409
-
8410
- throw new Error( slug + ' declares a different icon for vendor ' + vendor + ' than another manifest — one vendor, one mark' );
8411
-
8412
- }
8413
-
8414
- for( const field of declared.fields ){
8415
-
8416
- const existing = held.fields.find( ( entry ) => entry.key === field.key );
8417
-
8418
- if( existing ){
8419
-
8420
- if( canonical( existing ) !== canonical( field ) ){
8421
-
8422
- throw new Error( slug + ' declares ' + vendor + '.' + field.key + ' differently from another manifest — a field two connections spend is declared identically in both' );
8423
-
8424
- }
8425
-
8426
- continue;
8427
-
8428
- }
8429
-
8430
- if( field.credential ){
8431
-
8432
- const owner = claimed.get( field.credential );
8433
-
8434
- if( owner ){
8435
-
8436
- throw new Error( field.credential + ' is claimed by both ' + owner + ' and ' + vendor + '.' + field.key + ' — one would silently overwrite the other' );
8437
-
8438
- }
8439
-
8440
- claimed.set( field.credential, vendor + '.' + field.key );
8441
-
8442
- }
8443
-
8444
- held.fields.push( field );
8445
-
8446
- }
8447
-
8448
- }
8449
-
8450
- }
8451
-
8452
- return Object.freeze( Object.fromEntries(
8453
- Object.entries( merged ).map( ( [ vendor, held ] ) => [ vendor, Object.freeze({ ...held, fields : Object.freeze( held.fields ) }) ] )
8454
- ) );
8455
-
8456
- };
8457
-
8458
- const vendors = mergeVendors( connections );
8459
-
8460
8761
  // A STEP TYPE MAY BE SHARED, BUT NOT ITS QUEUE.
8461
8762
  //
8462
8763
  // A step type belongs to the CAPABILITY rather than to whoever implements it —
@@ -8512,6 +8813,8 @@ const availableConnections = ( env = {} ) => Object.fromEntries(
8512
8813
  )
8513
8814
  );
8514
8815
 
8816
+
8817
+
8515
8818
  // WHAT A MERCHANT CAN SEE AND CONNECT. Everything available, minus the private
8516
8819
  // ones.
8517
8820
  //
@@ -8774,12 +9077,6 @@ const publicConnectionKeys = Object.freeze([
8774
9077
  // excerpt, guide, and any vendor redirect copy.
8775
9078
  'content',
8776
9079
  'createdAt',
8777
- // THE MERCHANT'S OWN SWITCH. `enabled : false` is a connection the merchant
8778
- // paused — kept, configured, and not run — as distinct from a vendor the
8779
- // platform switched off (which the api overlays as an error). Absent means
8780
- // on; the dashboard renders Paused from it and the sync gates refuse a paused
8781
- // connection the way they refuse a missing one.
8782
- 'enabled',
8783
9080
  // The connection DOCUMENT's own errors array — scope-drift entries written by
8784
9081
  // drawbridge-sync. NOT the manifest's error copy, which is content.errors:
8785
9082
  // the document is spread OVER the resolved manifest downstream, so the two
@@ -8873,4 +9170,4 @@ const resolveConnection = ( item, data, env = {} ) => {
8873
9170
 
8874
9171
  };
8875
9172
 
8876
- export { AUTH_TYPES, GROUPS, HOOKS, HOOK_EFFECTS, HOOK_NAMES, HOOK_OPTIONS, HOOK_PROPS, INPUTS, OAUTH_FIELDS, OUTCOMES, RETIRED, STATUSES, STEPS, STEP_TYPES, WRITE_OPERATIONS, accessToken, authToken, availableConnections, build, catalogConnections, connectFields, connectionSteps, connections, effectsOf, hookSupport, isStale, mergeSettings, mergeVendors, projectConnection, publicConnectionKeys, publicSettingsBySlug, redactSettings, resolveConnection, runHook, scopesMessage, stepLabels, stepQueues, stepRoutes, tokenSettings, vendors };
9173
+ export { AUTH_TYPES, GROUPS, HOOKS, HOOK_EFFECTS, HOOK_NAMES, HOOK_OPTIONS, HOOK_PROPS, INPUTS, OAUTH_FIELDS, OUTCOMES, RETIRED, STATUSES, STEPS, STEP_TYPES, WRITE_OPERATIONS, accessToken, authToken, availableConnections, build, buildVendor, catalogConnections, connectFields, connectionSteps, connections, effectsOf, hookSupport, isStale, mergeSettings, projectConnection, publicConnectionKeys, publicSettingsBySlug, redactSettings, resolveConnection, runHook, scopesMessage, stepLabels, stepQueues, stepRoutes, tokenSettings, vendors };