@drawbridge/drawbridge-utils 0.0.121 → 0.0.125

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.
@@ -1,7 +1,9 @@
1
- import { mergeSettings } from './connections/index.cjs';
1
+ import { connections, mergeSettings } from './connections/index.cjs';
2
2
  import { decrypt, encrypt } from './encrypt.cjs';
3
3
  import './connections/oauth.cjs';
4
4
  import 'node:crypto';
5
+ import './phone.cjs';
6
+ import 'libphonenumber-js';
5
7
  import './http.cjs';
6
8
  import './pricing.cjs';
7
9
  import './plans.cjs';
@@ -16,8 +18,6 @@ import './billing.cjs';
16
18
  import './transactions.cjs';
17
19
  import '@drawbridge/drawbridge-telemetry';
18
20
  import './email.cjs';
19
- import './phone.cjs';
20
- import 'libphonenumber-js';
21
21
  import './safe-http.cjs';
22
22
  import 'dns';
23
23
  import 'node:http';
@@ -42,60 +42,41 @@ import './token.cjs';
42
42
  // the others have already rotated away.
43
43
 
44
44
 
45
- // WHAT AN ADMIN TYPES IN, per vendor.
45
+ // WHAT AN ADMIN TYPES IN, per vendor — READ FROM THE MANIFESTS.
46
46
  //
47
- // `redact` marks a secret: never returned by the api, and blank on save means
48
- // keep the stored value. `required` drives the live check. `input` picks the
49
- // form control.
47
+ // This was a table here, duplicating what every vendor file already knew. A
48
+ // vendor's credentials and its `requires` name the same things, and keeping
49
+ // them in two files meant a manifest could require a name that no admin screen
50
+ // offered to fill — the exact shape of "declared in one place, entered in
51
+ // another" these manifests exist to close.
50
52
  //
