@drawbridge/drawbridge-utils 0.0.177 → 0.0.178

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/features.cjs CHANGED
@@ -487,6 +487,16 @@ var TRIGGERS = Object.freeze({
487
487
  "range.ended": Object.freeze({ event: "range.ended", type: "range" }),
488
488
  "range.started": Object.freeze({ event: "range.started", type: "range" }),
489
489
  "schedule.day": Object.freeze({ event: "day", type: "schedule" }),
490
+ // THE THREE SHOPIFY TRIGGERS, which were never in this vocabulary because the
491
+ // four workflows they fire were hand-written in drawbridge-sync's register job
492
+ // rather than provisioned from the manifest. `webhook` is a fourth trigger
493
+ // TYPE, and it is the honest name: these fire when the vendor calls us, which
494
+ // is neither an event Drawbridge raised nor a schedule it keeps.
495
+ "shopify.order.create": Object.freeze({ event: "shopify.order.create", type: "webhook" }),
496
+ "shopify.product.update": Object.freeze({ event: "shopify.product.update", type: "webhook" }),
497
+ // The token audit, dispatched programmatically by the register job when a
498
+ // store's grant is exchanged or rotated.
499
+ "shopify.token": Object.freeze({ event: "shopify.token", type: "event" }),
490
500
  "schedule.month": Object.freeze({ event: "month", type: "schedule" }),
491
501
  "schedule.week": Object.freeze({ event: "week", type: "schedule" }),
492
502
  "segment.contact.add": Object.freeze({ event: "segment.contact.add", type: "event" }),
@@ -539,7 +549,7 @@ var OUTCOMES = Object.freeze({
539
549
  unimplemented: "unimplemented",
540
550
  unsupported: "unsupported"
541
551
  });
542
- var STATUSES = Object.freeze(["active", "disconnected", "error", "pending"]);
552
+ var STATUSES = Object.freeze(["active", "error", "pending"]);
543
553
  var PLATFORM_CREDENTIALS = Object.freeze([
544
554
  // Seals a merchant's stored connection settings. The webhook connection is
545
555
  // gated on it because its whole credential is a secret WE mint and store, so
@@ -5520,7 +5530,7 @@ var getAppToken = async ({ clientId, clientSecret, fetcher }) => {
5520
5530
  return fetchAppToken({ clientId, clientSecret, fetcher });
5521
5531
  };
5522
5532
  var toShopGid = (shopId) => String(shopId).startsWith("gid://") ? String(shopId) : "gid://shopify/Shop/" + shopId;
5523
- var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference, revision, shopId, timestamp, value }) => {
5533
+ var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference: reference2, revision, shopId, timestamp, value }) => {
5524
5534
  if (!shopId || !eventHandle || !(Number(value) > 0)) {
5525
5535
  throw new Error("sendAppEvent requires shopId, eventHandle, and a positive value");
5526
5536
  }
@@ -5534,8 +5544,8 @@ var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempo
5534
5544
  const body = {
5535
5545
  attributes: {
5536
5546
  value: Number(value),
5537
- ...reference && {
5538
- reference: String(reference).slice(0, 128)
5547
+ ...reference2 && {
5548
+ reference: String(reference2).slice(0, 128)
5539
5549
  }
5540
5550
  },
5541
5551
  event_handle: eventHandle,
@@ -7696,6 +7706,12 @@ var shopify_default2 = {
7696
7706
  orderId: String(orderId),
7697
7707
  rate,
7698
7708
  shopId: connection2.source.id,
7709
+ // WHICH VENDOR IS BEING CHARGED, so the sender does not have
7710
+ // to assume. queue/usage.js named the slug itself and
7711
+ // defaults to it for anything already queued when this
7712
+ // shipped — an app-store vendor billing through its own meter
7713
+ // enqueues the same job with its own slug.
7714
+ slug: connection2.slug,
7699
7715
  // The App Events API returns no event id, so one is generated
7700
7716
  // here — the event handle plus the order id — and sent as the
7701
7717
  // event's `reference`. queue/usage.js stamps the same id onto
@@ -8919,6 +8935,13 @@ var shopify_default2 = {
8919
8935
  key: "Shopify Order Tracking",
8920
8936
  queue: "connection",
8921
8937
  system: true,
8938
+ // THE TRIGGER THE REGISTER JOB HAND-WROTE. It is byte-identical to
8939
+ // the one on every stored workflow, because these documents exist on
8940
+ // prod with thousands of runs behind them and provisioning is
8941
+ // idempotent on (connection, system, title) — a different trigger
8942
+ // here would not be picked up by the existing rows anyway, and a
8943
+ // different TITLE would provision a second workflow for every store.
8944
+ trigger: { event: "shopify.order.create", type: "webhook" },
8922
8945
  type: "step.commerce.order.record"
8923
8946
  })
8924
8947
  },
@@ -8929,6 +8952,15 @@ var shopify_default2 = {
8929
8952
  key: "Shopify Product Sync",
8930
8953
  queue: "connection",
8931
8954
  system: true,
8955
+ // `shopify.product.update`, NOT `product.insert`. The plan named the
8956
+ // latter, and it is the right long-term answer — a Drawbridge product
8957
+ // row appearing should pull the vendor's copy, rather than
8958
+ // stream/product.js branching on the slug and enqueueing a
8959
+ // vendor-named queue. But a workflow carries ONE trigger, this one is
8960
+ // stored on prod against the webhook, and changing a live workflow's
8961
+ // trigger is a data migration. So the declaration matches what exists
8962
+ // and `product.insert` stays a separate, migration-gated change.
8963
+ trigger: { event: "shopify.product.update", type: "webhook" },
8932
8964
  type: "step.commerce.product.sync"
8933
8965
  })
8934
8966
  }
@@ -8950,26 +8982,37 @@ var shopify_default2 = {
8950
8982
  type: "step.connection.health.check"
8951
8983
  })
