@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.
@@ -496,6 +496,16 @@ var TRIGGERS = Object.freeze({
496
496
  "range.ended": Object.freeze({ event: "range.ended", type: "range" }),
497
497
  "range.started": Object.freeze({ event: "range.started", type: "range" }),
498
498
  "schedule.day": Object.freeze({ event: "day", type: "schedule" }),
499
+ // THE THREE SHOPIFY TRIGGERS, which were never in this vocabulary because the
500
+ // four workflows they fire were hand-written in drawbridge-sync's register job
501
+ // rather than provisioned from the manifest. `webhook` is a fourth trigger
502
+ // TYPE, and it is the honest name: these fire when the vendor calls us, which
503
+ // is neither an event Drawbridge raised nor a schedule it keeps.
504
+ "shopify.order.create": Object.freeze({ event: "shopify.order.create", type: "webhook" }),
505
+ "shopify.product.update": Object.freeze({ event: "shopify.product.update", type: "webhook" }),
506
+ // The token audit, dispatched programmatically by the register job when a
507
+ // store's grant is exchanged or rotated.
508
+ "shopify.token": Object.freeze({ event: "shopify.token", type: "event" }),
499
509
  "schedule.month": Object.freeze({ event: "month", type: "schedule" }),
500
510
  "schedule.week": Object.freeze({ event: "week", type: "schedule" }),
501
511
  "segment.contact.add": Object.freeze({ event: "segment.contact.add", type: "event" }),
@@ -548,7 +558,7 @@ var OUTCOMES = Object.freeze({
548
558
  unimplemented: "unimplemented",
549
559
  unsupported: "unsupported"
550
560
  });
551
- var STATUSES = Object.freeze(["active", "disconnected", "error", "pending"]);
561
+ var STATUSES = Object.freeze(["active", "error", "pending"]);
552
562
  var PLATFORM_CREDENTIALS = Object.freeze([
553
563
  // Seals a merchant's stored connection settings. The webhook connection is
554
564
  // gated on it because its whole credential is a secret WE mint and store, so
@@ -5529,7 +5539,7 @@ var getAppToken = async ({ clientId, clientSecret, fetcher }) => {
5529
5539
  return fetchAppToken({ clientId, clientSecret, fetcher });
5530
5540
  };
5531
5541
  var toShopGid = (shopId) => String(shopId).startsWith("gid://") ? String(shopId) : "gid://shopify/Shop/" + shopId;
5532
- var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference, revision, shopId, timestamp, value }) => {
5542
+ var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference: reference2, revision, shopId, timestamp, value }) => {
5533
5543
  if (!shopId || !eventHandle || !(Number(value) > 0)) {
5534
5544
  throw new Error("sendAppEvent requires shopId, eventHandle, and a positive value");
5535
5545
  }
@@ -5543,8 +5553,8 @@ var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempo
5543
5553
  const body = {
5544
5554
  attributes: {
5545
5555
  value: Number(value),
5546
- ...reference && {
5547
- reference: String(reference).slice(0, 128)
5556
+ ...reference2 && {
5557
+ reference: String(reference2).slice(0, 128)
5548
5558
  }
5549
5559
  },
5550
5560
  event_handle: eventHandle,
@@ -7705,6 +7715,12 @@ var shopify_default2 = {
7705
7715
  orderId: String(orderId),
7706
7716
  rate,
7707
7717
  shopId: connection.source.id,
7718
+ // WHICH VENDOR IS BEING CHARGED, so the sender does not have
7719
+ // to assume. queue/usage.js named the slug itself and
7720
+ // defaults to it for anything already queued when this
7721
+ // shipped — an app-store vendor billing through its own meter
7722
+ // enqueues the same job with its own slug.
7723
+ slug: connection.slug,
7708
7724
  // The App Events API returns no event id, so one is generated
7709
7725
  // here — the event handle plus the order id — and sent as the
7710
7726
  // event's `reference`. queue/usage.js stamps the same id onto
@@ -8928,6 +8944,13 @@ var shopify_default2 = {
8928
8944
  key: "Shopify Order Tracking",
8929
8945
  queue: "connection",
8930
8946
  system: true,
8947
+ // THE TRIGGER THE REGISTER JOB HAND-WROTE. It is byte-identical to
8948
+ // the one on every stored workflow, because these documents exist on
8949
+ // prod with thousands of runs behind them and provisioning is
8950
+ // idempotent on (connection, system, title) — a different trigger
8951
+ // here would not be picked up by the existing rows anyway, and a
8952
+ // different TITLE would provision a second workflow for every store.
8953
+ trigger: { event: "shopify.order.create", type: "webhook" },
8931
8954
  type: "step.commerce.order.record"
8932
8955
  })
8933
8956
  },
@@ -8938,6 +8961,15 @@ var shopify_default2 = {
8938
8961
  key: "Shopify Product Sync",
8939
8962
  queue: "connection",
8940
8963
  system: true,
8964
+ // `shopify.product.update`, NOT `product.insert`. The plan named the
8965
+ // latter, and it is the right long-term answer — a Drawbridge product
8966
+ // row appearing should pull the vendor's copy, rather than
8967
+ // stream/product.js branching on the slug and enqueueing a
8968
+ // vendor-named queue. But a workflow carries ONE trigger, this one is
8969
+ // stored on prod against the webhook, and changing a live workflow's
8970
+ // trigger is a data migration. So the declaration matches what exists
8971
+ // and `product.insert` stays a separate, migration-gated change.
8972
+ trigger: { event: "shopify.product.update", type: "webhook" },
8941
8973
  type: "step.commerce.product.sync"
8942
8974
  })
8943
8975
  }
@@ -8959,26 +8991,37 @@ var shopify_default2 = {
8959
8991
  type: "step.connection.health.check"
8960
8992
  })