51
- // These move onto the manifests themselves as `provider : { fields }` in the
52
- // follow-up plan, at which point providerFields() delegates to the manifest and
53
- // this constant goes. It is one table for now because a vendor's credentials and
54
- // its manifest are declared in the same package either way.
55
- const FIELDS = {
56
- attentive : [
57
- { input : 'text', key : 'clientId', env : 'ATTENTIVE_OAUTH_CLIENT_ID', label : 'Client ID', required : true },
58
- { input : 'password', key : 'clientSecret', env : 'ATTENTIVE_OAUTH_CLIENT_SECRET', label : 'Client secret', redact : true, required : true }
59
- ],
60
- drawbridge : [
61
- { input : 'email', key : 'accountSender', env : 'SENDGRID_FROM_ADDRESS', label : 'Account sender', message : 'Verification codes and security alerts send from here.', required : true },
62
- { input : 'password', key : 'apiKey', env : 'SENDGRID_API_KEY', label : 'SendGrid API key', redact : true, required : true },
63
- // NOT required. The CRM sync is best-effort internal tooling and no-ops
64
- // without a token — requiring it would make the whole drawbridge provider
65
- // read not-live over something no merchant ever sees.
66
- { input : 'password', key : 'hubspotToken', env : 'HUBSPOT_ACCESS_TOKEN', label : 'HubSpot access token', message : 'Drawbridge\'s own CRM portal. Internal — no merchant sees this.', redact : true },
67
- // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
68
- // either. Unset, it degrades to the account sender rather than
69
- // refusing to start.
70
- { input : 'email', key : 'leadSender', env : 'SENDGRID_SEND_FROM_ADDRESS', label : 'Lead sender', message : 'The default for lead-facing mail when a merchant has not verified their own domain.' },
71
- { input : 'text', key : 'smsFrom', env : 'TWILIO_ACCOUNT_FROM', label : 'SMS number', required : true },
72
- { input : 'password', key : 'smsSid', env : 'TWILIO_ACCOUNT_SID', label : 'Twilio account SID', redact : true, required : true },
73
- { input : 'password', key : 'smsToken', env : 'TWILIO_AUTH_TOKEN', label : 'Twilio auth token', redact : true, required : true }
74
- ],
75
- klaviyo : [
76
- { input : 'text', key : 'clientId', env : 'KLAVIYO_OAUTH_CLIENT_ID', label : 'Client ID', required : true },
77
- { input : 'password', key : 'clientSecret', env : 'KLAVIYO_OAUTH_CLIENT_SECRET', label : 'Client secret', redact : true, required : true }
78
- ],
79
- mailchimp : [
80
- { input : 'text', key : 'clientId', env : 'MAILCHIMP_OAUTH_CLIENT_ID', label : 'Client ID', required : true },
81
- { input : 'password', key : 'clientSecret', env : 'MAILCHIMP_OAUTH_CLIENT_SECRET', label : 'Client secret', redact : true, required : true }
82
- ],
83
- shopify : [
84
- { input : 'text', key : 'apiKey', env : 'SHOPIFY_API_KEY', label : 'API key', required : true },
85
- { input : 'password', key : 'apiSecret', env : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
86
- { input : 'text', key : 'appHandle', env : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
87
- { input : 'text', key : 'listingUrl', env : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true }
88
- ]
89
- };
90
-
91
- // Object.hasOwn, not `FIELDS[ slug ] ||` — a bare property read walks the
92
- // prototype chain, and FIELDS[ 'constructor' ] is a function whose .length is
93
- // 1, so the route's "does this vendor exist" guard passed for it and reached
53
+ // Each field's `credential` is the NAME the rest of the system asks for that
54
+ // value by. It is not an environment variable and there is no fallback to one:
55
+ // the names simply began life as env vars and were kept as the vocabulary, so
56
+ // `requires` and auth.oauth.client did not have to change when the storage
57
+ // moved into the database.
58
+ //
59
+ // So the declaration lives on the manifest as `provider : { fields }`, build()
60
+ // checks its shape at import, and this module is now only the reading, memoizing
61
+ // and encrypting around it.
62
+ //
63
+ // Object.hasOwn, not `connections[ slug ] ||` a bare property read walks the
64
+ // prototype chain, and connections[ 'constructor' ] is a function whose .length
65
+ // is 1, so the route's "does this vendor exist" guard passed for it and reached
94
66
  // the database before throwing.
95
- const providerFields = ( slug ) => Object.hasOwn( FIELDS, slug ) ? FIELDS[ slug ] : [];
67
+ const providerFields = ( slug ) => (
68
+ Object.hasOwn( connections, slug ) ? ( connections[ slug ].provider?.fields || [] ) : []
69
+ );
96
70
 
97
71
  // The vendors an admin can configure, in a stable order.
98
- const providerSlugs = () => Object.keys( FIELDS ).sort();
72
+ //
73
+ // DERIVED FROM WHO DECLARES A PROVIDER BLOCK, not from the catalog: `webhook` is
74
+ // a connection with no vendor behind it — nothing to authenticate against and
75
+ // nothing to type — so it must not appear on a screen whose whole purpose is
76
+ // entering credentials.
77
+ const providerSlugs = () => Object.keys( connections )
78
+ .filter( ( slug ) => connections[ slug ].provider )
79
+ .sort();
99
80
 
100
81
  // LIVE IS DERIVED, never stored. A stored flag is a second thing to keep in
101
82
  // sync and goes stale the moment a row is edited by hand.
@@ -270,17 +251,16 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
270
251
  const providerEnvNames = () => new Set(
271
252
  providerSlugs()
272
253
  .flatMap( ( slug ) => providerFields( slug ) )
273
- .map( ( field ) => field.env )
254
+ .map( ( field ) => field.credential )
274
255
  .filter( Boolean )
275
256
  );
276
257
 
277
- // EVERY STORED CREDENTIAL, SHAPED LIKE AN ENVIRONMENT.
258
+ // EVERY STORED CREDENTIAL, KEYED BY ITS NAME.
278
259
  //
279
- // Keyed by the env var each field replaces, because that is the shape the
280
- // manifests already speak: `requires` names env vars, and auth.oauth.client
281
- // names them too. Handing availableConnections this map instead of process.env
282
- // is the whole switch — the package does not change, and 137 call sites in this
283
- // repo keep reading a plain synchronous object.
260
+ // Keyed by each field's `credential`, because that is the vocabulary the
261
+ // manifests already speak: `requires` names credentials, and auth.oauth.client
262
+ // names them too. Handing availableConnections this map is what makes stored
263
+ // credentials answer the question the manifests were already asking.
284
264
  //
285
265
  // A field with no value is OMITTED rather than set empty, so `requires` sees
286
266
  // the same absence it would for an unset variable.
@@ -307,7 +287,7 @@ const providerCredentials = async ({ controller }) => {
307
287
 
308
288
  const value = settings?.[ field.key ];
309
289
 
310
- if( field.env && value ) credentials[ field.env ] = value;
290
+ if( field.credential && value ) credentials[ field.credential ] = value;
311
291
 
312
292
  }
313
293
 
@@ -1,7 +1,9 @@
1
- import { mergeSettings } from './connections/index.js';
1
+ import { connections, mergeSettings } from './connections/index.js';
2
2
  import { decrypt, encrypt } from './encrypt.js';
3
3
  import './connections/oauth.js';
4
4
  import 'node:crypto';
5
+ import './phone.js';
6
+ import 'libphonenumber-js';
5
7
  import './http.js';
6
8
  import './pricing.js';
7
9
  import './plans.js';
@@ -16,8 +18,6 @@ import './billing.js';
16
18
  import './transactions.js';
17
19
  import '@drawbridge/drawbridge-telemetry';
18
20
  import './email.js';
19
- import './phone.js';
20
- import 'libphonenumber-js';
21
21
  import './safe-http.js';
22
22
  import 'dns';
23
23
  import 'node:http';
@@ -42,60 +42,41 @@ import './token.js';
42
42
  // the others have already rotated away.
43
43
 
44
44
 
45
- // WHAT AN ADMIN TYPES IN, per vendor.
45
+ // WHAT AN ADMIN TYPES IN, per vendor — READ FROM THE MANIFESTS.
46
46
  //
47
- // `redact` marks a secret: never returned by the api, and blank on save means
48
- // keep the stored value. `required` drives the live check. `input` picks the
49
- // form control.
47
+ // This was a table here, duplicating what every vendor file already knew. A
48
+ // vendor's credentials and its `requires` name the same things, and keeping
49
+ // them in two files meant a manifest could require a name that no admin screen
50
+ // offered to fill — the exact shape of "declared in one place, entered in
51
+ // another" these manifests exist to close.
50
52
  //
51
- // These move onto the manifests themselves as `provider : { fields }` in the
52
- // follow-up plan, at which point providerFields() delegates to the manifest and
53
- // this constant goes. It is one table for now because a vendor's credentials and
54
- // its manifest are declared in the same package either way.
55
- const FIELDS = {
56
- attentive : [
57
- { input : 'text', key : 'clientId', env : 'ATTENTIVE_OAUTH_CLIENT_ID', label : 'Client ID', required : true },
58
- { input : 'password', key : 'clientSecret', env : 'ATTENTIVE_OAUTH_CLIENT_SECRET', label : 'Client secret', redact : true, required : true }
59
- ],
60
- drawbridge : [
61
- { input : 'email', key : 'accountSender', env : 'SENDGRID_FROM_ADDRESS', label : 'Account sender', message : 'Verification codes and security alerts send from here.', required : true },
62
- { input : 'password', key : 'apiKey', env : 'SENDGRID_API_KEY', label : 'SendGrid API key', redact : true, required : true },
63
- // NOT required. The CRM sync is best-effort internal tooling and no-ops
64
- // without a token — requiring it would make the whole drawbridge provider
65
- // read not-live over something no merchant ever sees.
66
- { input : 'password', key : 'hubspotToken', env : 'HUBSPOT_ACCESS_TOKEN', label : 'HubSpot access token', message : 'Drawbridge\'s own CRM portal. Internal — no merchant sees this.', redact : true },
67
- // Optional: SENDGRID_SEND_FROM_ADDRESS is not boot-required in sync
68
- // either. Unset, it degrades to the account sender rather than
69
- // refusing to start.
70
- { input : 'email', key : 'leadSender', env : 'SENDGRID_SEND_FROM_ADDRESS', label : 'Lead sender', message : 'The default for lead-facing mail when a merchant has not verified their own domain.' },
71
- { input : 'text', key : 'smsFrom', env : 'TWILIO_ACCOUNT_FROM', label : 'SMS number', required : true },
72
- { input : 'password', key : 'smsSid', env : 'TWILIO_ACCOUNT_SID', label : 'Twilio account SID', redact : true, required : true },
73
- { input : 'password', key : 'smsToken', env : 'TWILIO_AUTH_TOKEN', label : 'Twilio auth token', redact : true, required : true }
74
- ],
75
- klaviyo : [
76
- { input : 'text', key : 'clientId', env : 'KLAVIYO_OAUTH_CLIENT_ID', label : 'Client ID', required : true },
77
- { input : 'password', key : 'clientSecret', env : 'KLAVIYO_OAUTH_CLIENT_SECRET', label : 'Client secret', redact : true, required : true }
78
- ],
79
- mailchimp : [
80
- { input : 'text', key : 'clientId', env : 'MAILCHIMP_OAUTH_CLIENT_ID', label : 'Client ID', required : true },
81
- { input : 'password', key : 'clientSecret', env : 'MAILCHIMP_OAUTH_CLIENT_SECRET', label : 'Client secret', redact : true, required : true }
82
- ],
83
- shopify : [
84
- { input : 'text', key : 'apiKey', env : 'SHOPIFY_API_KEY', label : 'API key', required : true },
85
- { input : 'password', key : 'apiSecret', env : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
86
- { input : 'text', key : 'appHandle', env : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
87
- { input : 'text', key : 'listingUrl', env : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true }
88
- ]
89
- };
90
-
91
- // Object.hasOwn, not `FIELDS[ slug ] ||` — a bare property read walks the
92
- // prototype chain, and FIELDS[ 'constructor' ] is a function whose .length is
93
- // 1, so the route's "does this vendor exist" guard passed for it and reached
53
+ // Each field's `credential` is the NAME the rest of the system asks for that
54
+ // value by. It is not an environment variable and there is no fallback to one:
55
+ // the names simply began life as env vars and were kept as the vocabulary, so
56
+ // `requires` and auth.oauth.client did not have to change when the storage
57
+ // moved into the database.
58
+ //
59
+ // So the declaration lives on the manifest as `provider : { fields }`, build()
60
+ // checks its shape at import, and this module is now only the reading, memoizing
61
+ // and encrypting around it.
62
+ //
63
+ // Object.hasOwn, not `connections[ slug ] ||` a bare property read walks the
64
+ // prototype chain, and connections[ 'constructor' ] is a function whose .length
65
+ // is 1, so the route's "does this vendor exist" guard passed for it and reached
94
66
  // the database before throwing.
95
- const providerFields = ( slug ) => Object.hasOwn( FIELDS, slug ) ? FIELDS[ slug ] : [];
67
+ const providerFields = ( slug ) => (
68
+ Object.hasOwn( connections, slug ) ? ( connections[ slug ].provider?.fields || [] ) : []
69
+ );
96
70
 
97
71
  // The vendors an admin can configure, in a stable order.
98
- const providerSlugs = () => Object.keys( FIELDS ).sort();
72
+ //
73
+ // DERIVED FROM WHO DECLARES A PROVIDER BLOCK, not from the catalog: `webhook` is
74
+ // a connection with no vendor behind it — nothing to authenticate against and
75
+ // nothing to type — so it must not appear on a screen whose whole purpose is
76
+ // entering credentials.
77
+ const providerSlugs = () => Object.keys( connections )
78
+ .filter( ( slug ) => connections[ slug ].provider )
79
+ .sort();
99
80
 
100
81
  // LIVE IS DERIVED, never stored. A stored flag is a second thing to keep in
101
82
  // sync and goes stale the moment a row is edited by hand.
@@ -270,17 +251,16 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
270
251
  const providerEnvNames = () => new Set(
271
252
  providerSlugs()
272
253
  .flatMap( ( slug ) => providerFields( slug ) )
273
- .map( ( field ) => field.env )
254
+ .map( ( field ) => field.credential )
274
255
  .filter( Boolean )
275
256
  );
276
257
 
277
- // EVERY STORED CREDENTIAL, SHAPED LIKE AN ENVIRONMENT.
258
+ // EVERY STORED CREDENTIAL, KEYED BY ITS NAME.
278
259
  //
279
- // Keyed by the env var each field replaces, because that is the shape the
280
- // manifests already speak: `requires` names env vars, and auth.oauth.client
281
- // names them too. Handing availableConnections this map instead of process.env
282
- // is the whole switch — the package does not change, and 137 call sites in this
283
- // repo keep reading a plain synchronous object.
260
+ // Keyed by each field's `credential`, because that is the vocabulary the
261
+ // manifests already speak: `requires` names credentials, and auth.oauth.client
262
+ // names them too. Handing availableConnections this map is what makes stored
263
+ // credentials answer the question the manifests were already asking.
284
264
  //
285
265
  // A field with no value is OMITTED rather than set empty, so `requires` sees
286
266
  // the same absence it would for an unset variable.
@@ -307,7 +287,7 @@ const providerCredentials = async ({ controller }) => {
307
287
 
308
288
  const value = settings?.[ field.key ];
309
289
 
310
- if( field.env && value ) credentials[ field.env ] = value;
290
+ if( field.credential && value ) credentials[ field.credential ] = value;
311
291
 
312
292
  }
313
293