8952
8984
  },
8953
- // Audit-only. The "Shopify Token Activity" system workflow lists these
8954
- // for descriptive grouping, but its audit step docs are written manually
8955
- // at OAuth time — the workflow is never dispatched. Routing is declared
8956
- // defensively so that if it ever IS dispatched, the job lands on a real
8957
- // queue and the handler lookup misses cleanly instead of throwing
8958
- // "Unknown step type".
8985
+ // THE ONE WORKFLOW THAT CARRIES TWO STEPS, and the shape the plan called
8986
+ // the single genuinely new thing in it.
8987
+ //
8988
+ // Every other system step is its own workflow, titled by its own `key`.
8989
+ // These two are "Shopify Token Activity", one row in the merchant's list
8990
+ // with both steps in it — so they declare `workflow`, which names the
8991
+ // title, the trigger and the ORDER, and the provisioner groups on it.
8992
+ // Without that key the pair could only be provisioned by the hand-written
8993
+ // spec in drawbridge-sync's register job, which is exactly why that spec
8994
+ // outlived every other one.
8995
+ //
8996
+ // AUDIT-ONLY. The step documents are written manually at OAuth time and
8997
+ // the workflow is never dispatched; `queue` is declared defensively so
8998
+ // that if it ever IS, the job lands on a real queue and the handler lookup
8999
+ // misses cleanly rather than throwing "Unknown step type".
8959
9000
  token: {
8960
9001
  exchange: () => ({
8961
9002
  description: "Records the token exchange that completed an install. Audit only \u2014 never dispatched.",
8962
9003
  key: "Shopify Token Exchange",
8963
9004
  queue: "connection",
8964
9005
  system: true,
8965
- type: "step.connection.token.exchange"
9006
+ type: "step.connection.token.exchange",
9007
+ workflow: { key: "Shopify Token Activity", order: 0, trigger: { event: "shopify.token", type: "event" } }
8966
9008
  }),
8967
9009
  refresh: () => ({
8968
9010
  description: "Records a token rotation. Audit only \u2014 never dispatched.",
8969
9011
  key: "Shopify Token Refresh",
8970
9012
  queue: "connection",
8971
9013
  system: true,
8972
- type: "step.connection.token.refresh"
9014
+ type: "step.connection.token.refresh",
9015
+ workflow: { key: "Shopify Token Activity", order: 1, trigger: { event: "shopify.token", type: "event" } }
8973
9016
  })
8974
9017
  }
