@drawbridge/drawbridge-utils 0.0.114 → 0.0.115

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.
@@ -62,6 +62,7 @@ __export(connections_exports, {
62
62
  scopesMessage: () => scopesMessage,
63
63
  stepLabels: () => stepLabels,
64
64
  stepQueues: () => stepQueues,
65
+ stepRoutes: () => stepRoutes,
65
66
  tokenSettings: () => tokenSettings
66
67
  });
67
68
  module.exports = __toCommonJS(connections_exports);
@@ -446,6 +447,181 @@ var accessToken = async ({
446
447
  return next.accessToken;
447
448
  };
448
449
 
450
+ // lib/connections/icons/attentive.js
451
+ var attentive_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
452
+ <rect width="500" height="500" fill="#FFD967"/>
453
+ <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"/>
454
+ <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"/>
455
+ </svg>`;
456
+
457
+ // lib/connections/attentive.js
458
+ var attentive_default2 = {
459
+ auth: {
460
+ oauth: {
461
+ // NAMES of the env vars holding OUR app's client — set at registration,
462
+ // never before. No `headers` on the client: Attentive takes credentials
463
+ // as form fields, which is the runner's default.
464
+ client: {
465
+ id: "ATTENTIVE_OAUTH_CLIENT_ID",
466
+ secret: "ATTENTIVE_OAUTH_CLIENT_SECRET"
467
+ },
468
+ urls: {
469
+ authorize: "https://ui.attentivemobile.com/integrations/oauth-install",
470
+ redirect: "/api/connection/attentive/callback",
471
+ token: "https://api.attentivemobile.com/v1/authorization-codes/tokens"
472
+ }
473
+ },
474
+ type: "oauth"
475
+ },
476
+ // EVERYTHING A MERCHANT READS. `errors` would live in here too — the
477
+ // connection DOCUMENT carries its own `errors` array and is spread OVER the
478
+ // resolved manifest downstream, so a top-level one would never render.
479
+ content: {
480
+ confirm: "Disconnecting revokes Drawbridge's access to your Attentive account. Your subscribers stay in both Attentive and Drawbridge \u2014 neither list is deleted.",
481
+ description: [
482
+ "Attentive is where your SMS marketing lives, and this connection is becoming the way your Drawbridge contacts sync into an Attentive segment.",
483
+ "You authorize Drawbridge from inside Attentive and can revoke that access there at any time. Drawbridge never sees or stores your Attentive password.",
484
+ "Subscriber syncing is not live yet, so connecting today does nothing except choose the segment it will use when it ships."
485
+ ],
486
+ excerpt: "Sync your Drawbridge contacts into an Attentive segment.",
487
+ guide: [
488
+ "Press Connect. Drawbridge sends you to Attentive to approve access.",
489
+ "Sign in to Attentive if you are not already, and authorize the permissions listed.",
490
+ "You are returned here \u2014 choose which Attentive segment your contacts should sync into."
491
+ ]
492
+ },
493
+ // A contact destination, like Klaviyo and Mailchimp — a merchant could
494
+ // reasonably keep several up to date at once.
495
+ exclusive: false,
496
+ feature: "organization:connection:attentive",
497
+ fields: [
498
+ {
499
+ input: "select",
500
+ key: "segment",
501
+ label: "Attentive segment",
502
+ message: "Contacts your campaigns collect are synced into this segment.",
503
+ hook: "resources.audiences",
504
+ required: true
505
+ // No `search : false` here, and that is a first: /v2/segments takes a
506
+ // `name` filter (partial match, cited above), so this picker searches
507
+ // the ACCOUNT — Klaviyo and Mailchimp can only match the fetched page.
508
+ }
509
+ ],
510
+ group: "contacts",
511
+ // A HOOK'S VALUE IS ITS ANSWER. The consent is stored and can be revoked;
512
+ // nothing else is built yet, because subscriber sync has not shipped. Every
513
+ // false here is "not yet" rather than "never" — when the sync lands, probe
514
+ // and contacts.sync are the first to flip.
515
+ hooks: {
516
+ auth: {
517
+ // The exchange already yields the tokens, and Attentive documents no
518
+ // account-identity endpoint to enrich them with — Klaviyo's connect
519
+ // reads the account name back; this has nothing cited to read. The
520
+ // callback stores the tokens and skips enrichment on `unimplemented`.
521
+ connect: {},
522
+ disconnect: {},
523
+ probe: false,
524
+ scopes: false,
525
+ // THE ONE THING WRAPPED, and it is about the response rather than the
526
+ // request. Attentive's token reply carries expires_in : 900 while their
527
+ // auth overview says access tokens "do not expire" — and no refresh
528
+ // token exists to renew with. Storing that expiry would have
529
+ // accessToken() declaring the credential spent fifteen minutes after
530
+ // consent, with nothing to renew it: every connection would demand
531
+ // reconnecting four times an hour.
532
+ //
533
+ // The overview's answer is modelled — the expiry is dropped, so the
534
+ // token is treated as long-lived. Registration item 1 in the header is
535
+ // the live exchange that proves this right or wrong.
536
+ token: async (args) => {
537
+ const minted = await authToken(args);
538
+ return { ...minted, expiresIn: null };
539
+ }
540
+ },
541
+ commerce: false,
542
+ contacts: { remove: false, sync: false },
543
+ email: false,
544
+ inbound: false,
545
+ lifecycle: false,
546
+ resources: {
547
+ // The segments a merchant can sync into, for the picker on their
548
+ // connection.
549
+ //
550
+ // GET /v2/segments (cited in the header). `limit` caps at 1000 in their
551
+ // own spec, defaulting to 20 — low enough that leaving it unset would
552
+ // show a picker quietly missing most of a real account. The response's
553
+ // only identifier is `externalId`, so an entry without one cannot be
554
+ // stored and is dropped.
555
+ audiences: async ({ cursor, fetcher = fetch, limit = 100, search, token }) => {
556
+ const query = new URLSearchParams({
557
+ limit: String(Math.min(limit, 1e3)),
558
+ ...cursor && { cursor },
559
+ ...(search == null ? void 0 : search.value) && { name: String(search.value).trim() }
560
+ });
561
+ const response = await fetcher(
562
+ "https://api.attentivemobile.com/v2/segments?" + query,
563
+ {
564
+ headers: { authorization: "Bearer " + token },
565
+ signal: AbortSignal.timeout(15e3)
566
+ }
567
+ );
568
+ if (!response.ok) {
569
+ throw Object.assign(
570
+ new Error("Attentive refused the request (" + response.status + ")"),
571
+ { status: response.status }
572
+ );
573
+ }
574
+ const body = await response.json();
575
+ return {
576
+ items: ((body == null ? void 0 : body.segments) || []).filter((segment) => segment == null ? void 0 : segment.externalId).map((segment) => ({ id: segment.externalId, title: (segment == null ? void 0 : segment.name) || segment.externalId })),
577
+ pageInfo: {
578
+ endCursor: (body == null ? void 0 : body.hasMore) ? (body == null ? void 0 : body.cursor) || null : null,
579
+ hasNextPage: Boolean(body == null ? void 0 : body.hasMore)
580
+ }
581
+ };
582
+ },
583
+ prices: false,
584
+ products: false,
585
+ promotions: false
586
+ },
587
+ segment: false,
588
+ sms: false,
589
+ webhook: false
590
+ },
591
+ icon: attentive_default,
592
+ requires: [
593
+ "ATTENTIVE_OAUTH_CLIENT_ID",
594
+ "ATTENTIVE_OAUTH_CLIENT_SECRET"
595
+ ],
596
+ slug: "attentive",
597
+ // A consent with no segment chosen is authenticated and inert — the sync,
598
+ // when it ships, needs somewhere to put people.
599
+ status: (data2) => {
600
+ var _a;
601
+ return ((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? data2.status : "pending";
602
+ },
603
+ // No steps: subscriber sync has not shipped, so this vendor contributes
604
+ // nothing to a workflow yet. An empty steps object is the honest declaration.
605
+ steps: {},
606
+ tasks: (data2) => {
607
+ var _a;
608
+ return [
609
+ ...((_a = data2 == null ? void 0 : data2.settings) == null ? void 0 : _a.segment) ? [] : [
610
+ {
611
+ message: "Choose which Attentive segment your contacts should sync into. Until you do, nothing is being synced.",
612
+ title: "Choose a segment"
613
+ }
614
+ ],
615
+ {
616
+ message: "Contact syncing to Attentive segments has not shipped yet. Nothing is being sent to Attentive right now.",
617
+ title: "Subscriber sync not available yet",
618
+ type: "warning"
619
+ }
620
+ ];
621
+ },
622
+ title: "Attentive"
623
+ };
624
+
449
625
  // lib/connections/icons/drawbridge.js
450
626
  var drawbridge_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
451
627
  <rect width="500" height="500" fill="#BAEC5F"/>
@@ -528,6 +704,21 @@ var field = {
528
704
  }
529
705
  };
530
706
  var connection = {
707
+ attentive: {
708
+ key: "organization:connection:attentive",
709
+ error: "Plan does not include Attentive connection",
710
+ feature: "Attentive connection"
711
+ },
712
+ // Klaviyo shipped without an entry here, which meant no plan GRANTED its
713
+ // key and the feature gate denied every non-admin request — a latent 403
714
+ // found while adding Attentive. getPlanFeature answers granted:false for a
715
+ // key absent from the plan's map, so a manifest feature key that appears in
716
+ // no plan list is a connection only admins can manage.
717
+ klaviyo: {
718
+ key: "organization:connection:klaviyo",
719
+ error: "Plan does not include Klaviyo connection",
720
+ feature: "Klaviyo connection"
721
+ },
531
722
  mailchimp: {
532
723
  key: "organization:connection:mailchimp",
533
724
  error: "Plan does not include Mailchimp connection",
@@ -705,6 +896,8 @@ var overage = (actionCents) => ({
705
896
  });
706
897
  var all = {
707
898
  features: (array = []) => featuresFor([
899
+ connection.attentive.key,
900
+ connection.klaviyo.key,
708
901
  connection.mailchimp.key,
709
902
  connection.sendgrid.key,
710
903
  connection.shopify.key,
@@ -2277,8 +2470,65 @@ var shopify_default2 = {
2277
2470
  // variant and arrives with it, so there is nothing for prices to answer
2278
2471
  // that products does not already.
2279
2472
  prices: false,
2280
- products: {},
2281
- promotions: {}
2473
+ // WHAT THE VENDOR ANSWERS, shaped for a picker. Both of these were api
2474
+ // ROUTES — /organization/:organization/shopify/products and
2475
+ // .../connection/:id/shopify/discounts — vendor-named urls in a service
2476
+ // that is supposed to have none, reachable only by knowing the path.
2477
+ // They are the same two questions every other vendor answers through
2478
+ // resources.*, so they answer them the same way now.
2479
+ //
2480
+ // `shopify` is INJECTED: this package cannot import @drawbridge/shopify,
2481
+ // which depends on it. What arrives is the SDK's pure HTTP namespaces
2482
+ // and nothing else — no controller, no collection access. Resolving the
2483
+ // credential is the caller's job because it is Drawbridge's job: the
2484
+ // admin token refreshes and writes itself back, which is service work,
2485
+ // not vendor work.
2486
+ products: async ({ cursor, limit = 100, search, settings, shopify, sort }) => {
2487
+ var _a, _b, _c, _d;
2488
+ const products = await shopify.storefront.getProducts({
2489
+ cursor,
2490
+ domain: settings == null ? void 0 : settings.domain,
2491
+ limit: Number(limit),
2492
+ search: (search == null ? void 0 : search.value) || null,
2493
+ sort,
2494
+ storefrontAccessToken: settings == null ? void 0 : settings.storefrontAccessToken
2495
+ });
2496
+ return {
2497
+ items: ((products == null ? void 0 : products.edges) || []).map((edge) => edge.node),
2498
+ pageInfo: {
2499
+ endCursor: ((_a = products == null ? void 0 : products.pageInfo) == null ? void 0 : _a.endCursor) || null,
2500
+ hasNextPage: Boolean((_b = products == null ? void 0 : products.pageInfo) == null ? void 0 : _b.hasNextPage),
2501
+ hasPreviousPage: Boolean((_c = products == null ? void 0 : products.pageInfo) == null ? void 0 : _c.hasPreviousPage),
2502
+ startCursor: ((_d = products == null ? void 0 : products.pageInfo) == null ? void 0 : _d.startCursor) || null
2503
+ }
2504
+ };
2505
+ },
2506
+ promotions: async ({ cursor, limit = 100, search, settings, shopify }) => {
2507
+ var _a, _b;
2508
+ const discounts = await shopify.admin.getDiscounts({
2509
+ adminAccessToken: settings == null ? void 0 : settings.adminAccessToken,
2510
+ cursor,
2511
+ domain: settings == null ? void 0 : settings.domain,
2512
+ limit: Number(limit),
2513
+ search: (search == null ? void 0 : search.value) || null
2514
+ });
2515
+ return {
2516
+ // The GLOBAL id is what Shopify returns and the bare id is what a
2517
+ // picker stores, which is why the tail is taken here rather than by
2518
+ // each caller that happened to remember.
2519
+ items: ((discounts == null ? void 0 : discounts.edges) || []).map((edge) => {
2520
+ var _a2, _b2, _c;
2521
+ return {
2522
+ id: String(((_a2 = edge == null ? void 0 : edge.node) == null ? void 0 : _a2.id) || "").split("/").pop(),
2523
+ title: (_c = (_b2 = edge == null ? void 0 : edge.node) == null ? void 0 : _b2.codeDiscount) == null ? void 0 : _c.title
2524
+ };
2525
+ }),
2526
+ pageInfo: {
2527
+ endCursor: ((_a = discounts == null ? void 0 : discounts.pageInfo) == null ? void 0 : _a.endCursor) || null,
2528
+ hasNextPage: Boolean((_b = discounts == null ? void 0 : discounts.pageInfo) == null ? void 0 : _b.hasNextPage)
2529
+ }
2530
+ };
2531
+ }
2282
2532
  },
2283
2533
  // Drawbridge posts to a merchant's own endpoint, never through a vendor.
2284
2534
  webhook: false
@@ -2875,6 +3125,7 @@ var stepLabels = (catalog = connections) => Object.fromEntries(
2875
3125
  )
2876
3126
  );
2877
3127
  var connections = Object.freeze({
3128
+ attentive: build(attentive_default2),
2878
3129
  drawbridge: build(drawbridge_default2),
2879
3130
  hubspot: build(hubspot_default),
2880
3131
  klaviyo: build(klaviyo_default2),
@@ -2956,6 +3207,15 @@ var runHook = async (slug, name, args = {}) => {
2956
3207
  var stepQueues = (env = {}) => Object.fromEntries(
2957
3208
  connectionSteps(env).map((step) => [step.type, step.queue])
2958
3209
  );
3210
+ var stepRoutes = (env = {}) => {
3211
+ const live = stepQueues(env);
3212
+ return Object.freeze({
3213
+ ...live,
3214
+ ...Object.fromEntries(
3215
+ Object.entries(RETIRED).filter(([, replacement]) => live[replacement]).map(([retired, replacement]) => [retired, live[replacement]])
3216
+ )
3217
+ });
3218
+ };
2959
3219
  var scopesMessage = "Shopify permissions are out of date. Open the Drawbridge app in your Shopify admin to approve the updated permissions.";
2960
3220
  var mergeSettings = ({ existing, incoming }) => {
2961
3221
  if (!existing || typeof existing !== "object") return incoming;
@@ -3064,5 +3324,6 @@ var resolveConnection = (item, data2, env = {}) => {
3064
3324
  scopesMessage,
3065
3325
  stepLabels,
3066
3326
  stepQueues,
3327
+ stepRoutes,
3067
3328
  tokenSettings
3068
3329
  });