@drawbridge/drawbridge-utils 0.0.108 → 0.0.109
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 +196 -6
- package/dist/connections/index.d.cts +309 -7
- package/dist/connections/index.d.ts +309 -7
- package/dist/connections/index.js +192 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { refresh } from './oauth.cjs';
|
|
2
|
+
export { consentUrl, exchange, pkcePair } from './oauth.cjs';
|
|
2
3
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
3
4
|
|
|
4
5
|
// THE HOOK VOCABULARY. Closed, and every connection answers all of it.
|
|
@@ -190,6 +191,113 @@ const INPUTS = Object.freeze([
|
|
|
190
191
|
'url'
|
|
191
192
|
]);
|
|
192
193
|
|
|
194
|
+
// A LIVE ACCESS TOKEN, for any vendor that issues an expiring one.
|
|
195
|
+
//
|
|
196
|
+
// Written once here rather than per vendor, because every OAuth connection ends
|
|
197
|
+
// up needing it and the failure when it is missing is the same everywhere: a
|
|
198
|
+
// call that worked in testing and returns 401 an hour later.
|
|
199
|
+
//
|
|
200
|
+
// REFRESH BEFORE USE, not on a schedule. Correctness cannot depend on a job
|
|
201
|
+
// having run — a worker that was down, a queue that was drained, a connection
|
|
202
|
+
// nobody touched for a week all leave a scheduled refresh behind, and the next
|
|
203
|
+
// call fails. Doing it at the point of use also does work proportional to what
|
|
204
|
+
// is actually used rather than to how many connections exist.
|
|
205
|
+
//
|
|
206
|
+
// The scheduled probe (auth.probe, dispatched from sync) is a SEPARATE concern
|
|
207
|
+
// and both are needed: this one keeps calls working, that one keeps the grant
|
|
208
|
+
// alive against a vendor's idle window and reports health honestly.
|
|
209
|
+
|
|
210
|
+
// How long before real expiry a token counts as stale. Tokens are minted with a
|
|
211
|
+
// lifetime measured in the vendor's clock and spent against ours, and a request
|
|
212
|
+
// takes time to arrive — a token expiring in ten seconds is already too late for
|
|
213
|
+
// a call that has to travel.
|
|
214
|
+
const SKEW_SECONDS = 120;
|
|
215
|
+
|
|
216
|
+
const isStale = ( settings, now = Date.now() ) => {
|
|
217
|
+
|
|
218
|
+
// No expiry recorded means the vendor issues tokens that do not expire
|
|
219
|
+
// (Mailchimp) or a grant stored before expiry was tracked. Either way there
|
|
220
|
+
// is nothing to refresh toward, and treating unknown as stale would spend a
|
|
221
|
+
// refresh token on every single call.
|
|
222
|
+
if( ! settings?.expiresAt ) return false;
|
|
223
|
+
|
|
224
|
+
return new Date( settings.expiresAt ).getTime() - ( SKEW_SECONDS * 1000 ) <= now;
|
|
225
|
+
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Compute what to store from a mint. Exported because the OAuth callback stores
|
|
229
|
+
// the same shape at connect time, and two places deriving `expiresAt` from
|
|
230
|
+
// `expiresIn` separately is how they come to disagree.
|
|
231
|
+
const tokenSettings = ({ existing = {}, now = Date.now(), tokens }) => ({
|
|
232
|
+
accessToken : tokens.accessToken,
|
|
233
|
+
// A vendor that does not rotate its refresh token returns none on a refresh
|
|
234
|
+
// (Klaviyo). Keeping the existing one is what makes the NEXT refresh work —
|
|
235
|
+
// dropping it invalidates the grant one call later, nowhere near the cause.
|
|
236
|
+
...( ( tokens.refreshToken || existing.refreshToken ) && {
|
|
237
|
+
refreshToken : tokens.refreshToken || existing.refreshToken
|
|
238
|
+
}),
|
|
239
|
+
// Absent when the vendor issues non-expiring tokens, and absent is meaningful
|
|
240
|
+
// — isStale reads it as "nothing to refresh toward".
|
|
241
|
+
...( tokens.expiresIn && {
|
|
242
|
+
expiresAt : new Date( now + ( tokens.expiresIn * 1000 ) ).toISOString()
|
|
243
|
+
}),
|
|
244
|
+
...( tokens.scope && { scope : tokens.scope })
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Returns a token that is good right now, refreshing and persisting first if the
|
|
248
|
+
// stored one is spent.
|
|
249
|
+
//
|
|
250
|
+
// `save` is injected because this package cannot reach a database — the same
|
|
251
|
+
// reason `fetcher` is. It receives the settings to store and is responsible for
|
|
252
|
+
// encrypting them; forgetting to persist means a refresh on every call, which
|
|
253
|
+
// works but burns the vendor's rate limit and eventually trips it.
|
|
254
|
+
// `force` mints even when the stored token still looks good. That is what
|
|
255
|
+
// auth.probe needs: a probe that short-circuits on an unexpired token never
|
|
256
|
+
// reaches the vendor, so it reports healthy on a grant revoked an hour ago.
|
|
257
|
+
const accessToken = async ({
|
|
258
|
+
clientId,
|
|
259
|
+
clientSecret,
|
|
260
|
+
fetcher,
|
|
261
|
+
force = false,
|
|
262
|
+
manifest,
|
|
263
|
+
now = Date.now(),
|
|
264
|
+
save,
|
|
265
|
+
settings
|
|
266
|
+
} = {}) => {
|
|
267
|
+
|
|
268
|
+
if( ! settings?.accessToken && ! settings?.refreshToken ){
|
|
269
|
+
|
|
270
|
+
throw new Error( 'This connection holds no credential, so there is no token to use' );
|
|
271
|
+
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if( ! force && ! isStale( settings, now ) ) return settings.accessToken;
|
|
275
|
+
|
|
276
|
+
if( ! settings.refreshToken ){
|
|
277
|
+
|
|
278
|
+
// Expired with no way to mint another. Said plainly rather than returning
|
|
279
|
+
// a token the vendor will refuse — the merchant has to reconnect and the
|
|
280
|
+
// error should say so.
|
|
281
|
+
throw new Error( 'This connection has expired and cannot be renewed automatically. Reconnect it.' );
|
|
282
|
+
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const minted = await refresh({
|
|
286
|
+
clientId,
|
|
287
|
+
clientSecret,
|
|
288
|
+
descriptor : manifest.auth.oauth,
|
|
289
|
+
...( fetcher && { fetcher }),
|
|
290
|
+
refreshToken : settings.refreshToken
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const next = tokenSettings({ existing : settings, now, tokens : minted });
|
|
294
|
+
|
|
295
|
+
if( save ) await save( next );
|
|
296
|
+
|
|
297
|
+
return next.accessToken;
|
|
298
|
+
|
|
299
|
+
};
|
|
300
|
+
|
|
193
301
|
// Klaviyo, exported from the brand kit and left as authored — the fills are the
|
|
194
302
|
// vendor's own mark, not a recolour.
|
|
195
303
|
//
|
|
@@ -201,6 +309,38 @@ var icon$3 = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xm
|
|
|
201
309
|
<path d="M365.047 327.038H134.954V172.964H365.047L316.856 250.001L365.047 327.038Z" fill="#232121"/>
|
|
202
310
|
</svg>`;
|
|
203
311
|
|
|
312
|
+
// Klaviyo pins its API by DATE. A request without this header is refused, and
|
|
313
|
+
// one with an old date keeps the response shape that date shipped with — which
|
|
314
|
+
// is the point: bumping it is a deliberate act with a changelog to read, not
|
|
315
|
+
// something that drifts under us.
|
|
316
|
+
const REVISION = '2026-07-15';
|
|
317
|
+
|
|
318
|
+
const api = async ( path, { fetcher = fetch, token } ) => {
|
|
319
|
+
|
|
320
|
+
const response = await fetcher( 'https://a.klaviyo.com/api' + path, {
|
|
321
|
+
headers : {
|
|
322
|
+
// Bearer, not Klaviyo-API-Key — that header is for private keys, and
|
|
323
|
+
// sending it with an OAuth token fails in a way that reads like a bad
|
|
324
|
+
// token rather than a bad scheme.
|
|
325
|
+
authorization : 'Bearer ' + token,
|
|
326
|
+
revision : REVISION
|
|
327
|
+
},
|
|
328
|
+
signal : AbortSignal.timeout( 15000 )
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if( ! response.ok ){
|
|
332
|
+
|
|
333
|
+
throw Object.assign(
|
|
334
|
+
new Error( 'Klaviyo refused the request (' + response.status + ')' ),
|
|
335
|
+
{ status : response.status }
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return response.json();
|
|
341
|
+
|
|
342
|
+
};
|
|
343
|
+
|
|
204
344
|
// Klaviyo — contact and event sync, over OAuth.
|
|
205
345
|
//
|
|
206
346
|
// THE FIRST VENDOR ADDED AS A MANIFEST AND NOTHING ELSE. No form component, no
|
|
@@ -237,6 +377,11 @@ var klaviyo = {
|
|
|
237
377
|
// it is a fact about someone else's records rather than a string this
|
|
238
378
|
// code computes. Deriving one from a provider key produced
|
|
239
379
|
// redirect_uri_mismatch on a connection nobody had touched.
|
|
380
|
+
// Klaviyo drops a refresh token after 90 days of NON-USE. The vendor
|
|
381
|
+
// never mentions this at runtime — you discover it when a refresh fails
|
|
382
|
+
// on a connection nobody touched — so it is declared, and it is why
|
|
383
|
+
// auth.probe has to run on a schedule rather than only before a call.
|
|
384
|
+
idleExpiry : 90 * 24 * 60 * 60,
|
|
240
385
|
redirect : '/api/connection/klaviyo/callback',
|
|
241
386
|
// Space separated. accounts:read is required by Klaviyo on every app
|
|
242
387
|
// and must stay in the list; the rest are what a contact sync needs.
|
|
@@ -268,7 +413,92 @@ var klaviyo = {
|
|
|
268
413
|
label : 'Klaviyo account'
|
|
269
414
|
}
|
|
270
415
|
],
|
|
416
|
+
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
417
|
+
// live here rather than in sync. A vendor's own protocol belongs beside the
|
|
418
|
+
// vendor.
|
|
419
|
+
hooks : {
|
|
420
|
+
// Turn a fresh grant into settings worth showing. Without this the card
|
|
421
|
+
// renders an empty "Klaviyo account" field, because the merchant is never
|
|
422
|
+
// asked which account they connected — the consent already decided it and
|
|
423
|
+
// asking again would be a question we can answer ourselves.
|
|
424
|
+
'auth.connect' : async ({ fetcher, tokens }) => {
|
|
425
|
+
|
|
426
|
+
const body = await api( '/accounts', { fetcher, token : tokens.accessToken });
|
|
427
|
+
|
|
428
|
+
const account = body?.data?.[ 0 ];
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
account : account?.attributes?.contact_information?.organization_name || account?.id || null,
|
|
432
|
+
accountId : account?.id || null
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
},
|
|
436
|
+
// Revoke at KLAVIYO, not just locally. Forgetting our copy leaves the
|
|
437
|
+
// grant live in the merchant's account, so a disconnect that looks
|
|
438
|
+
// complete here still shows Drawbridge with access over there.
|
|
439
|
+
//
|
|
440
|
+
// Basic auth with our client, exactly like the token exchange — the token
|
|
441
|
+
// being revoked is the subject, not the credential.
|
|
442
|
+
'auth.disconnect' : async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
443
|
+
|
|
444
|
+
const token = settings?.refreshToken || settings?.accessToken;
|
|
445
|
+
|
|
446
|
+
if( ! token ) return { revoked : false };
|
|
447
|
+
|
|
448
|
+
const response = await fetcher( 'https://a.klaviyo.com/oauth/revoke', {
|
|
449
|
+
body : new URLSearchParams({
|
|
450
|
+
token,
|
|
451
|
+
token_type_hint : settings?.refreshToken ? 'refresh_token' : 'access_token'
|
|
452
|
+
}),
|
|
453
|
+
headers : {
|
|
454
|
+
authorization : 'Basic ' + Buffer.from( clientId + ':' + clientSecret ).toString( 'base64' ),
|
|
455
|
+
'content-type' : 'application/x-www-form-urlencoded'
|
|
456
|
+
},
|
|
457
|
+
method : 'POST',
|
|
458
|
+
signal : AbortSignal.timeout( 15000 )
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// A grant already revoked at the vendor answers non-2xx, and that is
|
|
462
|
+
// the outcome we wanted — surfacing it as a failure would leave a
|
|
463
|
+
// merchant unable to finish disconnecting.
|
|
464
|
+
return { revoked : response.ok };
|
|
465
|
+
|
|
466
|
+
},
|
|
467
|
+
// THE MINT IS THE PROBE. Asking "is this token still good" by inspecting
|
|
468
|
+
// what we stored answers the wrong question — a grant revoked inside
|
|
469
|
+
// Klaviyo still looks perfect in our database. Spending the refresh token
|
|
470
|
+
// is the only thing that asks Klaviyo.
|
|
471
|
+
//
|
|
472
|
+
// It also keeps the grant warm: Klaviyo expires a refresh token after 90
|
|
473
|
+
// days of NON-USE, so a connection nobody touches dies silently without
|
|
474
|
+
// this running.
|
|
475
|
+
'auth.probe' : async ({ clientId, clientSecret, fetcher, manifest, settings }) => {
|
|
476
|
+
|
|
477
|
+
const token = await accessToken({
|
|
478
|
+
clientId,
|
|
479
|
+
clientSecret,
|
|
480
|
+
fetcher,
|
|
481
|
+
// Mint even if the stored token still looks good — a probe that
|
|
482
|
+
// short-circuits never reaches Klaviyo and reports healthy on a
|
|
483
|
+
// grant revoked an hour ago.
|
|
484
|
+
force : true,
|
|
485
|
+
manifest,
|
|
486
|
+
settings
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return { ok : Boolean( token ) };
|
|
490
|
+
|
|
491
|
+
}
|
|
492
|
+
},
|
|
271
493
|
icon: icon$3,
|
|
494
|
+
// A grant with no list chosen is authenticated and useless. The list cannot
|
|
495
|
+
// be part of the consent flow — enumerating lists needs the token the consent
|
|
496
|
+
// returns — so it is always a second step, and the card must say so rather
|
|
497
|
+
// than showing Active over nothing.
|
|
498
|
+
incomplete : ( data ) => ( data?.settings?.list
|
|
499
|
+
? null
|
|
500
|
+
: 'Choose which Klaviyo list your contacts should sync into.'
|
|
501
|
+
),
|
|
272
502
|
label : 'klaviyo',
|
|
273
503
|
requires : [
|
|
274
504
|
'KLAVIYO_OAUTH_CLIENT_ID',
|
|
@@ -304,6 +534,16 @@ var klaviyo = {
|
|
|
304
534
|
'lifecycle.register' : false,
|
|
305
535
|
'lifecycle.rehydrate' : false
|
|
306
536
|
},
|
|
537
|
+
tasks : () => [
|
|
538
|
+
// Mailchimp carries the same warning, deliberately worded the same way. A
|
|
539
|
+
// merchant who connects either one and is told nothing reasonably assumes
|
|
540
|
+
// contacts are flowing, and finds out weeks later that they are not.
|
|
541
|
+
{
|
|
542
|
+
message : 'Contact syncing to Klaviyo lists has not shipped yet. Connecting stores your authorization so it is ready, but nothing is being sent to Klaviyo right now.',
|
|
543
|
+
title : 'List sync not available yet',
|
|
544
|
+
type : 'warning'
|
|
545
|
+
}
|
|
546
|
+
],
|
|
307
547
|
title : 'Klaviyo'
|
|
308
548
|
};
|
|
309
549
|
|
|
@@ -365,6 +605,15 @@ var mailchimp = {
|
|
|
365
605
|
}
|
|
366
606
|
],
|
|
367
607
|
icon: icon$2,
|
|
608
|
+
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
609
|
+
// needs its merge fields created on that audience before any Drawbridge total
|
|
610
|
+
// can be written to a member — unlike Klaviyo, its custom fields are not
|
|
611
|
+
// schemaless — so the audience must be picked before lifecycle.register has
|
|
612
|
+
// anything to register against.
|
|
613
|
+
incomplete : ( data ) => ( data?.settings?.audience
|
|
614
|
+
? null
|
|
615
|
+
: 'Choose which Mailchimp audience your contacts should sync into.'
|
|
616
|
+
),
|
|
368
617
|
label : 'mailchimp',
|
|
369
618
|
// Uniform surface, honest answers. A key is stored and can be removed; nothing
|
|
370
619
|
// else is built yet, because audience sync has not shipped. Every false here
|
|
@@ -501,7 +750,7 @@ const inbound = {
|
|
|
501
750
|
// Shopify's own GDPR deadline, not ours — these three are the only topics
|
|
502
751
|
// registered under `compliance_topics` in the app toml, and Shopify expects an
|
|
503
752
|
// answer even for a shop that has already uninstalled. Anything else on this
|
|
504
|
-
//
|
|
753
|
+
// channel is refused here rather than buffered, the same way a bad signature is:
|
|
505
754
|
// it is a request that should never have arrived.
|
|
506
755
|
const COMPLIANCE_TOPICS = new Set([
|
|
507
756
|
'customers/data_request',
|
|
@@ -554,17 +803,22 @@ var shopify = {
|
|
|
554
803
|
}
|
|
555
804
|
],
|
|
556
805
|
group : 'ecommerce',
|
|
806
|
+
// The install is the whole configuration — Shopify hands back the shop and
|
|
807
|
+
// there is nothing further to choose. `shop` absent means the install did not
|
|
808
|
+
// finish, which is a credential problem rather than a setup one, so the
|
|
809
|
+
// stored status already says so.
|
|
810
|
+
incomplete : () => null,
|
|
557
811
|
// verify and event lean entirely on the shared HMAC helper — Shopify's scheme
|
|
558
812
|
// is exactly the shape it covers, so there is nothing vendor-specific to
|
|
559
813
|
// write for either. receive is the one hook that genuinely differs by
|
|
560
|
-
//
|
|
814
|
+
// channel: /events buffers whatever arrives with the shop domain stamped on;
|
|
561
815
|
// /compliance enforces the topic allowlist above, because answering one late
|
|
562
816
|
// is a legal deadline rather than a retry.
|
|
563
817
|
hooks : {
|
|
564
818
|
'inbound.event' : ( args ) => readEventHeader({ ...args, descriptor : inbound }),
|
|
565
|
-
'inbound.receive' : ({
|
|
819
|
+
'inbound.receive' : ({ channel, event, headers, payload }) => {
|
|
566
820
|
|
|
567
|
-
if(
|
|
821
|
+
if( channel === 'compliance' && ! COMPLIANCE_TOPICS.has( event ) ){
|
|
568
822
|
|
|
569
823
|
throw Object.assign( new Error( 'Unrecognized compliance topic: ' + event ), { status : 401 });
|
|
570
824
|
|
|
@@ -575,7 +829,7 @@ var shopify = {
|
|
|
575
829
|
// own GDPR shape. The app-level event stream does not; that domain
|
|
576
830
|
// lives only in the header, so it is stamped on here rather than left
|
|
577
831
|
// for drawbridge-sync to reach into headers nobody hands it.
|
|
578
|
-
data :
|
|
832
|
+
data : channel === 'compliance'
|
|
579
833
|
? payload
|
|
580
834
|
: { ...payload, shop_domain : headers[ inbound.headers.shop ] || null },
|
|
581
835
|
provider : { id : headers[ inbound.headers.id ] || null }
|
|
@@ -767,6 +1021,9 @@ var webhook = {
|
|
|
767
1021
|
// It is the one card that reads wrong — our logo among vendor logos — and it
|
|
768
1022
|
// wants a mark of its own when there is one.
|
|
769
1023
|
icon,
|
|
1024
|
+
// The destination url is supplied per step, not per connection, so there is
|
|
1025
|
+
// nothing to finish here — generating the secret IS connecting.
|
|
1026
|
+
incomplete : () => null,
|
|
770
1027
|
label : 'webhook',
|
|
771
1028
|
// Gated on the encryption secret: without it the signing secret could not be
|
|
772
1029
|
// stored safely, so the connection must not be offered at all.
|
|
@@ -967,6 +1224,27 @@ const build = ( manifest ) => {
|
|
|
967
1224
|
|
|
968
1225
|
}
|
|
969
1226
|
|
|
1227
|
+
// THE REDIRECT MUST BE THE ONE PATH THE DASHBOARD SERVES.
|
|
1228
|
+
//
|
|
1229
|
+
// There is exactly one callback route — app/api/connection/[slug]/callback
|
|
1230
|
+
// in drawbridge-app-web — so this path is a fact about our own routing, not
|
|
1231
|
+
// a free choice. It stays declared rather than derived because it is also a
|
|
1232
|
+
// record of what is registered in the vendor's console, and reading it in
|
|
1233
|
+
// the manifest is how somebody checks the two agree.
|
|
1234
|
+
//
|
|
1235
|
+
// Declared-but-wrong is the dangerous state: it passes review, gets
|
|
1236
|
+
// registered at the vendor, and fails AFTER the merchant has consented —
|
|
1237
|
+
// they hit a dashboard 404 having already granted access. Cheaper to refuse
|
|
1238
|
+
// the manifest at import.
|
|
1239
|
+
if( manifest.auth.oauth.redirect !== '/api/connection/' + manifest.slug + '/callback' ){
|
|
1240
|
+
|
|
1241
|
+
throw new Error(
|
|
1242
|
+
manifest.slug + ' declares auth.oauth.redirect ' + manifest.auth.oauth.redirect
|
|
1243
|
+
+ ' but the only callback route is /api/connection/' + manifest.slug + '/callback'
|
|
1244
|
+
);
|
|
1245
|
+
|
|
1246
|
+
}
|
|
1247
|
+
|
|
970
1248
|
}
|
|
971
1249
|
|
|
972
1250
|
// A vendor that receives from the outside must say where it puts the event
|
|
@@ -984,6 +1262,27 @@ const build = ( manifest ) => {
|
|
|
984
1262
|
|
|
985
1263
|
}
|
|
986
1264
|
|
|
1265
|
+
// IS THE CREDENTIAL ENOUGH? For most vendors yes, and they answer with a bare
|
|
1266
|
+
// `() => null`. But a credential is not always a working connection: a
|
|
1267
|
+
// Klaviyo grant with no list chosen is authenticated and useless, and the
|
|
1268
|
+
// merchant sees an Active card doing nothing — the exact silence this
|
|
1269
|
+
// package exists to remove.
|
|
1270
|
+
//
|
|
1271
|
+
// Returns the REASON it is not usable yet, in the merchant's words, or null
|
|
1272
|
+
// when it is. One function rather than a boolean plus a message, because two
|
|
1273
|
+
// declarations of the same fact drift: the status the card shows and the
|
|
1274
|
+
// sentence explaining it come from the same call or they eventually
|
|
1275
|
+
// disagree.
|
|
1276
|
+
//
|
|
1277
|
+
// Required on every manifest for the same reason `supports` is: a missing
|
|
1278
|
+
// answer is indistinguishable from a vendor written before the question
|
|
1279
|
+
// existed.
|
|
1280
|
+
if( typeof manifest?.incomplete !== 'function' ){
|
|
1281
|
+
|
|
1282
|
+
throw new Error( manifest.slug + ' must declare incomplete( data ) — return null when the connection is usable, or the reason it is not' );
|
|
1283
|
+
|
|
1284
|
+
}
|
|
1285
|
+
|
|
987
1286
|
// How to get the credential, in the merchant's words. A connection page that
|
|
988
1287
|
// cannot say where to find an API key sends somebody to a vendor's docs
|
|
989
1288
|
// written for a different integration.
|
|
@@ -1310,6 +1609,9 @@ const publicConnectionKeys = Object.freeze([
|
|
|
1310
1609
|
'group',
|
|
1311
1610
|
'id',
|
|
1312
1611
|
'image',
|
|
1612
|
+
// The reason a connected vendor still is not usable — a Klaviyo grant with no
|
|
1613
|
+
// list chosen. Public because the card that shows Pending has to say why.
|
|
1614
|
+
'incomplete',
|
|
1313
1615
|
'label',
|
|
1314
1616
|
'setup',
|
|
1315
1617
|
'settings',
|
|
@@ -1366,4 +1668,4 @@ const resolveConnection = ( item, data ) => {
|
|
|
1366
1668
|
|
|
1367
1669
|
};
|
|
1368
1670
|
|
|
1369
|
-
export { AUTH_TYPES, CATEGORIES, HOOKS, HOOK_NAMES, INPUTS, OAUTH_FIELDS, OUTCOMES, availableConnections, build, connectFields, connectionSteps, connections, hookSupport, mergeSettings, projectConnection, publicConnectionKeys, publicSettingsBySlug, redactSettings, resolveConnection, runHook, scopesMessage, stepQueues };
|
|
1671
|
+
export { AUTH_TYPES, CATEGORIES, HOOKS, HOOK_NAMES, INPUTS, OAUTH_FIELDS, OUTCOMES, accessToken, availableConnections, build, connectFields, connectionSteps, connections, hookSupport, isStale, mergeSettings, projectConnection, publicConnectionKeys, publicSettingsBySlug, redactSettings, refresh, resolveConnection, runHook, scopesMessage, stepQueues, tokenSettings };
|