8975
9018
  }
@@ -9425,7 +9468,7 @@ var webhook_default = {
9425
9468
  // The connect prompt only where connecting is not already the card's whole
9426
9469
  // story: a disconnected or errored document renders a Connect action itself,
9427
9470
  // and a task repeating it talks over the button.
9428
- tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
9471
+ tasks: ({ settings, status } = {}) => status === "error" ? [] : (settings == null ? void 0 : settings.secret) ? [
9429
9472
  {
9430
9473
  message: 'Compute HMAC-SHA256( secret, t + "." + raw body ) and constant-time compare it against a v1 in the X-Drawbridge-Signature header; reject timestamps older than five minutes.',
9431
9474
  title: "Requests must be verified",
@@ -9512,6 +9555,77 @@ var attentive_default3 = {
9512
9555
  }
9513
9556
  };
9514
9557
 
9558
+ // lib/connections/icons/brightdata.js
9559
+ var brightdata_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
9560
+ <rect width="500" height="500" fill="#0F58FF"/>
9561
+ <circle cx="250" cy="250" r="52" fill="white"/>
9562
+ <rect x="234" y="70" width="32" height="96" rx="16" fill="white"/>
9563
+ <rect x="234" y="334" width="32" height="96" rx="16" fill="white"/>
9564
+ <rect x="70" y="234" width="96" height="32" rx="16" fill="white"/>
9565
+ <rect x="334" y="234" width="96" height="32" rx="16" fill="white"/>
9566
+ <rect x="117" y="140" width="32" height="96" rx="16" transform="rotate(-45 117 140)" fill="white"/>
9567
+ <rect x="304" y="327" width="32" height="96" rx="16" transform="rotate(-45 304 327)" fill="white"/>
9568
+ <rect x="360" y="117" width="32" height="96" rx="16" transform="rotate(45 360 117)" fill="white"/>
9569
+ <rect x="173" y="304" width="32" height="96" rx="16" transform="rotate(45 173 304)" fill="white"/>
9570
+ </svg>`;
9571
+
9572
+ // lib/connections/vendors/brightdata.js
9573
+ var brightdata_default2 = {
9574
+ fields: [
9575
+ {
9576
+ credential: "BRIGHTDATA_BROWSER_URI",
9577
+ input: "password",
9578
+ key: "browserUri",
9579
+ // A wss:// endpoint with the account's user and password in its
9580
+ // authority, which is why it is a password field rather than a url one:
9581
+ // the whole string is the credential.
9582
+ label: "Scraping Browser endpoint",
9583
+ message: "The wss:// Scraping Browser endpoint, carrying its own credentials. Used to render pages that need a real browser.",
9584
+ redact: true,
9585
+ required: true
9586
+ },
9587
+ {
9588
+ credential: "BRIGHTDATA_UNLOCKER_API_KEY",
9589
+ input: "password",
9590
+ key: "unlockerApiKey",
9591
+ label: "Web Unlocker API key",
9592
+ // NO EXPIRY FIELD, and that is a decision rather than an omission. Bright
9593
+ // Data offers an `Unlimited` expiry and recommends against it; Drawbridge
9594
+ // takes the unlimited key deliberately, because there is no way to rotate
9595
+ // this programmatically and a date nothing can act on is a date that only
9596
+ // surfaces after it has already broken scraping. If a fixed expiry is ever
9597
+ // taken instead, the field to add is a stored date and a warning ahead of
9598
+ // it — not a rotation, which the vendor cannot support.
9599
+ message: "Bright Data account settings \u2192 API keys. Taken with an unlimited expiry: their API offers no way to rotate a key, so a dated one can only ever break scraping silently.",
9600
+ redact: true,
9601
+ required: true
9602
+ },
9603
+ {
9604
+ credential: "BRIGHTDATA_UNLOCKER_ZONE",
9605
+ input: "text",
9606
+ key: "unlockerZone",
9607
+ label: "Web Unlocker zone",
9608
+ // Not secret — a zone is a label on the account, and it is shown rather
9609
+ // than redacted so an operator can confirm which one is in use without
9610
+ // clearing the field to read it.
9611
+ message: "The zone name the Unlocker requests run against.",
9612
+ required: true
9613
+ }
9614
+ ],
9615
+ icon: brightdata_default,
9616
+ name: "Bright Data",
9617
+ slug: "brightdata",
9618
+ // WHERE THE FACTS CAME FROM, read 2026-09-16.
9619
+ urls: {
9620
+ api: "https://docs.brightdata.com/api-reference/authentication",
9621
+ dashboard: "https://brightdata.com/cp/setting/users",
9622
+ // Bright Data has no scope model — a key carries the account's own access —
9623
+ // so there is nothing to link. `false` is the recorded answer rather than a
9624
+ // missing key.
9625
+ scopes: false
9626
+ }
9627
+ };
9628
+
9515
9629
  // lib/connections/vendors/hubspot.js
9516
9630
  var hubspot_default2 = {
9517
9631
  // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
@@ -9887,7 +10001,7 @@ var checkIcon = (owner, icon) => {
9887
10001
  }
9888
10002
  };
9889
10003
  var vendors = Object.freeze(Object.fromEntries(
9890
- [attentive_default3, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
10004
+ [attentive_default3, brightdata_default2, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
9891
10005
  ));
9892
10006
  var buildVendor = (vendor) => {
9893
10007
  if (!(vendor == null ? void 0 : vendor.slug)) throw new Error("A vendor needs a slug");
@@ -9914,7 +10028,7 @@ for (const [slug2, vendor] of Object.entries(vendors)) {
9914
10028
  buildVendor(vendor);
9915
10029
  }
9916
10030
  var build = (manifest) => {
9917
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
10031
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
9918
10032
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
9919
10033
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
9920
10034
  if (manifest == null ? void 0 : manifest.private) throw new Error(manifest.slug + " declares private \u2014 that is feature : false now");
@@ -10180,11 +10294,22 @@ var build = (manifest) => {
10180
10294
  manifest.slug + " step " + type2 + " may be triggered by " + trigger + ", which is not a trigger \u2014 one of " + Object.keys(TRIGGERS).join(", ")
10181
10295
  );
10182
10296
  }
10183
- if (declared2.trigger) {
10184
- const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === declared2.trigger.event && TRIGGERS[key].type === declared2.trigger.type)];
10297
+ if (declared2.workflow) {
10298
+ if (!declared2.workflow.key) {
10299
+ throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no key \u2014 the title several steps share");
10300
+ }
10301
+ if (typeof declared2.workflow.order !== "number") {
10302
+ throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no order \u2014 two steps in one row need a sequence");
10303
+ }
10304
+ if (declared2.trigger) {
10305
+ throw new Error(manifest.slug + " step " + type2 + " declares BOTH a trigger and a workflow \u2014 the workflow carries the trigger for every step in it");
10306
+ }
10307
+ }
10308
+ for (const candidate of [declared2.trigger, (_y = declared2.workflow) == null ? void 0 : _y.trigger].filter(Boolean)) {
10309
+ const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === candidate.event && TRIGGERS[key].type === candidate.type)];
10185
10310
  if (!known2) {
10186
10311
  throw new Error(
10187
- manifest.slug + " step " + type2 + " declares trigger { event : " + declared2.trigger.event + ", type : " + declared2.trigger.type + " }, which nothing dispatches"
10312
+ manifest.slug + " step " + type2 + " declares trigger { event : " + candidate.event + ", type : " + candidate.type + " }, which nothing dispatches"
10188
10313
  );
10189
10314
  }
10190
10315
  }
@@ -1,4 +1,4 @@
1
- import { y as connections } from './index-C59xHago.cjs';
1
+ import { y as connections } from './index-C2rxasGZ.cjs';
2
2
  import './usage.cjs';
3
3
  import './encrypt.cjs';
4
4
  import 'crypto';
@@ -1,4 +1,4 @@
1
- import { y as connections } from './index-B546oDNo.js';
1
+ import { y as connections } from './index-DOYCXtd7.js';
2
2
  import './usage.js';
3
3
  import './encrypt.js';
4
4
  import 'crypto';
package/dist/features.js CHANGED
@@ -453,6 +453,16 @@ var TRIGGERS = Object.freeze({
453
453
  "range.ended": Object.freeze({ event: "range.ended", type: "range" }),
454
454
  "range.started": Object.freeze({ event: "range.started", type: "range" }),
455
455
  "schedule.day": Object.freeze({ event: "day", type: "schedule" }),
456
+ // THE THREE SHOPIFY TRIGGERS, which were never in this vocabulary because the
457
+ // four workflows they fire were hand-written in drawbridge-sync's register job
458
+ // rather than provisioned from the manifest. `webhook` is a fourth trigger
459
+ // TYPE, and it is the honest name: these fire when the vendor calls us, which
460
+ // is neither an event Drawbridge raised nor a schedule it keeps.
461
+ "shopify.order.create": Object.freeze({ event: "shopify.order.create", type: "webhook" }),
462
+ "shopify.product.update": Object.freeze({ event: "shopify.product.update", type: "webhook" }),
463
+ // The token audit, dispatched programmatically by the register job when a
464
+ // store's grant is exchanged or rotated.
465
+ "shopify.token": Object.freeze({ event: "shopify.token", type: "event" }),
456
466
  "schedule.month": Object.freeze({ event: "month", type: "schedule" }),
457
467
  "schedule.week": Object.freeze({ event: "week", type: "schedule" }),
458
468
  "segment.contact.add": Object.freeze({ event: "segment.contact.add", type: "event" }),
@@ -505,7 +515,7 @@ var OUTCOMES = Object.freeze({
505
515
  unimplemented: "unimplemented",
506
516
  unsupported: "unsupported"
507
517
  });
508
- var STATUSES = Object.freeze(["active", "disconnected", "error", "pending"]);
518
+ var STATUSES = Object.freeze(["active", "error", "pending"]);
509
519
  var PLATFORM_CREDENTIALS = Object.freeze([
510
520
  // Seals a merchant's stored connection settings. The webhook connection is
511
521
  // gated on it because its whole credential is a secret WE mint and store, so
@@ -5486,7 +5496,7 @@ var getAppToken = async ({ clientId, clientSecret, fetcher }) => {
5486
5496
  return fetchAppToken({ clientId, clientSecret, fetcher });
5487
5497
  };
5488
5498
  var toShopGid = (shopId) => String(shopId).startsWith("gid://") ? String(shopId) : "gid://shopify/Shop/" + shopId;
5489
- var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference, revision, shopId, timestamp, value }) => {
5499
+ var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference: reference2, revision, shopId, timestamp, value }) => {
5490
5500
  if (!shopId || !eventHandle || !(Number(value) > 0)) {
5491
5501
  throw new Error("sendAppEvent requires shopId, eventHandle, and a positive value");
5492
5502
  }
@@ -5500,8 +5510,8 @@ var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempo
5500
5510
  const body = {
5501
5511
  attributes: {
5502
5512
  value: Number(value),
5503
- ...reference && {
5504
- reference: String(reference).slice(0, 128)
5513
+ ...reference2 && {
5514
+ reference: String(reference2).slice(0, 128)
5505
5515
  }
5506
5516
  },
5507
5517
  event_handle: eventHandle,
@@ -7662,6 +7672,12 @@ var shopify_default2 = {
7662
7672
  orderId: String(orderId),
7663
7673
  rate,
7664
7674
  shopId: connection2.source.id,
7675
+ // WHICH VENDOR IS BEING CHARGED, so the sender does not have
7676
+ // to assume. queue/usage.js named the slug itself and
7677
+ // defaults to it for anything already queued when this
7678
+ // shipped — an app-store vendor billing through its own meter
7679
+ // enqueues the same job with its own slug.
7680
+ slug: connection2.slug,
7665
7681
  // The App Events API returns no event id, so one is generated
7666
7682
  // here — the event handle plus the order id — and sent as the
7667
7683
  // event's `reference`. queue/usage.js stamps the same id onto
@@ -8885,6 +8901,13 @@ var shopify_default2 = {
8885
8901
  key: "Shopify Order Tracking",
8886
8902
  queue: "connection",
8887
8903
  system: true,
8904
+ // THE TRIGGER THE REGISTER JOB HAND-WROTE. It is byte-identical to
8905
+ // the one on every stored workflow, because these documents exist on
8906
+ // prod with thousands of runs behind them and provisioning is
8907
+ // idempotent on (connection, system, title) — a different trigger
8908
+ // here would not be picked up by the existing rows anyway, and a
8909
+ // different TITLE would provision a second workflow for every store.
8910
+ trigger: { event: "shopify.order.create", type: "webhook" },
8888
8911
  type: "step.commerce.order.record"
8889
8912
  })
8890
8913
  },
@@ -8895,6 +8918,15 @@ var shopify_default2 = {
8895
8918
  key: "Shopify Product Sync",
8896
8919
  queue: "connection",
8897
8920
  system: true,
8921
+ // `shopify.product.update`, NOT `product.insert`. The plan named the
8922
+ // latter, and it is the right long-term answer — a Drawbridge product
8923
+ // row appearing should pull the vendor's copy, rather than
8924
+ // stream/product.js branching on the slug and enqueueing a
8925
+ // vendor-named queue. But a workflow carries ONE trigger, this one is
8926
+ // stored on prod against the webhook, and changing a live workflow's
8927
+ // trigger is a data migration. So the declaration matches what exists
8928
+ // and `product.insert` stays a separate, migration-gated change.
8929
+ trigger: { event: "shopify.product.update", type: "webhook" },
8898
8930
  type: "step.commerce.product.sync"
8899
8931
  })
8900
8932
  }
@@ -8916,26 +8948,37 @@ var shopify_default2 = {
8916
8948
  type: "step.connection.health.check"
8917
8949
  })
8918
8950
  },
8919
- // Audit-only. The "Shopify Token Activity" system workflow lists these
8920
- // for descriptive grouping, but its audit step docs are written manually
8921
- // at OAuth time — the workflow is never dispatched. Routing is declared
8922
- // defensively so that if it ever IS dispatched, the job lands on a real
8923
- // queue and the handler lookup misses cleanly instead of throwing
8924
- // "Unknown step type".
8951
+ // THE ONE WORKFLOW THAT CARRIES TWO STEPS, and the shape the plan called
8952
+ // the single genuinely new thing in it.
8953
+ //
8954
+ // Every other system step is its own workflow, titled by its own `key`.
8955
+ // These two are "Shopify Token Activity", one row in the merchant's list
8956
+ // with both steps in it — so they declare `workflow`, which names the
8957
+ // title, the trigger and the ORDER, and the provisioner groups on it.
8958
+ // Without that key the pair could only be provisioned by the hand-written
8959
+ // spec in drawbridge-sync's register job, which is exactly why that spec
8960
+ // outlived every other one.
8961
+ //
8962
+ // AUDIT-ONLY. The step documents are written manually at OAuth time and
8963
+ // the workflow is never dispatched; `queue` is declared defensively so
8964
+ // that if it ever IS, the job lands on a real queue and the handler lookup
8965
+ // misses cleanly rather than throwing "Unknown step type".
8925
8966
  token: {
8926
8967
  exchange: () => ({
8927
8968
  description: "Records the token exchange that completed an install. Audit only \u2014 never dispatched.",
8928
8969
  key: "Shopify Token Exchange",
8929
8970
  queue: "connection",
8930
8971
  system: true,
8931
- type: "step.connection.token.exchange"
8972
+ type: "step.connection.token.exchange",
8973
+ workflow: { key: "Shopify Token Activity", order: 0, trigger: { event: "shopify.token", type: "event" } }
8932
8974
  }),
8933
8975
  refresh: () => ({
8934
8976
  description: "Records a token rotation. Audit only \u2014 never dispatched.",
8935
8977
  key: "Shopify Token Refresh",
8936
8978
  queue: "connection",
8937
8979
  system: true,
8938
- type: "step.connection.token.refresh"
8980
+ type: "step.connection.token.refresh",
8981
+ workflow: { key: "Shopify Token Activity", order: 1, trigger: { event: "shopify.token", type: "event" } }
8939
8982
  })
8940
8983
  }
8941
8984
  }
@@ -9391,7 +9434,7 @@ var webhook_default = {
9391
9434
  // The connect prompt only where connecting is not already the card's whole
9392
9435
  // story: a disconnected or errored document renders a Connect action itself,
9393
9436
  // and a task repeating it talks over the button.
9394
- tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
9437
+ tasks: ({ settings, status } = {}) => status === "error" ? [] : (settings == null ? void 0 : settings.secret) ? [
9395
9438
  {
9396
9439
  message: 'Compute HMAC-SHA256( secret, t + "." + raw body ) and constant-time compare it against a v1 in the X-Drawbridge-Signature header; reject timestamps older than five minutes.',
9397
9440
  title: "Requests must be verified",
@@ -9478,6 +9521,77 @@ var attentive_default3 = {
9478
9521
  }
9479
9522
  };
9480
9523
 
9524
+ // lib/connections/icons/brightdata.js
9525
+ var brightdata_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
9526
+ <rect width="500" height="500" fill="#0F58FF"/>
9527
+ <circle cx="250" cy="250" r="52" fill="white"/>
9528
+ <rect x="234" y="70" width="32" height="96" rx="16" fill="white"/>
9529
+ <rect x="234" y="334" width="32" height="96" rx="16" fill="white"/>
9530
+ <rect x="70" y="234" width="96" height="32" rx="16" fill="white"/>
9531
+ <rect x="334" y="234" width="96" height="32" rx="16" fill="white"/>
9532
+ <rect x="117" y="140" width="32" height="96" rx="16" transform="rotate(-45 117 140)" fill="white"/>
9533
+ <rect x="304" y="327" width="32" height="96" rx="16" transform="rotate(-45 304 327)" fill="white"/>
9534
+ <rect x="360" y="117" width="32" height="96" rx="16" transform="rotate(45 360 117)" fill="white"/>
9535
+ <rect x="173" y="304" width="32" height="96" rx="16" transform="rotate(45 173 304)" fill="white"/>
9536
+ </svg>`;
9537
+
9538
+ // lib/connections/vendors/brightdata.js
9539
+ var brightdata_default2 = {
9540
+ fields: [
9541
+ {
9542
+ credential: "BRIGHTDATA_BROWSER_URI",
9543
+ input: "password",
9544
+ key: "browserUri",
9545
+ // A wss:// endpoint with the account's user and password in its
9546
+ // authority, which is why it is a password field rather than a url one:
9547
+ // the whole string is the credential.
9548
+ label: "Scraping Browser endpoint",
9549
+ message: "The wss:// Scraping Browser endpoint, carrying its own credentials. Used to render pages that need a real browser.",
9550
+ redact: true,
9551
+ required: true
9552
+ },
9553
+ {
9554
+ credential: "BRIGHTDATA_UNLOCKER_API_KEY",
9555
+ input: "password",
9556
+ key: "unlockerApiKey",
9557
+ label: "Web Unlocker API key",
9558
+ // NO EXPIRY FIELD, and that is a decision rather than an omission. Bright
9559
+ // Data offers an `Unlimited` expiry and recommends against it; Drawbridge
9560
+ // takes the unlimited key deliberately, because there is no way to rotate
9561
+ // this programmatically and a date nothing can act on is a date that only
9562
+ // surfaces after it has already broken scraping. If a fixed expiry is ever
9563
+ // taken instead, the field to add is a stored date and a warning ahead of
9564
+ // it — not a rotation, which the vendor cannot support.
9565
+ message: "Bright Data account settings \u2192 API keys. Taken with an unlimited expiry: their API offers no way to rotate a key, so a dated one can only ever break scraping silently.",
9566
+ redact: true,
9567
+ required: true
9568
+ },
9569
+ {
9570
+ credential: "BRIGHTDATA_UNLOCKER_ZONE",
9571
+ input: "text",
9572
+ key: "unlockerZone",
9573
+ label: "Web Unlocker zone",
9574
+ // Not secret — a zone is a label on the account, and it is shown rather
9575
+ // than redacted so an operator can confirm which one is in use without
9576
+ // clearing the field to read it.
9577
+ message: "The zone name the Unlocker requests run against.",
9578
+ required: true
9579
+ }
9580
+ ],
9581
+ icon: brightdata_default,
9582
+ name: "Bright Data",
9583
+ slug: "brightdata",
9584
+ // WHERE THE FACTS CAME FROM, read 2026-09-16.
9585
+ urls: {
9586
+ api: "https://docs.brightdata.com/api-reference/authentication",
9587
+ dashboard: "https://brightdata.com/cp/setting/users",
9588
+ // Bright Data has no scope model — a key carries the account's own access —
9589
+ // so there is nothing to link. `false` is the recorded answer rather than a
9590
+ // missing key.
9591
+ scopes: false
9592
+ }
9593
+ };
9594
+
9481
9595
  // lib/connections/vendors/hubspot.js
9482
9596
  var hubspot_default2 = {
9483
9597
  // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
@@ -9853,7 +9967,7 @@ var checkIcon = (owner, icon) => {
9853
9967
  }
9854
9968
  };
9855
9969
  var vendors = Object.freeze(Object.fromEntries(
9856
- [attentive_default3, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
9970
+ [attentive_default3, brightdata_default2, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
9857
9971
  ));
9858
9972
  var buildVendor = (vendor) => {
9859
9973
  if (!(vendor == null ? void 0 : vendor.slug)) throw new Error("A vendor needs a slug");
@@ -9880,7 +9994,7 @@ for (const [slug2, vendor] of Object.entries(vendors)) {
9880
9994
  buildVendor(vendor);
9881
9995
  }
9882
9996
  var build = (manifest) => {
9883
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
9997
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
9884
9998
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
9885
9999
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
9886
10000
  if (manifest == null ? void 0 : manifest.private) throw new Error(manifest.slug + " declares private \u2014 that is feature : false now");
@@ -10146,11 +10260,22 @@ var build = (manifest) => {
10146
10260
  manifest.slug + " step " + type2 + " may be triggered by " + trigger + ", which is not a trigger \u2014 one of " + Object.keys(TRIGGERS).join(", ")
10147
10261
  );
10148
10262
  }
10149
- if (declared2.trigger) {
10150
- const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === declared2.trigger.event && TRIGGERS[key].type === declared2.trigger.type)];
10263
+ if (declared2.workflow) {
10264
+ if (!declared2.workflow.key) {
10265
+ throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no key \u2014 the title several steps share");
10266
+ }
10267
+ if (typeof declared2.workflow.order !== "number") {
10268
+ throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no order \u2014 two steps in one row need a sequence");
10269
+ }
10270
+ if (declared2.trigger) {
10271
+ throw new Error(manifest.slug + " step " + type2 + " declares BOTH a trigger and a workflow \u2014 the workflow carries the trigger for every step in it");
10272
+ }
10273
+ }
10274
+ for (const candidate of [declared2.trigger, (_y = declared2.workflow) == null ? void 0 : _y.trigger].filter(Boolean)) {
10275
+ const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === candidate.event && TRIGGERS[key].type === candidate.type)];
10151
10276
  if (!known2) {
10152
10277
  throw new Error(
10153
- manifest.slug + " step " + type2 + " declares trigger { event : " + declared2.trigger.event + ", type : " + declared2.trigger.type + " }, which nothing dispatches"
10278
+ manifest.slug + " step " + type2 + " declares trigger { event : " + candidate.event + ", type : " + candidate.type + " }, which nothing dispatches"
10154
10279
  );
10155
10280
  }
10156
10281
  }