@drawbridge/drawbridge-utils 0.0.152 → 0.0.153

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.
@@ -57,6 +57,7 @@ __export(connections_exports, {
57
57
  hookSupport: () => hookSupport,
58
58
  isStale: () => isStale,
59
59
  mergeSettings: () => mergeSettings,
60
+ mergeVendors: () => mergeVendors,
60
61
  pkcePair: () => pkcePair,
61
62
  projectConnection: () => projectConnection,
62
63
  publicConnectionKeys: () => publicConnectionKeys,
@@ -68,7 +69,8 @@ __export(connections_exports, {
68
69
  stepLabels: () => stepLabels,
69
70
  stepQueues: () => stepQueues,
70
71
  stepRoutes: () => stepRoutes,
71
- tokenSettings: () => tokenSettings
72
+ tokenSettings: () => tokenSettings,
73
+ vendors: () => vendors
72
74
  });
73
75
  module.exports = __toCommonJS(connections_exports);
74
76
 
@@ -859,10 +861,16 @@ var attentive_default2 = {
859
861
  // `redact` marks a secret — never returned by the api, and blank on save means
860
862
  // keep the stored value. `required` drives the live check.
861
863
  provider: {
862
- fields: [
863
- { 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 },
864
- { 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 }
865
- ]
864
+ vendors: {
865
+ attentive: {
866
+ fields: [
867
+ { 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 },
868
+ { 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 }
869
+ ],
870
+ icon: attentive_default,
871
+ name: "Attentive"
872
+ }
873
+ }
866
874
  },
867
875
  requires: [
868
876
  "ATTENTIVE_OAUTH_CLIENT_ID",
@@ -1128,6 +1136,43 @@ var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fi
1128
1136
  </defs>
1129
1137
  </svg>`;
1130
1138
 
1139
+ // lib/connections/icons/hubspot.js
1140
+ var hubspot_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1141
+ <rect width="500" height="500" fill="#FF7A59"/>
1142
+ <circle cx="290" cy="290" r="78" stroke="white" stroke-width="30"/>
1143
+ <circle cx="290" cy="290" r="18" fill="white"/>
1144
+ <path d="M290 212V150" stroke="white" stroke-width="30" stroke-linecap="round"/>
1145
+ <circle cx="290" cy="125" r="28" fill="white"/>
1146
+ <path d="M270 137L172 96" stroke="white" stroke-width="22" stroke-linecap="round"/>
1147
+ <circle cx="160" cy="91" r="20" fill="white"/>
1148
+ <path d="M233 347L184 396" stroke="white" stroke-width="26" stroke-linecap="round"/>
1149
+ <circle cx="172" cy="408" r="22" fill="white"/>
1150
+ </svg>`;
1151
+
1152
+ // lib/connections/icons/sendgrid.js
1153
+ var sendgrid_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1154
+ <rect width="500" height="500" fill="#F1FAFF"/>
1155
+ <g clip-path="url(#clip0_1011_2743)">
1156
+ <path d="M366.676 134.534V289.172H289.203V366.487H134.258L134.258 289.171L134.256 289.172V211.851H211.729V134.534H366.676Z" fill="#9DD6E3"/>
1157
+ <path d="M134.258 366.488H211.731V289.169H134.258V366.488Z" fill="#3F72AB"/>
1158
+ <path d="M289.203 289.172H366.677V211.852H289.203V289.172Z" fill="#00A9D1"/>
1159
+ <path d="M211.729 211.853H289.202V134.534H211.729V211.853Z" fill="#00A9D1"/>
1160
+ <path d="M211.729 289.17H289.202V211.852H211.729V289.17Z" fill="#2191C4"/>
1161
+ <path d="M289.203 211.853H366.677V134.534H289.203V211.853Z" fill="#3F72AB"/>
1162
+ </g>
1163
+ <defs>
1164
+ <clipPath id="clip0_1011_2743">
1165
+ <rect width="232.42" height="231.956" fill="white" transform="translate(134.256 134.534)"/>
1166
+ </clipPath>
1167
+ </defs>
1168
+ </svg>`;
1169
+
1170
+ // lib/connections/icons/twilio.js
1171
+ var twilio_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
1172
+ <rect width="500" height="500" fill="#F22F46"/>
1173
+ <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"/>
1174
+ </svg>`;
1175
+
1131
1176
  // lib/transactions.js
1132
1177
  var import_drawbridge_telemetry = require("@drawbridge/drawbridge-telemetry");
1133
1178
 
@@ -2294,37 +2339,56 @@ var drawbridge_default2 = {
2294
2339
  //
2295
2340
  // They belong on THIS manifest because this is the connection that sends: the
2296
2341
  // email, sms and segment hooks below are the only things that spend them, and
2297
- // a private connection is still where a vendor fact lives.
2342
+ // a private connection is still where a vendor fact lives. They are THREE
2343
+ // VENDORS, not one, and each is its own row and its own admin card — so
2344
+ // SendGrid reads live on a deployment that never configured Twilio, instead
2345
+ // of one card for all three that could only ever be as live as its weakest.
2298
2346
  //
2299
2347
  // UNLIKE every public vendor, none of these appear in `requires` — see the
2300
2348
  // comment there. Availability and configuration are different questions, and a
2301
2349
  // missing CRM token must not take every base workflow step away.
2350
+ //
2351
+ // ponytail: the field keys keep the names they had on the shared row
2352
+ // (smsSid, hubspotToken) so the readers destructuring vendorSettings() did
2353
+ // not move. Rename them to sid/token when those readers are next touched.
2302
2354
  provider: {
2303
- fields: [
2304
- { input: "email", key: "accountSender", credential: "SENDGRID_FROM_ADDRESS", label: "Account sender", message: "Verification codes and security alerts send from here.", required: true },
2305
- { input: "password", key: "apiKey", credential: "SENDGRID_API_KEY", label: "SendGrid API key", message: "SendGrid dashboard, Settings, API Keys \u2014 full access for sends.", redact: true, required: true },
2306
- // NOT required. The CRM sync is best-effort internal tooling and no-ops
2307
- // without a token requiring it would make the whole drawbridge provider
2308
- // read not-live over something no merchant ever sees.
2309
- { input: "password", key: "hubspotToken", credential: "HUBSPOT_ACCESS_TOKEN", label: "HubSpot access token", message: "Drawbridge's own CRM portal. Internal \u2014 no merchant sees this.", redact: true },
2310
- // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
2311
- // either. Unset, it degrades to the account sender rather than
2312
- // refusing to start.
2313
- { 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." },
2314
- // NOT required, the three of them together. SendGrid alone is enough to
2315
- // run the platform — sign-in codes, notifications and every lead-facing
2316
- // mail are email. Requiring Twilio here made the whole drawbridge
2317
- // provider read not-live on a deployment that never sends SMS, over a
2318
- // channel it was not using.
2319
- //
2355
+ vendors: {
2356
+ sendgrid: {
2357
+ fields: [
2358
+ { input: "email", key: "accountSender", credential: "SENDGRID_FROM_ADDRESS", label: "Account sender", message: "Verification codes and security alerts send from here.", required: true },
2359
+ { input: "password", key: "apiKey", credential: "SENDGRID_API_KEY", label: "SendGrid API key", message: "SendGrid dashboard, Settings, API Keys \u2014 full access for sends.", redact: true, required: true },
2360
+ // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
2361
+ // either. Unset, it degrades to the account sender rather than
2362
+ // refusing to start.
2363
+ { 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." }
2364
+ ],
2365
+ icon: sendgrid_default,
2366
+ name: "SendGrid"
2367
+ },
2320
2368
  // Missing, SMS is LOCKED DOWN rather than broken: every send path
2321
2369
  // refuses cleanly (queue/notification.js cancels, the otc stream skips
2322
2370
  // and logs), the number-purchase routes deny, and identityDefaults.sms
2323
- // hides the org's SMS settings entirely.
2324
- { input: "text", key: "smsFrom", credential: "TWILIO_ACCOUNT_FROM", label: "SMS number", message: "Without Twilio credentials, SMS is disabled across the platform." },
2325
- { input: "password", key: "smsSid", credential: "TWILIO_ACCOUNT_SID", label: "Twilio account SID", message: "Twilio console home \u2014 the AC\u2026 identifier on the account dashboard.", redact: true },
2326
- { input: "password", key: "smsToken", credential: "TWILIO_AUTH_TOKEN", label: "Twilio auth token", message: "Beside the account SID on the Twilio console dashboard.", redact: true }
2327
- ]
2371
+ // hides the org's SMS settings entirely. The card reads not live, which
2372
+ // is the truth about Twilio and says nothing about email.
2373
+ twilio: {
2374
+ fields: [
2375
+ { input: "text", key: "smsFrom", credential: "TWILIO_ACCOUNT_FROM", label: "SMS number", message: "Without Twilio credentials, SMS is disabled across the platform.", required: true },
2376
+ { input: "password", key: "smsSid", credential: "TWILIO_ACCOUNT_SID", label: "Twilio account SID", message: "Twilio console home \u2014 the AC\u2026 identifier on the account dashboard.", redact: true, required: true },
2377
+ { 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 }
2378
+ ],
2379
+ icon: twilio_default,
2380
+ name: "Twilio"
2381
+ },
2382
+ // The CRM sync is best-effort internal tooling and no-ops without a
2383
+ // token. Not live here costs nothing a merchant can see.
2384
+ hubspot: {
2385
+ fields: [
2386
+ { input: "password", key: "hubspotToken", credential: "HUBSPOT_ACCESS_TOKEN", label: "HubSpot access token", message: "Drawbridge's own CRM portal. Internal \u2014 no merchant sees this.", redact: true, required: true }
2387
+ ],
2388
+ icon: hubspot_default,
2389
+ name: "HubSpot"
2390
+ }
2391
+ }
2328
2392
  },
2329
2393
  // NOTHING, and HUBSPOT_ACCESS_TOKEN in particular must not be here.
2330
2394
  //
@@ -2910,12 +2974,20 @@ var klaviyo_default2 = {
2910
2974
  // DRAWBRIDGE'S OWN CREDENTIALS for this vendor, as opposed to a merchant's —
2911
2975
  // what an admin types on the provider screen. Declared here rather than in a
2912
2976
  // table in lib/providers.js, so a vendor's credentials sit beside the
2913
- // `requires` that names the same variables.
2977
+ // `requires` that names the same variables. Keyed by the VENDOR that holds
2978
+ // them — here the vendor and the connection are the same thing, which is not
2979
+ // true of every manifest.
2914
2980
  provider: {
2915
- fields: [
2916
- { 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 },
2917
- { 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 }
2918
- ]
2981
+ vendors: {
2982
+ klaviyo: {
2983
+ fields: [
2984
+ { 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 },
2985
+ { 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 }
2986
+ ],
2987
+ icon: klaviyo_default,
2988
+ name: "Klaviyo"
2989
+ }
2990
+ }
2919
2991
  },
2920
2992
  requires: [
2921
2993
  "KLAVIYO_OAUTH_CLIENT_ID",
@@ -3258,10 +3330,16 @@ var mailchimp_default2 = {
3258
3330
  // what an admin types on the provider screen, beside the `requires` naming the
3259
3331
  // same variables.
3260
3332
  provider: {
3261
- fields: [
3262
- { input: "text", key: "clientId", credential: "MAILCHIMP_OAUTH_CLIENT_ID", label: "Client ID", message: "From our registered app \u2014 Mailchimp account, Extras, Registered apps.", required: true },
3263
- { 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 }
3264
- ]
3333
+ vendors: {
3334
+ mailchimp: {
3335
+ fields: [
3336
+ { input: "text", key: "clientId", credential: "MAILCHIMP_OAUTH_CLIENT_ID", label: "Client ID", message: "From our registered app \u2014 Mailchimp account, Extras, Registered apps.", required: true },
3337
+ { 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 }
3338
+ ],
3339
+ icon: mailchimp_default,
3340
+ name: "Mailchimp"
3341
+ }
3342
+ }
3265
3343
  },
3266
3344
  // The OAuth client this deployment registered. Without both, the vendor drops
3267
3345
  // out of availableConnections rather than offering a Connect button that
@@ -4498,28 +4576,34 @@ var shopify_default2 = {
4498
4576
  // name required by the manifest and enterable nowhere is a vendor that can
4499
4577
  // never go live from the admin screen.
4500
4578
  provider: {
4501
- fields: [
4502
- { input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", message: "The app's Client ID \u2014 Dev Dashboard, the app, Settings.", required: true },
4503
- { 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 },
4504
- { 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 },
4505
- { input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", message: 'Where "View on the Shopify App Store" sends merchants \u2014 the public listing, or the Dev Dashboard install link for the dev app.', required: true },
4506
- // The usage meter's event handle, when the plan config's meter ever
4507
- // changes: stored value overrides the built-in default
4508
- // (events.order.handle) at the mint point, and `format : 'slug'`
4509
- // normalizes it on save a handle can carry no spaces or capitals,
4510
- // and classification against it is case-sensitive. Optional: empty
4511
- // means the default, and the sender still refuses any handle that
4512
- // disagrees with the transaction it minted.
4513
- { format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", message: "Must match the usage meter's Handle field on the plan exactly \u2014 lowercase and hyphens. Leave empty to use drawbridge-orders.", required: false, setting: true },
4514
- // The Partner API credential set — the ONE place meter billing is
4515
- // observable (CHARGE_USAGE accrual events). Org id and numeric app
4516
- // id come from the Partner Dashboard; the token from Settings
4517
- // Partner API clients (Manage-apps permission). All optional: the
4518
- // accrual probe simply stays off until they exist.
4519
- { input: "text", key: "partnerOrgId", label: "Partner organization ID", message: "The number in the Partner Dashboard URL \u2014 partners.shopify.com/<this number>/\u2026 \u2014 NOT the admin organization_id from the app listing link.", required: false, setting: true },
4520
- { input: "text", key: "partnerAppId", label: "Partner app ID (numeric)", message: "The numeric id in the app's Partner Dashboard URL \u2014 not the Client ID.", required: false, setting: true },
4521
- { input: "password", key: "partnerToken", label: "Partner API token", message: "Partner Dashboard, Settings, Partner API clients \u2014 create one with Manage apps and paste the prtapi_ token.", redact: true, required: false, setting: true }
4522
- ]
4579
+ vendors: {
4580
+ shopify: {
4581
+ fields: [
4582
+ { input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", message: "The app's Client ID \u2014 Dev Dashboard, the app, Settings.", required: true },
4583
+ { 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 },
4584
+ { 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 },
4585
+ { input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", message: 'Where "View on the Shopify App Store" sends merchants \u2014 the public listing, or the Dev Dashboard install link for the dev app.', required: true },
4586
+ // The usage meter's event handle, when the plan config's meter ever
4587
+ // changes: stored value overrides the built-in default
4588
+ // (events.order.handle) at the mint point, and `format : 'slug'`
4589
+ // normalizes it on save a handle can carry no spaces or capitals,
4590
+ // and classification against it is case-sensitive. Optional: empty
4591
+ // means the default, and the sender still refuses any handle that
4592
+ // disagrees with the transaction it minted.
4593
+ { format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", message: "Must match the usage meter's Handle field on the plan exactly \u2014 lowercase and hyphens. Leave empty to use drawbridge-orders.", required: false, setting: true },
4594
+ // The Partner API credential set the ONE place meter billing is
4595
+ // observable (CHARGE_USAGE accrual events). Org id and numeric app
4596
+ // id come from the Partner Dashboard; the token from Settings →
4597
+ // Partner API clients (Manage-apps permission). All optional: the
4598
+ // accrual probe simply stays off until they exist.
4599
+ { input: "text", key: "partnerOrgId", label: "Partner organization ID", message: "The number in the Partner Dashboard URL \u2014 partners.shopify.com/<this number>/\u2026 \u2014 NOT the admin organization_id from the app listing link.", required: false, setting: true },
4600
+ { input: "text", key: "partnerAppId", label: "Partner app ID (numeric)", message: "The numeric id in the app's Partner Dashboard URL \u2014 not the Client ID.", required: false, setting: true },
4601
+ { input: "password", key: "partnerToken", label: "Partner API token", message: "Partner Dashboard, Settings, Partner API clients \u2014 create one with Manage apps and paste the prtapi_ token.", redact: true, required: false, setting: true }
4602
+ ],
4603
+ icon: shopify_default,
4604
+ name: "Shopify"
4605
+ }
4606
+ }
4523
4607
  },
4524
4608
  // A pre-launch integration: it only surfaces once the App Store listing
4525
4609
  // exists and the app is fully configured. Requiring all four means it can
@@ -5000,8 +5084,19 @@ var implemented = (hooks, path) => {
5000
5084
  var leaves = (node, path = []) => Object.entries(node || {}).flatMap(
5001
5085
  ([key, value]) => typeof value === "function" ? [[[...path, key].join("."), value]] : value && typeof value === "object" ? leaves(value, [...path, key]) : []
5002
5086
  );
5087
+ var checkIcon = (owner, icon) => {
5088
+ if (typeof icon !== "string" || !icon.includes("<svg")) {
5089
+ throw new Error(owner + " needs an icon \u2014 the svg markup itself, not a path to one");
5090
+ }
5091
+ if (!icon.includes("viewBox")) {
5092
+ throw new Error(owner + " icon has no viewBox, so it cannot scale");
5093
+ }
5094
+ if (icon.includes("<image")) {
5095
+ throw new Error(owner + " icon wraps a raster \u2014 re-export it as vector shapes");
5096
+ }
5097
+ };
5003
5098
  var build = (manifest) => {
5004
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t;
5099
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u;
5005
5100
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
5006
5101
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
5007
5102
  if (!(manifest == null ? void 0 : manifest.private) && !(manifest == null ? void 0 : manifest.feature)) throw new Error(manifest.slug + " needs a plan feature key");
@@ -5031,46 +5126,53 @@ var build = (manifest) => {
5031
5126
  }
5032
5127
  }
5033
5128
  }
5034
- for (const field2 of ((_c = manifest.provider) == null ? void 0 : _c.fields) || []) {
5035
- if (!(field2 == null ? void 0 : field2.key) || !(field2 == null ? void 0 : field2.label)) {
5036
- throw new Error(manifest.slug + " declares a provider field with no key or label");
5037
- }
5038
- if (!INPUTS.includes(field2.input)) {
5039
- throw new Error(manifest.slug + ".provider." + field2.key + " needs an input the admin form can render \u2014 one of " + INPUTS.join(", "));
5129
+ if ((_c = manifest.provider) == null ? void 0 : _c.fields) {
5130
+ throw new Error(manifest.slug + " declares provider.fields \u2014 fields live under provider.vendors.<vendor> now, keyed by the vendor that holds them");
5131
+ }
5132
+ const declaredKeys = /* @__PURE__ */ new Set();
5133
+ for (const [vendor, declared] of Object.entries(((_d = manifest.provider) == null ? void 0 : _d.vendors) || {})) {
5134
+ if (!(declared == null ? void 0 : declared.name)) throw new Error(manifest.slug + " declares vendor " + vendor + " with no name");
5135
+ checkIcon(manifest.slug + " vendor " + vendor, declared.icon);
5136
+ if (!Array.isArray(declared.fields) || !declared.fields.length) {
5137
+ throw new Error(manifest.slug + " declares vendor " + vendor + " with no fields \u2014 a vendor nothing is typed into holds nothing");
5040
5138
  }
5041
- if (field2.input === "password" && !field2.redact) {
5042
- throw new Error(manifest.slug + ".provider." + field2.key + " is a password and must declare redact : true \u2014 the api would hand the value back");
5139
+ for (const field2 of declared.fields) {
5140
+ if (!(field2 == null ? void 0 : field2.key) || !(field2 == null ? void 0 : field2.label)) {
5141
+ throw new Error(manifest.slug + " declares a provider field with no key or label");
5142
+ }
5143
+ if (!INPUTS.includes(field2.input)) {
5144
+ throw new Error(manifest.slug + ".provider." + field2.key + " needs an input the admin form can render \u2014 one of " + INPUTS.join(", "));
5145
+ }
5146
+ if (field2.input === "password" && !field2.redact) {
5147
+ throw new Error(manifest.slug + ".provider." + field2.key + " is a password and must declare redact : true \u2014 the api would hand the value back");
5148
+ }
5149
+ if (declaredKeys.has(field2.key)) {
5150
+ throw new Error(manifest.slug + " declares " + field2.key + " under two vendors \u2014 vendorSettings() merges them and one would win");
5151
+ }
5152
+ declaredKeys.add(field2.key);
5043
5153
  }
5044
5154
  }
5045
- if (typeof (manifest == null ? void 0 : manifest.icon) !== "string" || !manifest.icon.includes("<svg")) {
5046
- throw new Error(manifest.slug + " needs an icon \u2014 the svg markup itself, not a path to one");
5047
- }
5048
- if (!manifest.icon.includes("viewBox")) {
5049
- throw new Error(manifest.slug + " icon has no viewBox, so it cannot scale");
5050
- }
5051
- if (manifest.icon.includes("<image")) {
5052
- throw new Error(manifest.slug + " icon wraps a raster \u2014 re-export it as vector shapes");
5053
- }
5155
+ checkIcon(manifest.slug, manifest == null ? void 0 : manifest.icon);
5054
5156
  if (!GROUPS.includes(manifest == null ? void 0 : manifest.group)) {
5055
5157
  throw new Error(manifest.slug + " needs a group \u2014 one of " + GROUPS.join(", "));
5056
5158
  }
5057
- if ((_d = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _d.type) {
5159
+ if ((_e = manifest == null ? void 0 : manifest.connect) == null ? void 0 : _e.type) {
5058
5160
  throw new Error(manifest.slug + " declares connect.type \u2014 that is auth.type now");
5059
5161
  }
5060
- if (!AUTH_TYPES.includes((_e = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _e.type)) {
5162
+ if (!AUTH_TYPES.includes((_f = manifest == null ? void 0 : manifest.auth) == null ? void 0 : _f.type)) {
5061
5163
  throw new Error(manifest.slug + " needs auth.type \u2014 one of " + AUTH_TYPES.join(", "));
5062
5164
  }
5063
5165
  if (manifest.auth.type === "oauth") {
5064
5166
  for (const field2 of OAUTH_FIELDS) {
5065
- if (!((_f = manifest.auth.oauth) == null ? void 0 : _f[field2])) {
5167
+ if (!((_g = manifest.auth.oauth) == null ? void 0 : _g[field2])) {
5066
5168
  throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field2);
5067
5169
  }
5068
5170
  }
5069
- if (typeof ((_h = (_g = manifest.hooks) == null ? void 0 : _g.auth) == null ? void 0 : _h.token) !== "function") {
5171
+ if (typeof ((_i = (_h = manifest.hooks) == null ? void 0 : _h.auth) == null ? void 0 : _i.token) !== "function") {
5070
5172
  throw new Error(manifest.slug + " is oauth and must implement hooks.auth.token \u2014 point it at authToken() or wrap it");
5071
5173
  }
5072
5174
  for (const url of OAUTH_URLS) {
5073
- if (!((_j = (_i = manifest.auth.oauth) == null ? void 0 : _i.urls) == null ? void 0 : _j[url])) {
5175
+ if (!((_k = (_j = manifest.auth.oauth) == null ? void 0 : _j.urls) == null ? void 0 : _k[url])) {
5074
5176
  throw new Error(manifest.slug + " is oauth and must declare auth.oauth.urls." + url);
5075
5177
  }
5076
5178
  }
@@ -5080,16 +5182,16 @@ var build = (manifest) => {
5080
5182
  );
5081
5183
  }
5082
5184
  }
5083
- 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)) {
5185
+ if (implemented(manifest.hooks, "inbound.event") && typeof ((_m = (_l = manifest.hooks) == null ? void 0 : _l.inbound) == null ? void 0 : _m.event) !== "function" && !((_o = (_n = manifest.inbound) == null ? void 0 : _n.headers) == null ? void 0 : _o.event)) {
5084
5186
  throw new Error(manifest.slug + " implements inbound.event but declares no inbound.headers.event");
5085
5187
  }
5086
- if (implemented(manifest.hooks, "inbound.verify") && !((_p = (_o = manifest.inbound) == null ? void 0 : _o.headers) == null ? void 0 : _p.signature)) {
5188
+ if (implemented(manifest.hooks, "inbound.verify") && !((_q = (_p = manifest.inbound) == null ? void 0 : _p.headers) == null ? void 0 : _q.signature)) {
5087
5189
  throw new Error(manifest.slug + " implements inbound.verify but declares no inbound.headers.signature");
5088
5190
  }
5089
5191
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
5090
5192
  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");
5091
5193
  }
5092
- if (!Array.isArray((_q = manifest == null ? void 0 : manifest.content) == null ? void 0 : _q.guide) || !manifest.content.guide.length) {
5194
+ if (!Array.isArray((_r = manifest == null ? void 0 : manifest.content) == null ? void 0 : _r.guide) || !manifest.content.guide.length) {
5093
5195
  throw new Error(manifest.slug + " needs content.guide \u2014 an array of steps for its page");
5094
5196
  }
5095
5197
  if (typeof (manifest == null ? void 0 : manifest.status) !== "function") {
@@ -5101,8 +5203,8 @@ var build = (manifest) => {
5101
5203
  }
5102
5204
  for (const [domain, verbs] of Object.entries(HOOKS)) {
5103
5205
  for (const verb of verbs) {
5104
- const hook = (_s = (_r = manifest.hooks) == null ? void 0 : _r[domain]) == null ? void 0 : _s[verb];
5105
- if (((_t = manifest.hooks) == null ? void 0 : _t[domain]) === false) continue;
5206
+ const hook = (_t = (_s = manifest.hooks) == null ? void 0 : _s[domain]) == null ? void 0 : _t[verb];
5207
+ if (((_u = manifest.hooks) == null ? void 0 : _u[domain]) === false) continue;
5106
5208
  if (hook !== false && !implemented({ [domain]: { [verb]: hook } }, domain + "." + verb)) {
5107
5209
  throw new Error(manifest.slug + " must answer hooks." + domain + "." + verb + " \u2014 false, a function, or {} if another repo implements it");
5108
5210
  }
@@ -5152,6 +5254,44 @@ var connections = Object.freeze({
5152
5254
  shopify: build(shopify_default2),
5153
5255
  webhook: build(webhook_default)
5154
5256
  });
5257
+ var canonical = (field2) => JSON.stringify(Object.fromEntries(Object.entries(field2).sort()));
5258
+ var mergeVendors = (catalog) => {
5259
+ var _a;
5260
+ const merged = {};
5261
+ const claimed = /* @__PURE__ */ new Map();
5262
+ for (const [slug2, manifest] of Object.entries(catalog)) {
5263
+ for (const [vendor, declared] of Object.entries(((_a = manifest.provider) == null ? void 0 : _a.vendors) || {})) {
5264
+ const held = merged[vendor] || (merged[vendor] = { fields: [], icon: declared.icon, name: declared.name });
5265
+ if (held.name !== declared.name) {
5266
+ throw new Error(slug2 + " names vendor " + vendor + ' "' + declared.name + '" but another manifest names it "' + held.name + '"');
5267
+ }
5268
+ if (held.icon !== declared.icon) {
5269
+ throw new Error(slug2 + " declares a different icon for vendor " + vendor + " than another manifest \u2014 one vendor, one mark");
5270
+ }
5271
+ for (const field2 of declared.fields) {
5272
+ const existing = held.fields.find((entry) => entry.key === field2.key);
5273
+ if (existing) {
5274
+ if (canonical(existing) !== canonical(field2)) {
5275
+ throw new Error(slug2 + " declares " + vendor + "." + field2.key + " differently from another manifest \u2014 a field two connections spend is declared identically in both");
5276
+ }
5277
+ continue;
5278
+ }
5279
+ if (field2.credential) {
5280
+ const owner = claimed.get(field2.credential);
5281
+ if (owner) {
5282
+ throw new Error(field2.credential + " is claimed by both " + owner + " and " + vendor + "." + field2.key + " \u2014 one would silently overwrite the other");
5283
+ }
5284
+ claimed.set(field2.credential, vendor + "." + field2.key);
5285
+ }
5286
+ held.fields.push(field2);
5287
+ }
5288
+ }
5289
+ }
5290
+ return Object.freeze(Object.fromEntries(
5291
+ Object.entries(merged).map(([vendor, held]) => [vendor, Object.freeze({ ...held, fields: Object.freeze(held.fields) })])
5292
+ ));
5293
+ };
5294
+ var vendors = mergeVendors(connections);
5155
5295
  (() => {
5156
5296
  var _a;
5157
5297
  const routes = {};
@@ -5352,6 +5492,7 @@ var resolveConnection = (item, data2, env = {}) => {
5352
5492
  hookSupport,
5353
5493
  isStale,
5354
5494
  mergeSettings,
5495
+ mergeVendors,
5355
5496
  pkcePair,
5356
5497
  projectConnection,
5357
5498
  publicConnectionKeys,
@@ -5363,5 +5504,6 @@ var resolveConnection = (item, data2, env = {}) => {
5363
5504
  stepLabels,
5364
5505
  stepQueues,
5365
5506
  stepRoutes,
5366
- tokenSettings
5507
+ tokenSettings,
5508
+ vendors
5367
5509
  });