8961
8993
  },
8962
- // Audit-only. The "Shopify Token Activity" system workflow lists these
8963
- // for descriptive grouping, but its audit step docs are written manually
8964
- // at OAuth time — the workflow is never dispatched. Routing is declared
8965
- // defensively so that if it ever IS dispatched, the job lands on a real
8966
- // queue and the handler lookup misses cleanly instead of throwing
8967
- // "Unknown step type".
8994
+ // THE ONE WORKFLOW THAT CARRIES TWO STEPS, and the shape the plan called
8995
+ // the single genuinely new thing in it.
8996
+ //
8997
+ // Every other system step is its own workflow, titled by its own `key`.
8998
+ // These two are "Shopify Token Activity", one row in the merchant's list
8999
+ // with both steps in it — so they declare `workflow`, which names the
9000
+ // title, the trigger and the ORDER, and the provisioner groups on it.
9001
+ // Without that key the pair could only be provisioned by the hand-written
9002
+ // spec in drawbridge-sync's register job, which is exactly why that spec
9003
+ // outlived every other one.
9004
+ //
9005
+ // AUDIT-ONLY. The step documents are written manually at OAuth time and
9006
+ // the workflow is never dispatched; `queue` is declared defensively so
9007
+ // that if it ever IS, the job lands on a real queue and the handler lookup
9008
+ // misses cleanly rather than throwing "Unknown step type".
8968
9009
  token: {
8969
9010
  exchange: () => ({
8970
9011
  description: "Records the token exchange that completed an install. Audit only \u2014 never dispatched.",
8971
9012
  key: "Shopify Token Exchange",
8972
9013
  queue: "connection",
8973
9014
  system: true,
8974
- type: "step.connection.token.exchange"
9015
+ type: "step.connection.token.exchange",
9016
+ workflow: { key: "Shopify Token Activity", order: 0, trigger: { event: "shopify.token", type: "event" } }
8975
9017
  }),
8976
9018
  refresh: () => ({
8977
9019
  description: "Records a token rotation. Audit only \u2014 never dispatched.",
8978
9020
  key: "Shopify Token Refresh",
8979
9021
  queue: "connection",
8980
9022
  system: true,
8981
- type: "step.connection.token.refresh"
9023
+ type: "step.connection.token.refresh",
9024
+ workflow: { key: "Shopify Token Activity", order: 1, trigger: { event: "shopify.token", type: "event" } }
8982
9025
  })
8983
9026
  }
8984
9027
  }
