@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.
@@ -935,6 +935,16 @@ const TRIGGERS = Object.freeze({
935
935
  'range.ended' : Object.freeze({ event : 'range.ended', type : 'range' }),
936
936
  'range.started' : Object.freeze({ event : 'range.started', type : 'range' }),
937
937
  'schedule.day' : Object.freeze({ event : 'day', type : 'schedule' }),
938
+ // THE THREE SHOPIFY TRIGGERS, which were never in this vocabulary because the
939
+ // four workflows they fire were hand-written in drawbridge-sync's register job
940
+ // rather than provisioned from the manifest. `webhook` is a fourth trigger
941
+ // TYPE, and it is the honest name: these fire when the vendor calls us, which
942
+ // is neither an event Drawbridge raised nor a schedule it keeps.
943
+ 'shopify.order.create' : Object.freeze({ event : 'shopify.order.create', type : 'webhook' }),
944
+ 'shopify.product.update' : Object.freeze({ event : 'shopify.product.update', type : 'webhook' }),
945
+ // The token audit, dispatched programmatically by the register job when a
946
+ // store's grant is exchanged or rotated.
947
+ 'shopify.token' : Object.freeze({ event : 'shopify.token', type : 'event' }),
938
948
  'schedule.month' : Object.freeze({ event : 'month', type : 'schedule' }),
939
949
  'schedule.week' : Object.freeze({ event : 'week', type : 'schedule' }),
940
950
  'segment.contact.add' : Object.freeze({ event : 'segment.contact.add', type : 'event' }),
@@ -1050,7 +1060,7 @@ const HOOK_NAMES = Object.freeze( names( HOOKS ) );
1050
1060
  // matched on it. A fifth outcome that no producer produces and no consumer reads
1051
1061
  // is a branch somebody writes one day for a case that cannot happen.
1052
1062
  //
1053
- // Not to be confused with STATUSES.disconnected below, which is a CONNECTION's
1063
+ // Not to be confused with a CONNECTION's own status, which is
1054
1064
  // state and stays until a count says it can go.
1055
1065
  const OUTCOMES = Object.freeze({
1056
1066
  answered : 'answered',
@@ -1076,7 +1086,13 @@ const OUTCOMES = Object.freeze({
1076
1086
  // NO `incomplete`. That is an ORGANIZATION status meaning outstanding invoices,
1077
1087
  // and reusing the word for a connection would leave nobody able to say which
1078
1088
  // object a status referred to.
1079
- const STATUSES = Object.freeze([ 'active', 'disconnected', 'error', 'pending' ]);
1089
+ //
1090
+ // NO `disconnected` EITHER, as of 2026-09-13. Nothing wrote it — drawbridge-api
1091
+ // DELETES a connection on disconnect — and its removal was gated on a count
1092
+ // rather than on the reading, because a value a live document still holds cannot
1093
+ // leave a strict validator without rejecting that document on its next write.
1094
+ // Counted read-only on both clusters the day it went: 0 on dev, 0 on prod.
1095
+ const STATUSES = Object.freeze([ 'active', 'error', 'pending' ]);
1080
1096
 
1081
1097
  // HOW A VENDOR IS CONNECTED — SAID BY THE SHAPE, not by an enum beside it.
1082
1098
  //
@@ -1296,18 +1312,16 @@ const bearing = ( entry ) => ERROR_SOURCES[ entry?.source ] !== false;
1296
1312
  // checked status alone, so saving a workflow flipped it active against a
1297
1313
  // connection sync immediately re-errored.
1298
1314
  //
1299
- // TERMINAL STATES ARE NOT REASSESSED. `disconnected` is a merchant's decision,
1300
- // and a passing health check proves a credential works — never that somebody
1301
- // wants the connection. (Nothing writes it today: the api DELETES a connection on
1302
- // disconnect. It stays in the vocabulary, and stays honoured here, until a count
1303
- // on both clusters says it can leave.)
1304
- const assess = ({ errors = [], status = null } = {}) => {
1305
-
1306
- if( status === 'disconnected' ) return status;
1307
-
1308
- return ( errors || [] ).some( bearing ) ? 'error' : 'active';
1309
-
1310
- };
1315
+ // NO TERMINAL STATE TO HONOUR ANY MORE. This used to return `disconnected`
1316
+ // untouched, on the reasoning that a passing health check proves a credential
1317
+ // works and never that somebody wants the connection. True but nothing ever
1318
+ // wrote that status, because drawbridge-api deletes a connection on disconnect,
1319
+ // and it left the vocabulary once a count on both clusters read zero.
1320
+ //
1321
+ // So every status this can see is derived, and it derives all of them.
1322
+ const assess = ({ errors = [] } = {}) => (
1323
+ ( errors || [] ).some( bearing ) ? 'error' : 'active'
1324
+ );
1311
1325
 
1312
1326
  // WHAT SURVIVES A PASS THAT PROVED SOMETHING. A health run proves the credential,
1313
1327
  // so it may clear `oauth` and whatever else it judged — and must leave every
@@ -1643,6 +1657,54 @@ const systemSteps = ( node, path = [] ) => Object.entries( node || {} ).flatMap(
1643
1657
  }
1644
1658
  );
1645
1659
 
1660
+ // THE WORKFLOWS A MANIFEST DECLARES, as data and without writing any of them.
1661
+ //
1662
+ // SHARED WITH THE MIGRATION THAT REPORTS WHAT IS MISSING. drawbridge-api's
1663
+ // provisionSystemWorkflows had its own copy of this walk so a dry run could say
1664
+ // what it WOULD create without creating it — and the copy fell behind the moment
1665
+ // a workflow could carry several steps: it keyed titles off each step's own
1666
+ // `key`, so a grouped pair looked like two workflows named after steps rather
1667
+ // than one named after the grouping. Two walks over the same manifests is
1668
+ // exactly the drift this package exists to remove, so there is one.
1669
+ //
1670
+ // ONE ROW PER TITLE, and several steps may share one. A step that declares
1671
+ // `workflow` takes its title, its trigger and its position from there; every
1672
+ // other step is a workflow of one, titled by its own key.
1673
+ //
1674
+ // NO TRIGGER IS A DECISION, and it is honoured here: a system step with neither
1675
+ // its own trigger nor a workflow carrying one is declared so a stray dispatch
1676
+ // lands on a real queue, and provisioning a row for it would put something in
1677
+ // the merchant's list that never runs.
1678
+ //
1679
+ // SORTED BY DECLARED ORDER, because a workflow of several runs them in sequence
1680
+ // and the tree walk is alphabetical, which is not meaningful.
1681
+ const systemWorkflows = ( manifest ) => {
1682
+
1683
+ const grouped = new Map();
1684
+
1685
+ for( const [ type, declared ] of systemSteps( manifest?.steps ) ){
1686
+
1687
+ const trigger = declared.workflow?.trigger || declared.trigger;
1688
+
1689
+ if( ! trigger?.event || ! trigger?.type ) continue;
1690
+
1691
+ const title = declared.workflow?.key || declared.key;
1692
+
1693
+ if( ! title ) continue;
1694
+
1695
+ if( ! grouped.has( title ) ) grouped.set( title, { steps : [], title, trigger });
1696
+
1697
+ grouped.get( title ).steps.push({ order : declared.workflow?.order ?? 0, type });
1698
+
1699
+ }
1700
+
1701
+ return [ ...grouped.values() ].map( ( workflow ) => ({
1702
+ ...workflow,
1703
+ steps : workflow.steps.slice().sort( ( a, b ) => a.order - b.order )
1704
+ }) );
1705
+
1706
+ };
1707
+
1646
1708
  // THE WORKFLOWS A CONNECTION OWNS, provisioned from the manifest rather than
1647
1709
  // from a branch per step type. A vendor that declares a new system step with a
1648
1710
  // trigger gets its workflow with no edit here, which is the whole point of the
@@ -1661,8 +1723,13 @@ const systemSteps = ( node, path = [] ) => Object.entries( node || {} ).flatMap(
1661
1723
  // { connection, system, title } where system is true refuses every create but
1662
1724
  // the first, and the catch below is what lets a loser carry on.
1663
1725
  //
1664
- // Shopify is excluded: its register job creates the same workflow with three
1665
- // siblings, and racing it here would write a duplicate.
1726
+ // SHOPIFY IS NO LONGER EXCLUDED. It was, because its register job hand-wrote the
1727
+ // same four workflows and racing it here would write duplicates — and the reason
1728
+ // that spec could not simply be deleted is that one of its four, "Shopify Token
1729
+ // Activity", carries TWO steps, which a declaration had no way to express. It
1730
+ // does now: a step may declare `workflow`, naming the title, the trigger and its
1731
+ // order, and the grouping below provisions one row for all the steps that share
1732
+ // a key. The hand-written spec is gone with it.
1666
1733
  //
1667
1734
  // `connections` is a parameter whose default is the real registry. The seam is
1668
1735
  // there so this module's behaviour can be tested against a manifest shape — a
@@ -1671,7 +1738,7 @@ const systemSteps = ( node, path = [] ) => Object.entries( node || {} ).flatMap(
1671
1738
  // to be installed. Production callers pass nothing and get the real thing.
1672
1739
  const ensureSystemWorkflows = async ({ connections : registry = connections, controller, doc }) => {
1673
1740
 
1674
- if( ! doc?.id || doc.slug === 'shopify' ) return [];
1741
+ if( ! doc?.id ) return [];
1675
1742
 
1676
1743
  const manifest = registry[ doc.slug ];
1677
1744
 
@@ -1679,17 +1746,7 @@ const ensureSystemWorkflows = async ({ connections : registry = connections, con
1679
1746
 
1680
1747
  const provisioned = [];
1681
1748
 
1682
- for( const [ type, declared ] of systemSteps( manifest.steps ) ){
1683
-
1684
- // NO TRIGGER IS A DECISION. Shopify's token-audit steps are declared so a
1685
- // stray dispatch lands on a real queue, and are never fired — provisioning
1686
- // a workflow for them would put a row in the merchant's list for something
1687
- // that never runs.
1688
- if( ! declared.trigger?.event || ! declared.trigger?.type ) continue;
1689
-
1690
- const title = declared.key;
1691
-
1692
- if( ! title ) continue;
1749
+ for( const { steps : members, title, trigger } of systemWorkflows( manifest ) ){
1693
1750
 
1694
1751
  const existing = await controller.count({
1695
1752
  collection : 'workflow',
@@ -1709,13 +1766,10 @@ const ensureSystemWorkflows = async ({ connections : registry = connections, con
1709
1766
  connection : doc.id,
1710
1767
  organization : doc.organization,
1711
1768
  status : 'active',
1712
- steps : [ {
1713
- settings : {},
1714
- type
1715
- } ],
1769
+ steps : members.map( ( { type } ) => ({ settings : {}, type }) ),
1716
1770
  system : true,
1717
1771
  title,
1718
- trigger : declared.trigger
1772
+ trigger
1719
1773
  };
1720
1774
 
1721
1775
  let created;
@@ -1754,6 +1808,213 @@ const ensureSystemWorkflows = async ({ connections : registry = connections, con
1754
1808
 
1755
1809
  };
1756
1810
 
1811
+ // THE FACTS OF THE REGISTRY, AS MARKDOWN.
1812
+ //
1813
+ // `drawbridge-docs/reference/connection-hooks.md` carried a support matrix under
1814
+ // the footnote "Generated from the manifests in drawbridge-utils — if this table
1815
+ // and the package disagree, the package is right."
1816
+ //
1817
+ // IT WAS NOT GENERATED, AND THEY DISAGREED. Measured 2026-09-12, before this
1818
+ // existed: four columns against six manifests, seventeen rows against
1819
+ // thirty-one hook slots, and THREE OF THE THIRTEEN checkable rows were factually
1820
+ // wrong — auth.connect documented `yes` for two vendors that decline it,
1821
+ // auth.disconnect `yes` for three that decline it, auth.scopes `no` for Klaviyo,
1822
+ // which implements it. Plus twenty-seven references to a `catalog.*` domain that
1823
+ // had been called `resources.*` for two renames.
1824
+ //
1825
+ // So the facts are emitted from the registry and the doc includes them. THE
1826
+ // PROSE AROUND THEM STAYS HAND-WRITTEN: the reasoning about why
1827
+ // `resources.prices` is declined everywhere is worth a person's words. Only the
1828
+ // facts rot, so only the facts are generated.
1829
+ //
1830
+ // A test asserts that regenerating produces no diff — which is the check that
1831
+ // footnote always claimed and nobody ever ran.
1832
+
1833
+ const MARKER = {
1834
+ end : '<!-- /generated -->',
1835
+ start : '<!-- generated: drawbridge-utils lib/connections/reference.js — do not edit by hand -->'
1836
+ };
1837
+
1838
+ const slugs = () => Object.keys( connections ).sort();
1839
+
1840
+ // Every hook slot, in the vocabulary's own order, as dotted names. HOOKS nests
1841
+ // arbitrarily deep — `notification.email.send`, `install.organizations.add` —
1842
+ // and the depth is part of the name.
1843
+ const slots = ( node = HOOKS, path = [] ) => Object.entries( node ).flatMap(
1844
+ ( [ key, value ] ) => (
1845
+ Array.isArray( value )
1846
+ ? value.map( ( verb ) => [ ...path, key, verb ].join( '.' ) )
1847
+ : slots( value, [ ...path, key ] )
1848
+ )
1849
+ );
1850
+
1851
+ const implemented$1 = ( hooks, name ) => {
1852
+
1853
+ const found = name.split( '.' ).reduce(
1854
+ ( node, key ) => ( node && typeof node === 'object' && Object.hasOwn( node, key ) ) ? node[ key ] : undefined,
1855
+ hooks
1856
+ );
1857
+
1858
+ if( typeof found === 'function' ) return 'yes';
1859
+
1860
+ // `{}` IS A THIRD ANSWER, not a yes: declared here, implemented in the repo
1861
+ // holding the dependencies. Collapsing it into `yes` is how a doc tells a
1862
+ // reader the package can do something it cannot.
1863
+ if( found && typeof found === 'object' ) return 'elsewhere';
1864
+
1865
+ return 'no';
1866
+
1867
+ };
1868
+
1869
+ const table = ( header, rows ) => [
1870
+ '| ' + header.join( ' | ' ) + ' |',
1871
+ '|' + header.map( () => '---' ).join( '|' ) + '|',
1872
+ ...rows.map( ( row ) => '| ' + row.join( ' | ' ) + ' |' )
1873
+ ].join( '\n' );
1874
+
1875
+ const matrix = () => {
1876
+
1877
+ const columns = slugs();
1878
+
1879
+ return table(
1880
+ [ 'Hook', ...columns ],
1881
+ slots().map( ( name ) => [
1882
+ '`' + name + '`',
1883
+ ...columns.map( ( slug ) => implemented$1( connections[ slug ].hooks, name ) )
1884
+ ] )
1885
+ );
1886
+
1887
+ };
1888
+
1889
+ const vocabularies = () => {
1890
+
1891
+ const list = ( values ) => values.map( ( value ) => '`' + value + '`' ).join( ', ' );
1892
+
1893
+ return table(
1894
+ [ 'Vocabulary', 'Values' ],
1895
+ [
1896
+ [ '`AUTH_KINDS`', list( AUTH_KINDS ) ],
1897
+ [ '`GROUPS`', list( GROUPS ) ],
1898
+ [ '`INPUTS`', list( INPUTS ) ],
1899
+ [ '`STATUSES`', list( STATUSES ) ],
1900
+ [ '`TRIGGERS`', list( Object.keys( TRIGGERS ).sort() ) ],
1901
+ [ '`TRIGGER_TYPES`', list( TRIGGER_TYPES ) ]
1902
+ ]
1903
+ );
1904
+
1905
+ };
1906
+
1907
+ // Every step in the registry, with the hook it calls and what fires it. A step
1908
+ // with NO hook is real and stays visible: Shopify's token-audit pair is declared
1909
+ // for routing and never dispatched.
1910
+ const steps = () => {
1911
+
1912
+ const rows = [];
1913
+
1914
+ const walk = ( node, path, slug ) => {
1915
+
1916
+ if( ! node || typeof node !== 'object' ) return;
1917
+
1918
+ for( const [ key, value ] of Object.entries( node ) ){
1919
+
1920
+ if( typeof value === 'function' ){
1921
+
1922
+ let declared;
1923
+
1924
+ try { declared = value({}); } catch { continue; }
1925
+
1926
+ if( ! declared?.type ) continue;
1927
+
1928
+ const fires = declared.workflow?.trigger || declared.trigger;
1929
+
1930
+ rows.push([
1931
+ '`' + declared.type + '`',
1932
+ slug,
1933
+ declared.hook ? '`' + declared.hook + '`' : '—',
1934
+ declared.queue,
1935
+ declared.system ? 'system' : declared.withdrawn ? 'withdrawn' : 'builder',
1936
+ fires ? '`' + fires.event + '` / `' + fires.type + '`' : ( declared.triggers || [] ).map( ( t ) => '`' + t + '`' ).join( ', ' ) || '—'
1937
+ ]);
1938
+
1939
+ continue;
1940
+
1941
+ }
1942
+
1943
+ walk( value, [ ...path, key ], slug );
1944
+
1945
+ }
1946
+
1947
+ };
1948
+
1949
+ for( const slug of slugs() ) walk( connections[ slug ].steps, [], slug );
1950
+
1951
+ return table( [ 'Step', 'Connection', 'Hook', 'Queue', 'Shape', 'Fired by' ], rows.sort( ( a, b ) => a[ 0 ].localeCompare( b[ 0 ] ) || a[ 1 ].localeCompare( b[ 1 ] ) ) );
1952
+
1953
+ };
1954
+
1955
+ // The workflows a connection is provisioned with, which is the other half of
1956
+ // "what runs on its own": a step is only reachable if something fires it.
1957
+ const workflows = () => table(
1958
+ [ 'Workflow', 'Connection', 'Steps', 'Trigger' ],
1959
+ slugs().flatMap( ( slug ) => systemWorkflows( connections[ slug ] ).map( ( workflow ) => [
1960
+ workflow.title,
1961
+ slug,
1962
+ workflow.steps.map( ( step ) => '`' + step.type + '`' ).join( ' → ' ),
1963
+ '`' + workflow.trigger.event + '` / `' + workflow.trigger.type + '`'
1964
+ ] ) )
1965
+ );
1966
+
1967
+ const reference = () => [
1968
+ MARKER.start,
1969
+ '',
1970
+ '### The support matrix',
1971
+ '',
1972
+ '`yes` is a body in this package. `elsewhere` is declared here and implemented in the repo holding the dependencies — a different fact from `no`, and collapsing the two is how a table tells a reader the package can do something it cannot.',
1973
+ '',
1974
+ matrix(),
1975
+ '',
1976
+ '### The closed vocabularies',
1977
+ '',
1978
+ vocabularies(),
1979
+ '',
1980
+ '### Every step',
1981
+ '',
1982
+ 'A step with no hook is real: Shopify\'s token-audit pair is declared so a stray dispatch lands on a real queue, and is never fired.',
1983
+ '',
1984
+ steps(),
1985
+ '',
1986
+ '### The system workflows a connection is provisioned with',
1987
+ '',
1988
+ 'Idempotent on (connection, system, title), so the TITLE is load-bearing — one that varied by account would provision a second workflow every time it changed.',
1989
+ '',
1990
+ workflows(),
1991
+ '',
1992
+ '### Counts',
1993
+ '',
1994
+ '- **' + slugs().length + '** connections',
1995
+ '- **' + slots().length + '** hook slots',
1996
+ '- **' + Object.keys( STEPS ).length + '** step types',
1997
+ '- **' + Object.keys( TRIGGERS ).length + '** triggers',
1998
+ '',
1999
+ MARKER.end
2000
+ ].join( '\n' );
2001
+
2002
+ // Splice the generated block into a document, replacing whatever is between the
2003
+ // markers. Returns the document unchanged when it carries no markers, so a
2004
+ // caller can tell a doc that opted in from one that did not.
2005
+ const spliced = ( document ) => {
2006
+
2007
+ const from = document.indexOf( MARKER.start );
2008
+ const to = document.indexOf( MARKER.end );
2009
+
2010
+ if( from < 0 || to < 0 ) return document;
2011
+
2012
+ return document.slice( 0, from ) + reference() + document.slice( to + MARKER.end.length );
2013
+
2014
+ };
2015
+
2016
+ const MARKERS = MARKER;
2017
+
1757
2018
  // A LIVE ACCESS TOKEN, for any vendor that issues an expiring one.
1758
2019
  //
1759
2020
  // Written once here rather than per vendor, because every OAuth connection ends
@@ -2757,7 +3018,7 @@ var scaffold = {
2757
3018
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
2758
3019
  // lib/ directly. A bare .svg import would need a bundler loader and force the
2759
3020
  // tests through dist.
2760
- var icon$7 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
3021
+ var icon$8 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
2761
3022
  <rect width="500" height="500" fill="#FFD967"/>
2762
3023
  <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"/>
2763
3024
  <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"/>
@@ -3590,7 +3851,7 @@ var attentive = {
3590
3851
  webhook : false
3591
3852
 
3592
3853
  },
3593
- icon: icon$7,
3854
+ icon: icon$8,
3594
3855
  // DERIVED from the vendors this connection spends — every `credential` their
3595
3856
  // files declare. It used to be typed out here as well, which made the same env
3596
3857
  // names a three-copy fact (here, auth.oauth.client, and the vendor field).
@@ -4209,7 +4470,7 @@ const contacts = {
4209
4470
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
4210
4471
  // lib/ directly. A bare .svg import would need a bundler loader and force the
4211
4472
  // tests onto dist/, which is a worse trade than one line of wrapper.
4212
- var icon$6 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4473
+ var icon$7 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4213
4474
  <rect width="500" height="500" fill="#0D1314"/>
4214
4475
  <g clip-path="url(#clip0_2115_2832)">
4215
4476
  <path d="M153.198 142.241L183.69 196.065V303.852L153 357.676L184.82 375.028L220.116 313.163V186.865L185.046 125L153.198 142.241Z" fill="#BAEC5F"/>
@@ -4230,7 +4491,7 @@ var icon$6 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
4230
4491
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
4231
4492
  // lib/ directly. A bare .svg import would need a bundler loader and force the
4232
4493
  // tests onto dist/, which is a worse trade than one line of wrapper.
4233
- var icon$5 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4494
+ var icon$6 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4234
4495
  <rect width="500" height="500" fill="#FF7A59"/>
4235
4496
  <circle cx="290" cy="290" r="78" stroke="white" stroke-width="30"/>
4236
4497
  <circle cx="290" cy="290" r="18" fill="white"/>
@@ -4248,7 +4509,7 @@ var icon$5 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
4248
4509
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
4249
4510
  // lib/ directly. A bare .svg import would need a bundler loader and force the
4250
4511
  // tests onto dist/, which is a worse trade than one line of wrapper.
4251
- var icon$4 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4512
+ var icon$5 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4252
4513
  <rect width="500" height="500" fill="#F1FAFF"/>
4253
4514
  <g clip-path="url(#clip0_1011_2743)">
4254
4515
  <path d="M366.676 134.534V289.172H289.203V366.487H134.258L134.258 289.171L134.256 289.172V211.851H211.729V134.534H366.676Z" fill="#9DD6E3"/>
@@ -4271,7 +4532,7 @@ var icon$4 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
4271
4532
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
4272
4533
  // lib/ directly. A bare .svg import would need a bundler loader and force the
4273
4534
  // tests onto dist/, which is a worse trade than one line of wrapper.
4274
- var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4535
+ var icon$4 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
4275
4536
  <rect width="500" height="500" fill="#F22F46"/>
4276
4537
  <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"/>
4277
4538
  </svg>`;
@@ -5416,7 +5677,7 @@ var drawbridge = {
5416
5677
  },
5417
5678
  webhook : false
5418
5679
  },
5419
- icon: icon$6,
5680
+ icon: icon$7,
5420
5681
 
5421
5682
  // WHERE TWILIO PUTS THINGS on an inbound request, and WHICH credential
5422
5683
  // verifies each channel. The route resolves the NAME to the stored value and
@@ -5673,7 +5934,7 @@ var drawbridge = {
5673
5934
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
5674
5935
  // lib/ directly. A bare .svg import would need a bundler loader and force the
5675
5936
  // tests onto dist/, which is a worse trade than one line of wrapper.
5676
- var icon$2 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
5937
+ var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
5677
5938
  <rect width="500" height="500" fill="#FF4B32"/>
5678
5939
  <path d="M365.047 327.038H134.954V172.964H365.047L316.856 250.001L365.047 327.038Z" fill="#232121"/>
5679
5940
  </svg>`;
@@ -6655,7 +6916,7 @@ var klaviyo = {
6655
6916
  webhook : false
6656
6917
 
6657
6918
  },
6658
- icon: icon$2,
6919
+ icon: icon$3,
6659
6920
  // DERIVED from the vendors this connection spends — every `credential` their
6660
6921
  // files declare. It used to be typed out here as well, which made the same env
6661
6922
  // names a three-copy fact (here, auth.oauth.client, and the vendor field).
@@ -6814,7 +7075,7 @@ var klaviyo = {
6814
7075
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
6815
7076
  // lib/ directly. A bare .svg import would need a bundler loader and force the
6816
7077
  // tests onto dist/, which is a worse trade than one line of wrapper.
6817
- var icon$1 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
7078
+ var icon$2 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
6818
7079
  <rect width="500" height="500" fill="#FFE01B"/>
6819
7080
  <path d="M310.633 243.561C312.49 243.336 314.249 243.308 315.882 243.561C316.81 241.394 316.979 237.665 316.149 233.598C314.882 227.561 313.18 223.917 309.662 224.508C306.13 225.071 305.989 229.433 307.269 235.47C307.973 238.847 309.24 241.746 310.633 243.561ZM280.392 248.332C282.925 249.429 284.473 250.147 285.05 249.542C285.458 249.148 285.345 248.36 284.74 247.375C283.112 245.042 280.844 243.229 278.211 242.154C275.397 240.982 272.328 240.556 269.301 240.916C266.274 241.275 263.391 242.41 260.93 244.209C259.256 245.447 257.666 247.15 257.863 248.191C257.961 248.515 258.186 248.782 258.777 248.895C260.17 249.049 265.025 246.587 270.64 246.249C274.608 245.968 277.859 247.206 280.392 248.332ZM275.312 251.216C272.047 251.737 270.232 252.807 269.078 253.848C268.079 254.72 267.488 255.649 267.488 256.339L267.741 256.93L268.262 257.127C269.008 257.127 270.668 256.479 270.668 256.479C275.228 254.861 278.253 255.044 281.25 255.382C282.897 255.579 283.671 255.663 284.037 255.1C284.135 254.931 284.29 254.608 283.938 254.059C283.15 252.778 279.843 250.682 275.312 251.216ZM300.416 261.855C302.654 262.953 305.102 262.502 305.904 260.884C306.735 259.266 305.567 257.056 303.329 255.973C301.106 254.861 298.643 255.283 297.841 256.902C297.039 258.52 298.207 260.757 300.416 261.855ZM314.742 249.317C312.94 249.289 311.449 251.259 311.393 253.764C311.35 256.254 312.8 258.281 314.615 258.295C316.43 258.323 317.922 256.339 317.964 253.862C318.006 251.385 316.571 249.359 314.742 249.317ZM193.103 294.108C192.653 293.545 191.907 293.7 191.189 293.897C190.683 293.995 190.12 294.136 189.515 294.122C188.909 294.134 188.308 293.998 187.767 293.726C187.225 293.454 186.757 293.054 186.405 292.56C185.575 291.294 185.617 289.394 186.546 287.241L186.968 286.27C188.431 283.019 190.838 277.559 188.122 272.367C187.234 270.534 185.908 268.948 184.261 267.75C182.614 266.553 180.697 265.78 178.679 265.5C176.772 265.256 174.835 265.469 173.026 266.123C171.218 266.776 169.591 267.85 168.28 269.257C164.284 273.661 163.679 279.684 164.439 281.823C164.734 282.611 165.184 282.822 165.494 282.864C166.169 282.963 167.169 282.456 167.802 280.754L167.999 280.219C168.28 279.318 168.801 277.63 169.659 276.307C170.717 274.689 172.375 273.558 174.267 273.162C176.159 272.766 178.131 273.138 179.749 274.196C182.563 276.039 183.619 279.473 182.423 282.752C181.803 284.455 180.804 287.691 181.015 290.351C181.466 295.74 184.815 297.907 187.77 298.175C190.669 298.273 192.695 296.655 193.216 295.459C193.511 294.713 193.258 294.277 193.103 294.108Z" fill="#231E15"/>
6820
7081
  <path d="M360.476 284.243C360.35 283.835 359.618 281.204 358.647 278.052L356.621 272.648C360.575 266.696 360.645 261.405 360.124 258.393C359.531 254.519 357.693 250.944 354.89 248.205C351.766 244.94 345.363 241.563 336.385 239.044L331.671 237.736C331.643 237.524 331.418 226.619 331.235 221.933C331.08 218.555 330.798 213.264 329.152 208.058C327.182 200.993 323.791 194.858 319.527 190.876C331.277 178.717 338.594 165.307 338.58 153.81C338.538 131.703 311.379 124.976 277.902 138.851L270.824 141.863C270.795 141.835 258.004 129.283 257.821 129.128C219.63 95.8334 100.327 228.476 138.49 260.687L146.835 267.737C144.581 273.775 143.781 280.259 144.499 286.664C145.414 295.543 149.973 304.029 157.375 310.6C164.411 316.82 173.684 320.788 182.648 320.774C197.494 354.998 231.408 375.965 271.175 377.161C313.842 378.427 349.641 358.403 364.67 322.435C365.641 319.916 369.806 308.546 369.806 298.513C369.792 288.409 364.093 284.229 360.476 284.243ZM185.913 311.149C184.613 311.381 183.293 311.48 181.973 311.445C169.083 311.079 155.166 299.483 153.787 285.735C152.253 270.537 160.02 258.829 173.783 256.071C175.415 255.72 177.414 255.537 179.552 255.635C187.264 256.085 198.606 261.996 201.209 278.784C203.517 293.63 199.858 308.785 185.913 311.149ZM171.545 246.953C163.179 248.499 155.744 253.244 150.817 260.18C148.045 257.873 142.909 253.426 142.008 251.681C134.635 237.693 150.043 210.478 160.823 195.111C187.405 157.145 229.086 128.424 248.393 133.603C251.517 134.503 261.902 146.563 261.902 146.563C261.902 146.563 242.623 157.244 224.724 172.16C200.646 190.735 182.423 217.697 171.545 246.953ZM306.792 305.464C306.937 305.403 307.057 305.295 307.134 305.157C307.211 305.019 307.239 304.86 307.214 304.704C307.205 304.61 307.178 304.519 307.133 304.436C307.088 304.353 307.027 304.28 306.954 304.221C306.88 304.162 306.796 304.118 306.705 304.092C306.614 304.066 306.519 304.059 306.426 304.071C306.426 304.071 286.246 307.054 267.179 300.089C269.247 293.348 274.792 295.754 283.137 296.444C296.108 297.209 309.116 295.802 321.623 292.279C330.25 289.788 341.592 284.905 350.401 277.953C353.384 284.497 354.425 291.674 354.425 291.674C354.425 291.674 356.719 291.265 358.647 292.447C360.476 293.573 361.799 295.895 360.898 301.89C359.027 313.119 354.271 322.224 346.235 330.611C341.236 336.036 335.277 340.492 328.659 343.754C324.983 345.691 321.151 347.32 317.205 348.623C286.964 358.487 256.006 347.638 246.029 324.321C245.224 322.535 244.556 320.691 244.03 318.804C239.781 303.438 243.383 285.032 254.655 273.408C255.372 272.676 256.09 271.804 256.09 270.706C256.09 269.806 255.499 268.835 255.007 268.131C251.066 262.418 237.374 252.666 240.132 233.795C242.088 220.23 253.951 210.689 265.012 211.252L267.826 211.421C272.611 211.702 276.79 212.307 280.73 212.49C287.344 212.758 293.268 211.801 300.304 205.947C302.683 203.949 304.582 202.246 307.791 201.711C308.128 201.627 308.973 201.359 310.647 201.416C312.365 201.485 314.032 202.015 315.474 202.949C321.103 206.693 321.905 215.783 322.214 222.439C322.383 226.225 322.848 235.414 322.988 238.031C323.354 244.054 324.944 244.912 328.125 245.954C329.94 246.573 331.615 246.995 334.077 247.713C341.521 249.781 345.968 251.934 348.754 254.65C350.198 256.049 351.13 257.893 351.4 259.885C352.315 266.316 346.432 274.252 330.925 281.457C313.954 289.324 293.367 291.322 279.154 289.732L274.173 289.169C262.774 287.649 256.315 302.34 263.14 312.402C267.545 318.889 279.52 323.11 291.523 323.11C319.006 323.139 340.142 311.402 348.023 301.242L348.642 300.356C349.008 299.765 348.712 299.469 348.22 299.779C341.817 304.169 313.279 321.619 282.771 316.384C282.771 316.384 279.056 315.765 275.678 314.442C273.005 313.429 267.362 310.811 266.686 305.042C291.27 312.683 306.792 305.478 306.792 305.464ZM220.671 194.971C230.127 184.051 241.765 174.538 252.206 169.219C252.558 169.022 252.938 169.43 252.741 169.754C251.46 172 250.476 174.403 249.814 176.902C249.73 177.282 250.138 177.592 250.461 177.353C256.963 172.934 268.248 168.192 278.155 167.601C278.251 167.584 278.351 167.601 278.436 167.65C278.521 167.698 278.586 167.775 278.621 167.866C278.656 167.957 278.658 168.058 278.627 168.151C278.596 168.244 278.533 168.323 278.451 168.375C276.809 169.634 275.342 171.105 274.088 172.751C273.891 173.032 274.074 173.44 274.426 173.44C281.378 173.483 291.186 175.903 297.56 179.491C297.982 179.745 297.673 180.575 297.209 180.462C287.527 178.253 271.724 176.564 255.288 180.575C240.597 184.149 229.396 189.666 221.248 195.618C220.826 195.899 220.333 195.351 220.671 194.971Z" fill="#231E15"/>
@@ -7684,7 +7945,7 @@ var mailchimp = {
7684
7945
  // Drawbridge posts to a merchant's own endpoint, never through a vendor.
7685
7946
  webhook : false
7686
7947
  },
7687
- icon: icon$1,
7948
+ icon: icon$2,
7688
7949
  // The OAuth client this deployment registered. Without both, the vendor drops
7689
7950
  // out of availableConnections rather than offering a Connect button that
7690
7951
  // cannot complete.
@@ -7844,7 +8105,7 @@ var mailchimp = {
7844
8105
  // A .js wrapper around otherwise untouched SVG so `node --test` can run against
7845
8106
  // lib/ directly. A bare .svg import would need a bundler loader and force the
7846
8107
  // tests onto dist/, which is a worse trade than one line of wrapper.
7847
- var icon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
8108
+ var icon$1 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
7848
8109
  <rect width="500" height="500" fill="#95C049"/>
7849
8110
  <path d="M292.997 147.633C292.997 147.633 289.98 148.495 285.023 150.004C284.161 147.202 282.868 143.969 281.144 140.521C275.54 129.744 267.134 123.925 257.22 123.925C256.574 123.925 255.927 123.925 255.065 124.141C254.849 123.71 254.418 123.494 254.203 123.063C249.892 118.322 244.289 116.166 237.607 116.382C224.676 116.813 211.744 126.081 201.399 142.676C194.071 154.314 188.468 168.97 186.959 180.393C172.088 184.919 161.743 188.152 161.527 188.367C153.984 190.738 153.768 190.954 152.906 198.066C151.613 203.454 132 355.4 132 355.4L294.937 383.633V147.202C294.075 147.418 293.429 147.418 292.997 147.633ZM255.281 159.271C246.66 161.858 237.176 164.875 227.909 167.677C230.495 157.547 235.668 147.418 241.702 140.736C244.073 138.365 247.306 135.564 250.97 133.839C254.634 141.598 255.496 152.159 255.281 159.271ZM237.823 125.003C240.84 125.003 243.427 125.649 245.582 126.943C242.133 128.667 238.685 131.469 235.452 134.702C227.262 143.538 221.012 157.332 218.426 170.479C210.667 172.85 202.908 175.22 195.796 177.376C200.322 156.901 217.779 125.649 237.823 125.003ZM212.607 243.542C213.469 257.335 249.892 260.353 252.048 292.897C253.556 318.545 238.47 336.002 216.701 337.295C190.407 339.02 175.967 323.502 175.967 323.502L181.571 299.794C181.571 299.794 196.011 310.786 207.649 309.924C215.193 309.493 217.995 303.242 217.779 298.932C216.701 280.828 186.959 281.905 185.019 252.163C183.295 227.377 199.675 202.161 235.883 199.79C249.892 198.928 257.005 202.377 257.005 202.377L248.815 233.412C248.815 233.412 239.547 229.102 228.555 229.964C212.607 231.041 212.391 241.171 212.607 243.542ZM263.902 156.685C263.902 150.219 263.039 140.952 260.022 133.193C269.936 135.133 274.678 146.124 276.833 152.806C272.954 153.883 268.643 155.176 263.902 156.685Z" fill="white"/>
7850
8111
  <path d="M300.325 382.771L368 365.96C368 365.96 338.904 169.185 338.689 167.892C338.473 166.599 337.396 165.737 336.318 165.737C335.24 165.737 316.274 165.306 316.274 165.306C316.274 165.306 304.636 154.099 300.325 149.788V382.771Z" fill="white"/>
@@ -9586,6 +9847,12 @@ var shopify = {
9586
9847
  orderId : String( orderId ),
9587
9848
  rate,
9588
9849
  shopId : connection.source.id,
9850
+ // WHICH VENDOR IS BEING CHARGED, so the sender does not have
9851
+ // to assume. queue/usage.js named the slug itself and
9852
+ // defaults to it for anything already queued when this
9853
+ // shipped — an app-store vendor billing through its own meter
9854
+ // enqueues the same job with its own slug.
9855
+ slug : connection.slug,
9589
9856
  // The App Events API returns no event id, so one is generated
9590
9857
  // here — the event handle plus the order id — and sent as the
9591
9858
  // event's `reference`. queue/usage.js stamps the same id onto
@@ -11185,7 +11452,7 @@ var shopify = {
11185
11452
  // Drawbridge posts to a merchant's own endpoint, never through a vendor.
11186
11453
  webhook : false
11187
11454
  },
11188
- icon,
11455
+ icon: icon$1,
11189
11456
  // A pre-launch integration: it only surfaces once the App Store listing
11190
11457
  // exists and the app is fully configured. Requiring all four means it can
11191
11458
  // never render half-configured — and absence of any one excludes the
@@ -11300,6 +11567,13 @@ var shopify = {
11300
11567
  key : 'Shopify Order Tracking',
11301
11568
  queue : 'connection',
11302
11569
  system : true,
11570
+ // THE TRIGGER THE REGISTER JOB HAND-WROTE. It is byte-identical to
11571
+ // the one on every stored workflow, because these documents exist on
11572
+ // prod with thousands of runs behind them and provisioning is
11573
+ // idempotent on (connection, system, title) — a different trigger
11574
+ // here would not be picked up by the existing rows anyway, and a
11575
+ // different TITLE would provision a second workflow for every store.
11576
+ trigger : { event : 'shopify.order.create', type : 'webhook' },
11303
11577
  type : 'step.commerce.order.record'
11304
11578
  })
11305
11579
  },
@@ -11311,6 +11585,15 @@ var shopify = {
11311
11585
  key : 'Shopify Product Sync',
11312
11586
  queue : 'connection',
11313
11587
  system : true,
11588
+ // `shopify.product.update`, NOT `product.insert`. The plan named the
11589
+ // latter, and it is the right long-term answer — a Drawbridge product
11590
+ // row appearing should pull the vendor's copy, rather than
11591
+ // stream/product.js branching on the slug and enqueueing a
11592
+ // vendor-named queue. But a workflow carries ONE trigger, this one is
11593
+ // stored on prod against the webhook, and changing a live workflow's
11594
+ // trigger is a data migration. So the declaration matches what exists
11595
+ // and `product.insert` stays a separate, migration-gated change.
11596
+ trigger : { event : 'shopify.product.update', type : 'webhook' },
11314
11597
  type : 'step.commerce.product.sync'
11315
11598
  })
11316
11599
  }
@@ -11336,26 +11619,37 @@ var shopify = {
11336
11619
  })
11337
11620
  },
11338
11621
 
11339
- // Audit-only. The "Shopify Token Activity" system workflow lists these
11340
- // for descriptive grouping, but its audit step docs are written manually
11341
- // at OAuth time — the workflow is never dispatched. Routing is declared
11342
- // defensively so that if it ever IS dispatched, the job lands on a real
11343
- // queue and the handler lookup misses cleanly instead of throwing
11344
- // "Unknown step type".
11622
+ // THE ONE WORKFLOW THAT CARRIES TWO STEPS, and the shape the plan called
11623
+ // the single genuinely new thing in it.
11624
+ //
11625
+ // Every other system step is its own workflow, titled by its own `key`.
11626
+ // These two are "Shopify Token Activity", one row in the merchant's list
11627
+ // with both steps in it — so they declare `workflow`, which names the
11628
+ // title, the trigger and the ORDER, and the provisioner groups on it.
11629
+ // Without that key the pair could only be provisioned by the hand-written
11630
+ // spec in drawbridge-sync's register job, which is exactly why that spec
11631
+ // outlived every other one.
11632
+ //
11633
+ // AUDIT-ONLY. The step documents are written manually at OAuth time and
11634
+ // the workflow is never dispatched; `queue` is declared defensively so
11635
+ // that if it ever IS, the job lands on a real queue and the handler lookup
11636
+ // misses cleanly rather than throwing "Unknown step type".
11345
11637
  token : {
11346
11638
  exchange : () => ({
11347
11639
  description : 'Records the token exchange that completed an install. Audit only — never dispatched.',
11348
11640
  key : 'Shopify Token Exchange',
11349
11641
  queue : 'connection',
11350
11642
  system : true,
11351
- type : 'step.connection.token.exchange'
11643
+ type : 'step.connection.token.exchange',
11644
+ workflow : { key : 'Shopify Token Activity', order : 0, trigger : { event : 'shopify.token', type : 'event' } }
11352
11645
  }),
11353
11646
  refresh : () => ({
11354
11647
  description : 'Records a token rotation. Audit only — never dispatched.',
11355
11648
  key : 'Shopify Token Refresh',
11356
11649
  queue : 'connection',
11357
11650
  system : true,
11358
- type : 'step.connection.token.refresh'
11651
+ type : 'step.connection.token.refresh',
11652
+ workflow : { key : 'Shopify Token Activity', order : 1, trigger : { event : 'shopify.token', type : 'event' } }
11359
11653
  })
11360
11654
  }
11361
11655
 
@@ -11693,7 +11987,7 @@ var webhook = {
11693
11987
  // Borrowed: this is the Drawbridge mark, because Webhooks has none of its own.
11694
11988
  // It is the one card that reads wrong — our logo among vendor logos — and it
11695
11989
  // wants a mark of its own when there is one.
11696
- icon: icon$6,
11990
+ icon: icon$7,
11697
11991
  // EXPLICIT, not derived, because the name is not a vendor's. There is no third
11698
11992
  // party behind a webhook — connecting MINTS a secret — so this is one of
11699
11993
  // Drawbridge's own keys, listed in PLATFORM_CREDENTIALS. Without it the signing
@@ -11742,7 +12036,7 @@ var webhook = {
11742
12036
  // The connect prompt only where connecting is not already the card's whole
11743
12037
  // story: a disconnected or errored document renders a Connect action itself,
11744
12038
  // and a task repeating it talks over the button.
11745
- tasks : ( { settings, status } = {} ) => ( [ 'disconnected', 'error' ].includes( status )
12039
+ tasks : ( { settings, status } = {} ) => ( status === 'error'
11746
12040
  ? []
11747
12041
  : settings?.secret
11748
12042
  ? [
@@ -11831,7 +12125,7 @@ var attentiveVendor = {
11831
12125
 
11832
12126
  // The same mark the connection carries. One vendor, one icon — mergeVendors
11833
12127
  // used to throw when two manifests disagreed about this.
11834
- icon: icon$7,
12128
+ icon: icon$8,
11835
12129
 
11836
12130
  name : 'Attentive',
11837
12131
 
@@ -11850,6 +12144,112 @@ var attentiveVendor = {
11850
12144
 
11851
12145
  };
11852
12146
 
12147
+ // Bright Data's mark, drawn by hand rather than exported from a brand kit —
12148
+ // there was no export to hand. The eight-spoke burst on their blue is the
12149
+ // recognisable part; replace with the kit's own mark when one is available.
12150
+ //
12151
+ // A .js wrapper around otherwise untouched SVG so `node --test` can run against
12152
+ // lib/ directly. A bare .svg import would need a bundler loader and force the
12153
+ // tests onto dist/, which is a worse trade than one line of wrapper.
12154
+ var icon = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
12155
+ <rect width="500" height="500" fill="#0F58FF"/>
12156
+ <circle cx="250" cy="250" r="52" fill="white"/>
12157
+ <rect x="234" y="70" width="32" height="96" rx="16" fill="white"/>
12158
+ <rect x="234" y="334" width="32" height="96" rx="16" fill="white"/>
12159
+ <rect x="70" y="234" width="96" height="32" rx="16" fill="white"/>
12160
+ <rect x="334" y="234" width="96" height="32" rx="16" fill="white"/>
12161
+ <rect x="117" y="140" width="32" height="96" rx="16" transform="rotate(-45 117 140)" fill="white"/>
12162
+ <rect x="304" y="327" width="32" height="96" rx="16" transform="rotate(-45 304 327)" fill="white"/>
12163
+ <rect x="360" y="117" width="32" height="96" rx="16" transform="rotate(45 360 117)" fill="white"/>
12164
+ <rect x="173" y="304" width="32" height="96" rx="16" transform="rotate(45 173 304)" fill="white"/>
12165
+ </svg>`;
12166
+
12167
+ // DRAWBRIDGE'S OWN CREDENTIALS FOR BRIGHT DATA — the scrape path's rendering and
12168
+ // unblocking, spent on our account and never on a merchant's.
12169
+ //
12170
+ // A VENDOR WITH NO CONNECTION, and the second one after Stripe. Nobody links a
12171
+ // Bright Data account to Drawbridge; scraping is infrastructure that happens to
12172
+ // cost money, so there is no manifest, no hooks and nothing merchant-facing. It
12173
+ // is here because a vendor is the row in the `provider` collection holding our
12174
+ // keys for it — and until vendors became their own files, a vendor with no
12175
+ // connection could not hold credentials at all, because the admin screen's
12176
+ // vocabulary was derived from the connection registry.
12177
+ //
12178
+ // WHY IT MOVED OFF ENV, 2026-09-16. The unlocker key had an expiry nobody knew
12179
+ // about and Bright Data mailed a three-day warning to one person's inbox. Their
12180
+ // API offers no programmatic rotation — the docs are explicit that refreshing is
12181
+ // a human in the control panel and that "any request you will send with the old
12182
+ // key will fail to authenticate" the moment you do — so renewal is always going
12183
+ // to be a paste. The question was only where it gets pasted: three env vars
12184
+ // across two DigitalOcean app specs, each needing a redeploy, or one admin
12185
+ // screen with a 60-second memo. Darren, 2026-09-16, choosing the latter and an
12186
+ // unlimited-expiry key.
12187
+ //
12188
+ // `credential` NAMES THE ENV VAR EACH FIELD FALLS BACK TO, which is what makes
12189
+ // the move safe to ship before the row exists: old deployments keep reading env,
12190
+ // and the row takes over the moment it is filled.
12191
+ var brightdataVendor = {
12192
+
12193
+ fields : [
12194
+ {
12195
+ credential : 'BRIGHTDATA_BROWSER_URI',
12196
+ input : 'password',
12197
+ key : 'browserUri',
12198
+ // A wss:// endpoint with the account's user and password in its
12199
+ // authority, which is why it is a password field rather than a url one:
12200
+ // the whole string is the credential.
12201
+ label : 'Scraping Browser endpoint',
12202
+ message : 'The wss:// Scraping Browser endpoint, carrying its own credentials. Used to render pages that need a real browser.',
12203
+ redact : true,
12204
+ required : true
12205
+ },
12206
+ {
12207
+ credential : 'BRIGHTDATA_UNLOCKER_API_KEY',
12208
+ input : 'password',
12209
+ key : 'unlockerApiKey',
12210
+ label : 'Web Unlocker API key',
12211
+ // NO EXPIRY FIELD, and that is a decision rather than an omission. Bright
12212
+ // Data offers an `Unlimited` expiry and recommends against it; Drawbridge
12213
+ // takes the unlimited key deliberately, because there is no way to rotate
12214
+ // this programmatically and a date nothing can act on is a date that only
12215
+ // surfaces after it has already broken scraping. If a fixed expiry is ever
12216
+ // taken instead, the field to add is a stored date and a warning ahead of
12217
+ // it — not a rotation, which the vendor cannot support.
12218
+ message : 'Bright Data account settings → 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.',
12219
+ redact : true,
12220
+ required : true
12221
+ },
12222
+ {
12223
+ credential : 'BRIGHTDATA_UNLOCKER_ZONE',
12224
+ input : 'text',
12225
+ key : 'unlockerZone',
12226
+ label : 'Web Unlocker zone',
12227
+ // Not secret — a zone is a label on the account, and it is shown rather
12228
+ // than redacted so an operator can confirm which one is in use without
12229
+ // clearing the field to read it.
12230
+ message : 'The zone name the Unlocker requests run against.',
12231
+ required : true
12232
+ }
12233
+ ],
12234
+
12235
+ icon,
12236
+
12237
+ name : 'Bright Data',
12238
+
12239
+ slug : 'brightdata',
12240
+
12241
+ // WHERE THE FACTS CAME FROM, read 2026-09-16.
12242
+ urls : {
12243
+ api : 'https://docs.brightdata.com/api-reference/authentication',
12244
+ dashboard : 'https://brightdata.com/cp/setting/users',
12245
+ // Bright Data has no scope model — a key carries the account's own access —
12246
+ // so there is nothing to link. `false` is the recorded answer rather than a
12247
+ // missing key.
12248
+ scopes : false
12249
+ }
12250
+
12251
+ };
12252
+
11853
12253
  // DRAWBRIDGE'S OWN CREDENTIALS FOR HUBSPOT, and where the facts about it came
11854
12254
  // from. One file per vendor, because a vendor is NOT a connection: it is the row
11855
12255
  // in the `provider` collection holding our keys for it, and a connection names
@@ -11880,7 +12280,7 @@ var hubspotVendor = {
11880
12280
 
11881
12281
  // The vendor's own mark rather than the connection's: drawbridge fronts three
11882
12282
  // vendors and none of them is Drawbridge.
11883
- icon: icon$5,
12283
+ icon: icon$6,
11884
12284
 
11885
12285
  name : 'HubSpot',
11886
12286
 
@@ -11937,7 +12337,7 @@ var klaviyoVendor = {
11937
12337
 
11938
12338
  // The same mark the connection carries. One vendor, one icon — mergeVendors
11939
12339
  // used to throw when two manifests disagreed about this.
11940
- icon: icon$2,
12340
+ icon: icon$3,
11941
12341
 
11942
12342
  name : 'Klaviyo',
11943
12343
 
@@ -11994,7 +12394,7 @@ var mailchimpVendor = {
11994
12394
 
11995
12395
  // The same mark the connection carries. One vendor, one icon — mergeVendors
11996
12396
  // used to throw when two manifests disagreed about this.
11997
- icon: icon$1,
12397
+ icon: icon$2,
11998
12398
 
11999
12399
  name : 'Mailchimp',
12000
12400
 
@@ -12065,7 +12465,7 @@ var sendgridVendor = {
12065
12465
 
12066
12466
  // The vendor's own mark rather than the connection's: drawbridge fronts three
12067
12467
  // vendors and none of them is Drawbridge.
12068
- icon: icon$4,
12468
+ icon: icon$5,
12069
12469
 
12070
12470
  name : 'SendGrid',
12071
12471
 
@@ -12172,7 +12572,7 @@ var shopifyVendor = {
12172
12572
 
12173
12573
  // The same mark the connection carries. One vendor, one icon — mergeVendors
12174
12574
  // used to throw when two manifests disagreed about this.
12175
- icon,
12575
+ icon: icon$1,
12176
12576
 
12177
12577
  name : 'Shopify',
12178
12578
 
@@ -12238,7 +12638,7 @@ var twilioVendor = {
12238
12638
 
12239
12639
  // The vendor's own mark rather than the connection's: drawbridge fronts three
12240
12640
  // vendors and none of them is Drawbridge.
12241
- icon: icon$3,
12641
+ icon: icon$4,
12242
12642
 
12243
12643
  name : 'Twilio',
12244
12644
 
@@ -12418,7 +12818,7 @@ const checkIcon = ( owner, icon ) => {
12418
12818
  // its icon, declaring one of its fields two ways, or claiming the same credential.
12419
12819
  // None of those can happen when a vendor is declared once, so all of it is gone.
12420
12820
  const vendors = Object.freeze( Object.fromEntries(
12421
- [ attentiveVendor, hubspotVendor, klaviyoVendor, mailchimpVendor, sendgridVendor, shopifyVendor, twilioVendor ]
12821
+ [ attentiveVendor, brightdataVendor, hubspotVendor, klaviyoVendor, mailchimpVendor, sendgridVendor, shopifyVendor, twilioVendor ]
12422
12822
  .map( ( vendor ) => [ vendor.slug, vendor ] )
12423
12823
  ) );
12424
12824
 
@@ -13301,17 +13701,42 @@ const build = ( manifest ) => {
13301
13701
 
13302
13702
  }
13303
13703
 
13304
- if( declared.trigger ){
13704
+ // A WORKFLOW GROUPING several steps into one row. Its trigger is checked
13705
+ // against the same table as a step's own, because a typo here is the same
13706
+ // silent failure: the workflow saves, reads active, and matches nothing.
13707
+ if( declared.workflow ){
13708
+
13709
+ if( ! declared.workflow.key ){
13710
+
13711
+ throw new Error( manifest.slug + ' step ' + type + ' declares a workflow with no key — the title several steps share' );
13712
+
13713
+ }
13714
+
13715
+ if( typeof declared.workflow.order !== 'number' ){
13716
+
13717
+ throw new Error( manifest.slug + ' step ' + type + ' declares a workflow with no order — two steps in one row need a sequence' );
13718
+
13719
+ }
13720
+
13721
+ if( declared.trigger ){
13722
+
13723
+ throw new Error( manifest.slug + ' step ' + type + ' declares BOTH a trigger and a workflow — the workflow carries the trigger for every step in it' );
13724
+
13725
+ }
13726
+
13727
+ }
13728
+
13729
+ for( const candidate of [ declared.trigger, declared.workflow?.trigger ].filter( Boolean ) ){
13305
13730
 
13306
13731
  const known = TRIGGERS[
13307
- Object.keys( TRIGGERS ).find( ( key ) => TRIGGERS[ key ].event === declared.trigger.event && TRIGGERS[ key ].type === declared.trigger.type )
13732
+ Object.keys( TRIGGERS ).find( ( key ) => TRIGGERS[ key ].event === candidate.event && TRIGGERS[ key ].type === candidate.type )
13308
13733
  ];
13309
13734
 
13310
13735
  if( ! known ){
13311
13736
 
13312
13737
  throw new Error(
13313
- manifest.slug + ' step ' + type + ' declares trigger { event : ' + declared.trigger.event
13314
- + ', type : ' + declared.trigger.type + ' }, which nothing dispatches'
13738
+ manifest.slug + ' step ' + type + ' declares trigger { event : ' + candidate.event
13739
+ + ', type : ' + candidate.type + ' }, which nothing dispatches'
13315
13740
  );
13316
13741
 
13317
13742
  }
@@ -13463,6 +13888,16 @@ const connectionSteps = ( env = {} ) => Object.entries( availableConnections( en
13463
13888
 
13464
13889
  // Which vendors implement a given hook. The uniform surface makes this total —
13465
13890
  // every vendor appears in exactly one of the two lists, never neither.
13891
+ // DOES THIS VENDOR IMPLEMENT THIS HOOK? The same walk `hookSupport` and
13892
+ // `build()` use, exported because callers outside this package were asking it by
13893
+ // naming a slug — drawbridge-sync dispatched the post-connect register job on
13894
+ // `slug === 'shopify'`, with a comment explaining that no other vendor has one,
13895
+ // which is this function's answer written out by hand.
13896
+ //
13897
+ // TAKES THE HOOKS, not the manifest, so a caller holding a built manifest and a
13898
+ // caller holding a fixture ask the same way.
13899
+ const implementsHook = ( hooks, name ) => implemented( hooks, name );
13900
+
13466
13901
  const hookSupport = ( name ) => ({
13467
13902
  no : Object.keys( connections ).filter( ( slug ) => ! implemented( connections[ slug ].hooks, name ) ),
13468
13903
  yes : Object.keys( connections ).filter( ( slug ) => implemented( connections[ slug ].hooks, name ) )
@@ -13806,4 +14241,4 @@ const resolveConnection = ( item, data, env = {} ) => {
13806
14241
 
13807
14242
  };
13808
14243
 
13809
- export { stepRoutes as $, ANSWER_KEYS as A, ensureSystemWorkflows as B, hookSupport as C, isStale as D, ERROR_SOURCES as E, isStatus as F, GROUPS as G, HOOKS as H, INPUTS as I, mergeSettings as J, perform as K, projectConnection as L, publicConnectionKeys as M, publicSettingsBySlug as N, OAUTH_ENDPOINTS as O, PLATFORM_CREDENTIALS as P, reconcileConnectionScopes as Q, RETIRED as R, STATUSES as S, TRIGGERS as T, redactSettings as U, resolveConnection as V, WRITE_OPERATIONS as W, runHook as X, scopesMessage as Y, stepLabels as Z, stepQueues as _, AUTH_KINDS as a, surviving as a0, systemSteps as a1, tokenSettings as a2, vendors as a3, AUTH_TYPES as b, HOOK_CONFIG as c, HOOK_EFFECTS as d, HOOK_NAMES as e, HOOK_OPTIONS as f, HOOK_PROPS as g, HOOK_SLOT_PROPS as h, OAUTH_FIELDS as i, OUTCOMES as j, STEPS as k, STEP_TYPES as l, TRIGGER_TYPES as m, accessToken as n, answerOf as o, assess as p, availableConnections as q, build as r, buildVendor as s, catalogConnections as t, channels as u, connectFields as v, connectionSettings as w, connectionSteps as x, connections as y, effectsOf as z };
14244
+ export { scopesMessage as $, ANSWER_KEYS as A, ensureSystemWorkflows as B, hookSupport as C, implementsHook as D, ERROR_SOURCES as E, isStale as F, GROUPS as G, HOOKS as H, INPUTS as I, isStatus as J, mergeSettings as K, perform as L, MARKERS as M, projectConnection as N, OAUTH_ENDPOINTS as O, PLATFORM_CREDENTIALS as P, publicConnectionKeys as Q, RETIRED as R, STATUSES as S, TRIGGERS as T, publicSettingsBySlug as U, reconcileConnectionScopes as V, WRITE_OPERATIONS as W, redactSettings as X, reference as Y, resolveConnection as Z, runHook as _, AUTH_KINDS as a, spliced as a0, stepLabels as a1, stepQueues as a2, stepRoutes as a3, surviving as a4, systemSteps as a5, systemWorkflows as a6, tokenSettings as a7, vendors as a8, AUTH_TYPES as b, HOOK_CONFIG as c, HOOK_EFFECTS as d, HOOK_NAMES as e, HOOK_OPTIONS as f, HOOK_PROPS as g, HOOK_SLOT_PROPS as h, OAUTH_FIELDS as i, OUTCOMES as j, STEPS as k, STEP_TYPES as l, TRIGGER_TYPES as m, accessToken as n, answerOf as o, assess as p, availableConnections as q, build as r, buildVendor as s, catalogConnections as t, channels as u, connectFields as v, connectionSettings as w, connectionSteps as x, connections as y, effectsOf as z };