@drawbridge/drawbridge-utils 0.0.118 → 0.0.121
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connections/index.cjs +1182 -75
- package/dist/connections/index.d.cts +1832 -95
- package/dist/connections/index.d.ts +1832 -95
- package/dist/connections/index.js +1175 -73
- package/dist/phone.d.cts +1 -1
- package/dist/phone.d.ts +1 -1
- package/dist/providers.cjs +1154 -95
- package/dist/providers.d.cts +86 -77
- package/dist/providers.d.ts +86 -77
- package/dist/providers.js +1150 -91
- package/dist/sendgrid.cjs +4 -6
- package/dist/sendgrid.d.cts +19 -14
- package/dist/sendgrid.d.ts +19 -14
- package/dist/sendgrid.js +4 -6
- package/dist/twilio.cjs +2 -3
- package/dist/twilio.d.cts +11 -3
- package/dist/twilio.d.ts +11 -3
- package/dist/twilio.js +2 -3
- package/package.json +1 -1
package/dist/providers.d.cts
CHANGED
|
@@ -15,6 +15,9 @@ import './usage.cjs';
|
|
|
15
15
|
import './billing.cjs';
|
|
16
16
|
import './transactions.cjs';
|
|
17
17
|
import '@drawbridge/drawbridge-telemetry';
|
|
18
|
+
import './email.cjs';
|
|
19
|
+
import './phone.cjs';
|
|
20
|
+
import 'libphonenumber-js';
|
|
18
21
|
import './safe-http.cjs';
|
|
19
22
|
import 'dns';
|
|
20
23
|
import 'node:http';
|
|
@@ -120,42 +123,68 @@ const isLive = ( slug, settings ) => {
|
|
|
120
123
|
// Fixed-width dots rather than one per character, so the length does not travel
|
|
121
124
|
// either. Below the threshold a secret is short enough that a prefix and suffix
|
|
122
125
|
// would be most of it, so nothing is revealed.
|
|
126
|
+
//
|
|
127
|
+
// SIXTEEN, not twelve. The reveal is 3 + 4 = 7 characters, so a twelve-character
|
|
128
|
+
// value handed back more than half of itself while passing a check whose whole
|
|
129
|
+
// point was that it did not — the threshold has to be comfortably more than
|
|
130
|
+
// twice what is shown for the reasoning above to hold.
|
|
123
131
|
const mask = ( value ) => {
|
|
124
132
|
|
|
125
133
|
if( ! value ) return null;
|
|
126
134
|
|
|
127
|
-
if( String( value ).length <
|
|
135
|
+
if( String( value ).length < 16 ) return '••••••••';
|
|
128
136
|
|
|
129
137
|
return String( value ).slice( 0, 3 ) + '••••••••' + String( value ).slice( -4 );
|
|
130
138
|
|
|
131
139
|
};
|
|
132
140
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
141
|
+
// A MEMO, NOT A CACHE, and the difference is the whole point. Shared Redis made
|
|
142
|
+
// one process's view of a credential another process's problem: a bust from the
|
|
143
|
+
// api had to reach sync and webhooks before either served a key someone had just
|
|
144
|
+
// rotated because it leaked, and cache.use has no compare-and-set to make that
|
|
145
|
+
// safe. Per-process state cannot be stale for anyone but the process holding it,
|
|
146
|
+
// and it expires on its own.
|
|
147
|
+
//
|
|
148
|
+
// slug -> { at, value }, where value is the DECRYPTED settings. Exported only so
|
|
149
|
+
// tests can age an entry without sleeping; nothing else should read it.
|
|
150
|
+
const providerMemo = new Map();
|
|
151
|
+
|
|
152
|
+
// SIXTY SECONDS, chosen for the human rather than for the load. The read is one
|
|
153
|
+
// indexed document and a decrypt — near enough free that memoizing it longer
|
|
154
|
+
// buys nothing worth the staleness. The moment that actually matters is the one
|
|
155
|
+
// right after an admin first types a credential in and goes looking for the
|
|
156
|
+
// vendor to come alive: they are watching, and a minute is the longest that wait
|
|
157
|
+
// should ever be.
|
|
158
|
+
const MEMO_TTL_MS = 60 * 1000;
|
|
159
|
+
|
|
160
|
+
// For tests. A save clears its own slug; this clears everything.
|
|
161
|
+
const clearProviderMemo = () => providerMemo.clear();
|
|
162
|
+
|
|
163
|
+
// Read a vendor's credentials, memoized, because this is on the path of every
|
|
140
164
|
// OAuth callback and every send.
|
|
141
165
|
//
|
|
142
166
|
// An unconfigured vendor answers {} rather than throwing: the admin list has to
|
|
143
167
|
// render the row that lets someone fix it.
|
|
144
168
|
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
169
|
+
// The memoized object is handed to every reader inside the window rather than
|
|
170
|
+
// copied per read — no caller mutates its credentials, and defending against one
|
|
171
|
+
// that does not exist would cost a clone on every send.
|
|
172
|
+
const providerSettings = async ({ controller, slug }) => {
|
|
173
|
+
|
|
174
|
+
const memoized = providerMemo.get( slug );
|
|
175
|
+
|
|
176
|
+
if( memoized && Date.now() - memoized.at < MEMO_TTL_MS ) return memoized.value;
|
|
148
177
|
|
|
149
|
-
const
|
|
178
|
+
const row = await controller.get({
|
|
150
179
|
collection : 'provider',
|
|
151
180
|
query : { slug }
|
|
152
181
|
});
|
|
153
182
|
|
|
154
|
-
|
|
155
|
-
// requiring a cache there would mean inventing one that never caches.
|
|
156
|
-
const row = cache ? await cache.use( cacheKey( slug ), read, 30 ) : await read();
|
|
183
|
+
const value = row?.settings ? decrypt( row.settings ) : {};
|
|
157
184
|
|
|
158
|
-
|
|
185
|
+
providerMemo.set( slug, { at : Date.now(), value });
|
|
186
|
+
|
|
187
|
+
return value;
|
|
159
188
|
|
|
160
189
|
};
|
|
161
190
|
|
|
@@ -168,7 +197,7 @@ const providerSettings = async ({ cache, controller, slug }) => {
|
|
|
168
197
|
// BLANK MEANS KEEP. A redacted field is never returned by the GET, so an
|
|
169
198
|
// unchanged form posts it back empty; treating that as a clear would wipe a
|
|
170
199
|
// credential every time someone edited the field next to it.
|
|
171
|
-
const saveProviderSettings = async ({ authenticated,
|
|
200
|
+
const saveProviderSettings = async ({ authenticated, clear, controller, settings, slug }) => {
|
|
172
201
|
|
|
173
202
|
const fields = providerFields( slug );
|
|
174
203
|
|
|
@@ -217,33 +246,20 @@ const saveProviderSettings = async ({ authenticated, cache, clear, controller, s
|
|
|
217
246
|
query : { slug }
|
|
218
247
|
});
|
|
219
248
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// the
|
|
249
|
+
// THE PROCESS THAT TOOK THE SAVE IS CORRECT IMMEDIATELY. Without this the
|
|
250
|
+
// admin who just typed the credential in reads back the list this same
|
|
251
|
+
// process memoized a moment ago and sees the vendor still not live, for up to
|
|
252
|
+
// a minute, with nothing to do about it.
|
|
223
253
|
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
// bounds that — seconds, not the default — because this is the Redis all
|
|
229
|
-
// three services share and the value is a credential someone may have just
|
|
230
|
-
// rotated BECAUSE it leaked.
|
|
231
|
-
await cache.delete( cacheKey( slug ) );
|
|
254
|
+
// The other processes are not covered and do not need to be: their memos age
|
|
255
|
+
// out on their own within the window, which is the trade a per-process memo
|
|
256
|
+
// buys in exchange for never needing a bust to travel between services.
|
|
257
|
+
providerMemo.delete( slug );
|
|
232
258
|
|
|
233
259
|
return result;
|
|
234
260
|
|
|
235
261
|
};
|
|
236
262
|
|
|
237
|
-
// EVERY STORED CREDENTIAL, SHAPED LIKE AN ENVIRONMENT.
|
|
238
|
-
//
|
|
239
|
-
// Keyed by the env var each field replaces, because that is the shape the
|
|
240
|
-
// manifests already speak: `requires` names env vars, and auth.oauth.client
|
|
241
|
-
// names them too. Handing availableConnections this map instead of process.env
|
|
242
|
-
// is the whole switch — the package does not change, and 137 call sites in this
|
|
243
|
-
// repo keep reading a plain synchronous object.
|
|
244
|
-
//
|
|
245
|
-
// A field with no value is OMITTED rather than set empty, so `requires` sees
|
|
246
|
-
// the same absence it would for an unset variable.
|
|
247
263
|
// EVERY ENV NAME THE PROVIDER SCREEN OWNS.
|
|
248
264
|
//
|
|
249
265
|
// The boundary between "an admin types this in" and "the deployment supplies
|
|
@@ -258,60 +274,53 @@ const providerEnvNames = () => new Set(
|
|
|
258
274
|
.filter( Boolean )
|
|
259
275
|
);
|
|
260
276
|
|
|
261
|
-
|
|
277
|
+
// EVERY STORED CREDENTIAL, SHAPED LIKE AN ENVIRONMENT.
|
|
278
|
+
//
|
|
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.
|
|
284
|
+
//
|
|
285
|
+
// A field with no value is OMITTED rather than set empty, so `requires` sees
|
|
286
|
+
// the same absence it would for an unset variable.
|
|
287
|
+
const providerCredentials = async ({ controller }) => {
|
|
262
288
|
|
|
263
289
|
const credentials = {};
|
|
264
290
|
|
|
265
291
|
for( const slug of providerSlugs() ){
|
|
266
292
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
293
|
+
// ONE BAD ROW COSTS ONE VENDOR. decrypt throws on a value that was written
|
|
294
|
+
// under a different ENCRYPT_CONNECTION_SECRET, and unguarded that throw
|
|
295
|
+
// escaped the whole loop — a single half-rotated row read as a platform
|
|
296
|
+
// with no SendGrid, no Twilio and no Shopify either, which is a far worse
|
|
297
|
+
// outage than the one it came from.
|
|
298
|
+
//
|
|
299
|
+
// Degrading to "that vendor is unconfigured" lands in a state the product
|
|
300
|
+
// already has an answer for: the admin screen renders the row as not live,
|
|
301
|
+
// which is exactly where someone goes to fix it.
|
|
302
|
+
try {
|
|
270
303
|
|
|
271
|
-
const
|
|
304
|
+
const settings = await providerSettings({ controller, slug });
|
|
272
305
|
|
|
273
|
-
|
|
306
|
+
for( const field of providerFields( slug ) ){
|
|
274
307
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
308
|
+
const value = settings?.[ field.key ];
|
|
278
309
|
|
|
279
|
-
|
|
310
|
+
if( field.env && value ) credentials[ field.env ] = value;
|
|
280
311
|
|
|
281
|
-
}
|
|
312
|
+
}
|
|
282
313
|
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
// The apps are not the only readers. @drawbridge/shopify verifies every webhook
|
|
286
|
-
// HMAC against process.env.SHOPIFY_API_SECRET, drawbridge-utils' own sendgrid
|
|
287
|
-
// and twilio wrappers default to process.env, and drawbridge-sync freezes its
|
|
288
|
-
// step→queue routing table from process.env at require time. Threading a
|
|
289
|
-
// credentials object through all of that would be a sweep across three repos and
|
|
290
|
-
// a published package, and every site it missed would fail silently.
|
|
291
|
-
//
|
|
292
|
-
// So the environment stays the transport and the collection becomes the SOURCE:
|
|
293
|
-
// this writes the stored values into process.env before anything reads them.
|
|
294
|
-
//
|
|
295
|
-
// IT DELETES FIRST. Every name a provider field declares is removed whether or
|
|
296
|
-
// not the database has a value for it, so a variable left behind in a deployment
|
|
297
|
-
// cannot keep a vendor alive after its credential was taken out of the database
|
|
298
|
-
// — which would leave the connection reading available and failing on use.
|
|
299
|
-
// Names no provider declares are untouched: ENCRYPT_CONNECTION_SECRET is the
|
|
300
|
-
// clearest case, since it is the key this collection is decrypted with and can
|
|
301
|
-
// never be stored inside it.
|
|
302
|
-
//
|
|
303
|
-
// Returns the names it set, so a caller can log what it loaded without logging
|
|
304
|
-
// what it loaded them to.
|
|
305
|
-
const hydrateEnvironment = async ({ cache, controller, env = process.env }) => {
|
|
314
|
+
} catch {
|
|
306
315
|
|
|
307
|
-
|
|
316
|
+
continue;
|
|
308
317
|
|
|
309
|
-
|
|
318
|
+
}
|
|
310
319
|
|
|
311
|
-
|
|
320
|
+
}
|
|
312
321
|
|
|
313
|
-
return
|
|
322
|
+
return credentials;
|
|
314
323
|
|
|
315
324
|
};
|
|
316
325
|
|
|
317
|
-
export {
|
|
326
|
+
export { clearProviderMemo, isLive, mask, providerCredentials, providerEnvNames, providerFields, providerMemo, providerSettings, providerSlugs, saveProviderSettings };
|
package/dist/providers.d.ts
CHANGED
|
@@ -15,6 +15,9 @@ import './usage.js';
|
|
|
15
15
|
import './billing.js';
|
|
16
16
|
import './transactions.js';
|
|
17
17
|
import '@drawbridge/drawbridge-telemetry';
|
|
18
|
+
import './email.js';
|
|
19
|
+
import './phone.js';
|
|
20
|
+
import 'libphonenumber-js';
|
|
18
21
|
import './safe-http.js';
|
|
19
22
|
import 'dns';
|
|
20
23
|
import 'node:http';
|
|
@@ -120,42 +123,68 @@ const isLive = ( slug, settings ) => {
|
|
|
120
123
|
// Fixed-width dots rather than one per character, so the length does not travel
|
|
121
124
|
// either. Below the threshold a secret is short enough that a prefix and suffix
|
|
122
125
|
// would be most of it, so nothing is revealed.
|
|
126
|
+
//
|
|
127
|
+
// SIXTEEN, not twelve. The reveal is 3 + 4 = 7 characters, so a twelve-character
|
|
128
|
+
// value handed back more than half of itself while passing a check whose whole
|
|
129
|
+
// point was that it did not — the threshold has to be comfortably more than
|
|
130
|
+
// twice what is shown for the reasoning above to hold.
|
|
123
131
|
const mask = ( value ) => {
|
|
124
132
|
|
|
125
133
|
if( ! value ) return null;
|
|
126
134
|
|
|
127
|
-
if( String( value ).length <
|
|
135
|
+
if( String( value ).length < 16 ) return '••••••••';
|
|
128
136
|
|
|
129
137
|
return String( value ).slice( 0, 3 ) + '••••••••' + String( value ).slice( -4 );
|
|
130
138
|
|
|
131
139
|
};
|
|
132
140
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
//
|
|
141
|
+
// A MEMO, NOT A CACHE, and the difference is the whole point. Shared Redis made
|
|
142
|
+
// one process's view of a credential another process's problem: a bust from the
|
|
143
|
+
// api had to reach sync and webhooks before either served a key someone had just
|
|
144
|
+
// rotated because it leaked, and cache.use has no compare-and-set to make that
|
|
145
|
+
// safe. Per-process state cannot be stale for anyone but the process holding it,
|
|
146
|
+
// and it expires on its own.
|
|
147
|
+
//
|
|
148
|
+
// slug -> { at, value }, where value is the DECRYPTED settings. Exported only so
|
|
149
|
+
// tests can age an entry without sleeping; nothing else should read it.
|
|
150
|
+
const providerMemo = new Map();
|
|
151
|
+
|
|
152
|
+
// SIXTY SECONDS, chosen for the human rather than for the load. The read is one
|
|
153
|
+
// indexed document and a decrypt — near enough free that memoizing it longer
|
|
154
|
+
// buys nothing worth the staleness. The moment that actually matters is the one
|
|
155
|
+
// right after an admin first types a credential in and goes looking for the
|
|
156
|
+
// vendor to come alive: they are watching, and a minute is the longest that wait
|
|
157
|
+
// should ever be.
|
|
158
|
+
const MEMO_TTL_MS = 60 * 1000;
|
|
159
|
+
|
|
160
|
+
// For tests. A save clears its own slug; this clears everything.
|
|
161
|
+
const clearProviderMemo = () => providerMemo.clear();
|
|
162
|
+
|
|
163
|
+
// Read a vendor's credentials, memoized, because this is on the path of every
|
|
140
164
|
// OAuth callback and every send.
|
|
141
165
|
//
|
|
142
166
|
// An unconfigured vendor answers {} rather than throwing: the admin list has to
|
|
143
167
|
// render the row that lets someone fix it.
|
|
144
168
|
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
169
|
+
// The memoized object is handed to every reader inside the window rather than
|
|
170
|
+
// copied per read — no caller mutates its credentials, and defending against one
|
|
171
|
+
// that does not exist would cost a clone on every send.
|
|
172
|
+
const providerSettings = async ({ controller, slug }) => {
|
|
173
|
+
|
|
174
|
+
const memoized = providerMemo.get( slug );
|
|
175
|
+
|
|
176
|
+
if( memoized && Date.now() - memoized.at < MEMO_TTL_MS ) return memoized.value;
|
|
148
177
|
|
|
149
|
-
const
|
|
178
|
+
const row = await controller.get({
|
|
150
179
|
collection : 'provider',
|
|
151
180
|
query : { slug }
|
|
152
181
|
});
|
|
153
182
|
|
|
154
|
-
|
|
155
|
-
// requiring a cache there would mean inventing one that never caches.
|
|
156
|
-
const row = cache ? await cache.use( cacheKey( slug ), read, 30 ) : await read();
|
|
183
|
+
const value = row?.settings ? decrypt( row.settings ) : {};
|
|
157
184
|
|
|
158
|
-
|
|
185
|
+
providerMemo.set( slug, { at : Date.now(), value });
|
|
186
|
+
|
|
187
|
+
return value;
|
|
159
188
|
|
|
160
189
|
};
|
|
161
190
|
|
|
@@ -168,7 +197,7 @@ const providerSettings = async ({ cache, controller, slug }) => {
|
|
|
168
197
|
// BLANK MEANS KEEP. A redacted field is never returned by the GET, so an
|
|
169
198
|
// unchanged form posts it back empty; treating that as a clear would wipe a
|
|
170
199
|
// credential every time someone edited the field next to it.
|
|
171
|
-
const saveProviderSettings = async ({ authenticated,
|
|
200
|
+
const saveProviderSettings = async ({ authenticated, clear, controller, settings, slug }) => {
|
|
172
201
|
|
|
173
202
|
const fields = providerFields( slug );
|
|
174
203
|
|
|
@@ -217,33 +246,20 @@ const saveProviderSettings = async ({ authenticated, cache, clear, controller, s
|
|
|
217
246
|
query : { slug }
|
|
218
247
|
});
|
|
219
248
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
// the
|
|
249
|
+
// THE PROCESS THAT TOOK THE SAVE IS CORRECT IMMEDIATELY. Without this the
|
|
250
|
+
// admin who just typed the credential in reads back the list this same
|
|
251
|
+
// process memoized a moment ago and sees the vendor still not live, for up to
|
|
252
|
+
// a minute, with nothing to do about it.
|
|
223
253
|
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
// bounds that — seconds, not the default — because this is the Redis all
|
|
229
|
-
// three services share and the value is a credential someone may have just
|
|
230
|
-
// rotated BECAUSE it leaked.
|
|
231
|
-
await cache.delete( cacheKey( slug ) );
|
|
254
|
+
// The other processes are not covered and do not need to be: their memos age
|
|
255
|
+
// out on their own within the window, which is the trade a per-process memo
|
|
256
|
+
// buys in exchange for never needing a bust to travel between services.
|
|
257
|
+
providerMemo.delete( slug );
|
|
232
258
|
|
|
233
259
|
return result;
|
|
234
260
|
|
|
235
261
|
};
|
|
236
262
|
|
|
237
|
-
// EVERY STORED CREDENTIAL, SHAPED LIKE AN ENVIRONMENT.
|
|
238
|
-
//
|
|
239
|
-
// Keyed by the env var each field replaces, because that is the shape the
|
|
240
|
-
// manifests already speak: `requires` names env vars, and auth.oauth.client
|
|
241
|
-
// names them too. Handing availableConnections this map instead of process.env
|
|
242
|
-
// is the whole switch — the package does not change, and 137 call sites in this
|
|
243
|
-
// repo keep reading a plain synchronous object.
|
|
244
|
-
//
|
|
245
|
-
// A field with no value is OMITTED rather than set empty, so `requires` sees
|
|
246
|
-
// the same absence it would for an unset variable.
|
|
247
263
|
// EVERY ENV NAME THE PROVIDER SCREEN OWNS.
|
|
248
264
|
//
|
|
249
265
|
// The boundary between "an admin types this in" and "the deployment supplies
|
|
@@ -258,60 +274,53 @@ const providerEnvNames = () => new Set(
|
|
|
258
274
|
.filter( Boolean )
|
|
259
275
|
);
|
|
260
276
|
|
|
261
|
-
|
|
277
|
+
// EVERY STORED CREDENTIAL, SHAPED LIKE AN ENVIRONMENT.
|
|
278
|
+
//
|
|
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.
|
|
284
|
+
//
|
|
285
|
+
// A field with no value is OMITTED rather than set empty, so `requires` sees
|
|
286
|
+
// the same absence it would for an unset variable.
|
|
287
|
+
const providerCredentials = async ({ controller }) => {
|
|
262
288
|
|
|
263
289
|
const credentials = {};
|
|
264
290
|
|
|
265
291
|
for( const slug of providerSlugs() ){
|
|
266
292
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
293
|
+
// ONE BAD ROW COSTS ONE VENDOR. decrypt throws on a value that was written
|
|
294
|
+
// under a different ENCRYPT_CONNECTION_SECRET, and unguarded that throw
|
|
295
|
+
// escaped the whole loop — a single half-rotated row read as a platform
|
|
296
|
+
// with no SendGrid, no Twilio and no Shopify either, which is a far worse
|
|
297
|
+
// outage than the one it came from.
|
|
298
|
+
//
|
|
299
|
+
// Degrading to "that vendor is unconfigured" lands in a state the product
|
|
300
|
+
// already has an answer for: the admin screen renders the row as not live,
|
|
301
|
+
// which is exactly where someone goes to fix it.
|
|
302
|
+
try {
|
|
270
303
|
|
|
271
|
-
const
|
|
304
|
+
const settings = await providerSettings({ controller, slug });
|
|
272
305
|
|
|
273
|
-
|
|
306
|
+
for( const field of providerFields( slug ) ){
|
|
274
307
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
308
|
+
const value = settings?.[ field.key ];
|
|
278
309
|
|
|
279
|
-
|
|
310
|
+
if( field.env && value ) credentials[ field.env ] = value;
|
|
280
311
|
|
|
281
|
-
}
|
|
312
|
+
}
|
|
282
313
|
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
// The apps are not the only readers. @drawbridge/shopify verifies every webhook
|
|
286
|
-
// HMAC against process.env.SHOPIFY_API_SECRET, drawbridge-utils' own sendgrid
|
|
287
|
-
// and twilio wrappers default to process.env, and drawbridge-sync freezes its
|
|
288
|
-
// step→queue routing table from process.env at require time. Threading a
|
|
289
|
-
// credentials object through all of that would be a sweep across three repos and
|
|
290
|
-
// a published package, and every site it missed would fail silently.
|
|
291
|
-
//
|
|
292
|
-
// So the environment stays the transport and the collection becomes the SOURCE:
|
|
293
|
-
// this writes the stored values into process.env before anything reads them.
|
|
294
|
-
//
|
|
295
|
-
// IT DELETES FIRST. Every name a provider field declares is removed whether or
|
|
296
|
-
// not the database has a value for it, so a variable left behind in a deployment
|
|
297
|
-
// cannot keep a vendor alive after its credential was taken out of the database
|
|
298
|
-
// — which would leave the connection reading available and failing on use.
|
|
299
|
-
// Names no provider declares are untouched: ENCRYPT_CONNECTION_SECRET is the
|
|
300
|
-
// clearest case, since it is the key this collection is decrypted with and can
|
|
301
|
-
// never be stored inside it.
|
|
302
|
-
//
|
|
303
|
-
// Returns the names it set, so a caller can log what it loaded without logging
|
|
304
|
-
// what it loaded them to.
|
|
305
|
-
const hydrateEnvironment = async ({ cache, controller, env = process.env }) => {
|
|
314
|
+
} catch {
|
|
306
315
|
|
|
307
|
-
|
|
316
|
+
continue;
|
|
308
317
|
|
|
309
|
-
|
|
318
|
+
}
|
|
310
319
|
|
|
311
|
-
|
|
320
|
+
}
|
|
312
321
|
|
|
313
|
-
return
|
|
322
|
+
return credentials;
|
|
314
323
|
|
|
315
324
|
};
|
|
316
325
|
|
|
317
|
-
export {
|
|
326
|
+
export { clearProviderMemo, isLive, mask, providerCredentials, providerEnvNames, providerFields, providerMemo, providerSettings, providerSlugs, saveProviderSettings };
|