@@ -9434,7 +9477,7 @@ var webhook_default = {
9434
9477
  // The connect prompt only where connecting is not already the card's whole
9435
9478
  // story: a disconnected or errored document renders a Connect action itself,
9436
9479
  // and a task repeating it talks over the button.
9437
- tasks: ({ settings, status } = {}) => ["disconnected", "error"].includes(status) ? [] : (settings == null ? void 0 : settings.secret) ? [
9480
+ tasks: ({ settings, status } = {}) => status === "error" ? [] : (settings == null ? void 0 : settings.secret) ? [
9438
9481
  {
9439
9482
  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.',
9440
9483
  title: "Requests must be verified",
@@ -9521,6 +9564,77 @@ var attentive_default3 = {
9521
9564
  }
9522
9565
  };
9523
9566
 
9567
+ // lib/connections/icons/brightdata.js
9568
+ var brightdata_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
9569
+ <rect width="500" height="500" fill="#0F58FF"/>
9570
+ <circle cx="250" cy="250" r="52" fill="white"/>
9571
+ <rect x="234" y="70" width="32" height="96" rx="16" fill="white"/>
9572
+ <rect x="234" y="334" width="32" height="96" rx="16" fill="white"/>
9573
+ <rect x="70" y="234" width="96" height="32" rx="16" fill="white"/>
9574
+ <rect x="334" y="234" width="96" height="32" rx="16" fill="white"/>
9575
+ <rect x="117" y="140" width="32" height="96" rx="16" transform="rotate(-45 117 140)" fill="white"/>
9576
+ <rect x="304" y="327" width="32" height="96" rx="16" transform="rotate(-45 304 327)" fill="white"/>
9577
+ <rect x="360" y="117" width="32" height="96" rx="16" transform="rotate(45 360 117)" fill="white"/>
9578
+ <rect x="173" y="304" width="32" height="96" rx="16" transform="rotate(45 173 304)" fill="white"/>
9579
+ </svg>`;
9580
+
9581
+ // lib/connections/vendors/brightdata.js
9582
+ var brightdata_default2 = {
9583
+ fields: [
9584
+ {
9585
+ credential: "BRIGHTDATA_BROWSER_URI",
9586
+ input: "password",
9587
+ key: "browserUri",
9588
+ // A wss:// endpoint with the account's user and password in its
9589
+ // authority, which is why it is a password field rather than a url one:
9590
+ // the whole string is the credential.
9591
+ label: "Scraping Browser endpoint",
9592
+ message: "The wss:// Scraping Browser endpoint, carrying its own credentials. Used to render pages that need a real browser.",
9593
+ redact: true,
9594
+ required: true
9595
+ },
9596
+ {
9597
+ credential: "BRIGHTDATA_UNLOCKER_API_KEY",
9598
+ input: "password",
9599
+ key: "unlockerApiKey",
9600
+ label: "Web Unlocker API key",
9601
+ // NO EXPIRY FIELD, and that is a decision rather than an omission. Bright
9602
+ // Data offers an `Unlimited` expiry and recommends against it; Drawbridge
9603
+ // takes the unlimited key deliberately, because there is no way to rotate
9604
+ // this programmatically and a date nothing can act on is a date that only
9605
+ // surfaces after it has already broken scraping. If a fixed expiry is ever
9606
+ // taken instead, the field to add is a stored date and a warning ahead of
9607
+ // it — not a rotation, which the vendor cannot support.
9608
+ 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.",
9609
+ redact: true,
9610
+ required: true
9611
+ },
9612
+ {
9613
+ credential: "BRIGHTDATA_UNLOCKER_ZONE",
9614
+ input: "text",
9615
+ key: "unlockerZone",
9616
+ label: "Web Unlocker zone",
9617
+ // Not secret — a zone is a label on the account, and it is shown rather
9618
+ // than redacted so an operator can confirm which one is in use without
9619
+ // clearing the field to read it.
9620
+ message: "The zone name the Unlocker requests run against.",
9621
+ required: true
9622
+ }
9623
+ ],
9624
+ icon: brightdata_default,
9625
+ name: "Bright Data",
9626
+ slug: "brightdata",
9627
+ // WHERE THE FACTS CAME FROM, read 2026-09-16.
9628
+ urls: {
9629
+ api: "https://docs.brightdata.com/api-reference/authentication",
9630
+ dashboard: "https://brightdata.com/cp/setting/users",
9631
+ // Bright Data has no scope model — a key carries the account's own access —
9632
+ // so there is nothing to link. `false` is the recorded answer rather than a
9633
+ // missing key.
9634
+ scopes: false
9635
+ }
9636
+ };
9637
+
9524
9638
  // lib/connections/vendors/hubspot.js
9525
9639
  var hubspot_default2 = {
9526
9640
  // WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
@@ -9896,7 +10010,7 @@ var checkIcon = (owner, icon) => {
9896
10010
  }
9897
10011
  };
9898
10012
  var vendors = Object.freeze(Object.fromEntries(
9899
- [attentive_default3, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
10013
+ [attentive_default3, brightdata_default2, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
9900
10014
  ));
9901
10015
  var buildVendor = (vendor) => {
9902
10016
  if (!(vendor == null ? void 0 : vendor.slug)) throw new Error("A vendor needs a slug");
@@ -9923,7 +10037,7 @@ for (const [slug2, vendor] of Object.entries(vendors)) {
9923
10037
  buildVendor(vendor);
9924
10038
  }
9925
10039
  var build = (manifest) => {
9926
- 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;
10040
+ 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;
9927
10041
  if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
9928
10042
  if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
9929
10043
  if (manifest == null ? void 0 : manifest.private) throw new Error(manifest.slug + " declares private \u2014 that is feature : false now");
@@ -10189,11 +10303,22 @@ var build = (manifest) => {
10189
10303
  manifest.slug + " step " + type2 + " may be triggered by " + trigger + ", which is not a trigger \u2014 one of " + Object.keys(TRIGGERS).join(", ")
10190
10304
  );
10191
10305
  }
10192
- if (declared2.trigger) {
10193
- const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === declared2.trigger.event && TRIGGERS[key].type === declared2.trigger.type)];
10306
+ if (declared2.workflow) {
10307
+ if (!declared2.workflow.key) {
10308
+ throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no key \u2014 the title several steps share");
10309
+ }
10310
+ if (typeof declared2.workflow.order !== "number") {
10311
+ throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no order \u2014 two steps in one row need a sequence");
10312
+ }
10313
+ if (declared2.trigger) {
10314
+ 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");
10315
+ }
10316
+ }
10317
+ for (const candidate of [declared2.trigger, (_y = declared2.workflow) == null ? void 0 : _y.trigger].filter(Boolean)) {
10318
+ const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === candidate.event && TRIGGERS[key].type === candidate.type)];
10194
10319
  if (!known2) {
10195
10320
  throw new Error(
10196
- manifest.slug + " step " + type2 + " declares trigger { event : " + declared2.trigger.event + ", type : " + declared2.trigger.type + " }, which nothing dispatches"
10321
+ manifest.slug + " step " + type2 + " declares trigger { event : " + candidate.event + ", type : " + candidate.type + " }, which nothing dispatches"
10197
10322
  );
10198
10323
  }
10199
10324
  }
@@ -1,4 +1,4 @@
1
- import { a3 as vendors, J as mergeSettings, y as connections } from './index-C59xHago.cjs';
1
+ import { a8 as vendors, K as mergeSettings, y as connections } from './index-C2rxasGZ.cjs';
2
2
  import { decrypt, encrypt } from './encrypt.cjs';
3
3
  import { slugify } from './slugify.cjs';
4
4
  import './usage.cjs';
@@ -1,4 +1,4 @@
1
- import { a3 as vendors, J as mergeSettings, y as connections } from './index-B546oDNo.js';
1
+ import { a8 as vendors, K as mergeSettings, y as connections } from './index-DOYCXtd7.js';
2
2
  import { decrypt, encrypt } from './encrypt.js';
3
3
  import { slugify } from './slugify.js';
4
4
  import './usage.js';
package/dist/providers.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: connection.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: connection.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
  }
package/package.json CHANGED
@@ -256,5 +256,5 @@
256
256
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
257
257
  },
258
258
  "types": "dist/index.d.ts",
259
- "version": "0.0.177"
259
+ "version": "0.0.178"
260
260
  }