@drawbridge/drawbridge-utils 0.0.107 → 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 +306 -41
- package/dist/connections/index.d.cts +485 -42
- package/dist/connections/index.d.ts +485 -42
- package/dist/connections/index.js +302 -40
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ __export(connections_exports, {
|
|
|
26
26
|
INPUTS: () => INPUTS,
|
|
27
27
|
OAUTH_FIELDS: () => OAUTH_FIELDS,
|
|
28
28
|
OUTCOMES: () => OUTCOMES,
|
|
29
|
+
accessToken: () => accessToken,
|
|
29
30
|
availableConnections: () => availableConnections,
|
|
30
31
|
build: () => build,
|
|
31
32
|
connectFields: () => connectFields,
|
|
@@ -34,6 +35,7 @@ __export(connections_exports, {
|
|
|
34
35
|
consentUrl: () => consentUrl,
|
|
35
36
|
exchange: () => exchange,
|
|
36
37
|
hookSupport: () => hookSupport,
|
|
38
|
+
isStale: () => isStale,
|
|
37
39
|
mergeSettings: () => mergeSettings,
|
|
38
40
|
pkcePair: () => pkcePair,
|
|
39
41
|
projectConnection: () => projectConnection,
|
|
@@ -44,7 +46,8 @@ __export(connections_exports, {
|
|
|
44
46
|
resolveConnection: () => resolveConnection,
|
|
45
47
|
runHook: () => runHook,
|
|
46
48
|
scopesMessage: () => scopesMessage,
|
|
47
|
-
stepQueues: () => stepQueues
|
|
49
|
+
stepQueues: () => stepQueues,
|
|
50
|
+
tokenSettings: () => tokenSettings
|
|
48
51
|
});
|
|
49
52
|
module.exports = __toCommonJS(connections_exports);
|
|
50
53
|
|
|
@@ -76,14 +79,49 @@ var HOOKS = Object.freeze({
|
|
|
76
79
|
"cleanup"
|
|
77
80
|
]),
|
|
78
81
|
// Receiving from the vendor.
|
|
82
|
+
//
|
|
83
|
+
// Split across TWO SERVICES, and the split is the point. `verify` and `event`
|
|
84
|
+
// and `receive` run in drawbridge-webhooks against the vendor's open
|
|
85
|
+
// connection, where the budget is whatever that vendor's timeout is —
|
|
86
|
+
// Shopify's is about five seconds, and missing it means they retry and the
|
|
87
|
+
// delivery is buffered twice. `process` runs in drawbridge-sync off the
|
|
88
|
+
// buffer, where it can take as long as it needs and retry without the vendor
|
|
89
|
+
// ever knowing.
|
|
90
|
+
//
|
|
91
|
+
// One hook spanning that seam would hide it, and the thing it hides is the
|
|
92
|
+
// one most likely to bite: slow work written on the receiving side turns
|
|
93
|
+
// into duplicate deliveries.
|
|
79
94
|
inbound: Object.freeze([
|
|
80
|
-
// Prove the request came from the vendor
|
|
81
|
-
//
|
|
95
|
+
// Prove the request came from the vendor, AND return the payload it
|
|
96
|
+
// carries. One hook rather than two because for some vendors they are
|
|
97
|
+
// genuinely one operation: a JWT-bodied webhook (Kinde) cannot be decoded
|
|
98
|
+
// into anything trustworthy without verifying it first, and a decode that
|
|
99
|
+
// runs before the signature check is exactly the bug this shape prevents.
|
|
100
|
+
//
|
|
101
|
+
// So the raw bytes stop here. Everything downstream receives the payload
|
|
102
|
+
// this returned, which means nothing downstream can act on unverified
|
|
103
|
+
// data even by mistake.
|
|
104
|
+
//
|
|
105
|
+
// It is also where a request gets refused for any other reason — an event
|
|
106
|
+
// outside the allowlist, a connection in the wrong state. Anything that
|
|
107
|
+
// can reject belongs here, so the route holds no rules of its own.
|
|
82
108
|
"verify",
|
|
83
|
-
// Name the event, from wherever this vendor puts it.
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
"
|
|
109
|
+
// Name the event, from wherever this vendor puts it. A header for
|
|
110
|
+
// Shopify, the route itself for Twilio, a claim in the payload for a
|
|
111
|
+
// JWT-bodied vendor.
|
|
112
|
+
"event",
|
|
113
|
+
// What to buffer and what to answer RIGHT NOW, while the vendor waits.
|
|
114
|
+
// Thin by obligation. Most vendors only shape a buffer doc here; one that
|
|
115
|
+
// must reply with content (Twilio answers HELP inline with TwiML, which
|
|
116
|
+
// carriers require) returns that too.
|
|
117
|
+
"receive",
|
|
118
|
+
// Do the work the event implies. Runs in drawbridge-sync, off the buffer.
|
|
119
|
+
//
|
|
120
|
+
// DECLARED here, IMPLEMENTED there — the same split as the step handlers,
|
|
121
|
+
// and for the same reason: this half needs controllers, queues and vendor
|
|
122
|
+
// SDKs, and putting those behind a published package makes every consumer
|
|
123
|
+
// carry them. The declaration is what proves the implementation exists.
|
|
124
|
+
"process"
|
|
87
125
|
]),
|
|
88
126
|
// Vendor data a campaign draws on. Named for what every store platform has,
|
|
89
127
|
// not for what Shopify calls it: Shopify says discounts, Stripe says coupons
|
|
@@ -215,6 +253,56 @@ var refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refr
|
|
|
215
253
|
};
|
|
216
254
|
};
|
|
217
255
|
|
|
256
|
+
// lib/connections/token.js
|
|
257
|
+
var SKEW_SECONDS = 120;
|
|
258
|
+
var isStale = (settings, now = Date.now()) => {
|
|
259
|
+
if (!(settings == null ? void 0 : settings.expiresAt)) return false;
|
|
260
|
+
return new Date(settings.expiresAt).getTime() - SKEW_SECONDS * 1e3 <= now;
|
|
261
|
+
};
|
|
262
|
+
var tokenSettings = ({ existing = {}, now = Date.now(), tokens }) => ({
|
|
263
|
+
accessToken: tokens.accessToken,
|
|
264
|
+
// A vendor that does not rotate its refresh token returns none on a refresh
|
|
265
|
+
// (Klaviyo). Keeping the existing one is what makes the NEXT refresh work —
|
|
266
|
+
// dropping it invalidates the grant one call later, nowhere near the cause.
|
|
267
|
+
...(tokens.refreshToken || existing.refreshToken) && {
|
|
268
|
+
refreshToken: tokens.refreshToken || existing.refreshToken
|
|
269
|
+
},
|
|
270
|
+
// Absent when the vendor issues non-expiring tokens, and absent is meaningful
|
|
271
|
+
// — isStale reads it as "nothing to refresh toward".
|
|
272
|
+
...tokens.expiresIn && {
|
|
273
|
+
expiresAt: new Date(now + tokens.expiresIn * 1e3).toISOString()
|
|
274
|
+
},
|
|
275
|
+
...tokens.scope && { scope: tokens.scope }
|
|
276
|
+
});
|
|
277
|
+
var accessToken = async ({
|
|
278
|
+
clientId,
|
|
279
|
+
clientSecret,
|
|
280
|
+
fetcher,
|
|
281
|
+
force = false,
|
|
282
|
+
manifest,
|
|
283
|
+
now = Date.now(),
|
|
284
|
+
save,
|
|
285
|
+
settings
|
|
286
|
+
} = {}) => {
|
|
287
|
+
if (!(settings == null ? void 0 : settings.accessToken) && !(settings == null ? void 0 : settings.refreshToken)) {
|
|
288
|
+
throw new Error("This connection holds no credential, so there is no token to use");
|
|
289
|
+
}
|
|
290
|
+
if (!force && !isStale(settings, now)) return settings.accessToken;
|
|
291
|
+
if (!settings.refreshToken) {
|
|
292
|
+
throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
|
|
293
|
+
}
|
|
294
|
+
const minted = await refresh({
|
|
295
|
+
clientId,
|
|
296
|
+
clientSecret,
|
|
297
|
+
descriptor: manifest.auth.oauth,
|
|
298
|
+
...fetcher && { fetcher },
|
|
299
|
+
refreshToken: settings.refreshToken
|
|
300
|
+
});
|
|
301
|
+
const next = tokenSettings({ existing: settings, now, tokens: minted });
|
|
302
|
+
if (save) await save(next);
|
|
303
|
+
return next.accessToken;
|
|
304
|
+
};
|
|
305
|
+
|
|
218
306
|
// lib/connections/icons/klaviyo.js
|
|
219
307
|
var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
220
308
|
<rect width="500" height="500" fill="white"/>
|
|
@@ -222,6 +310,26 @@ var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
222
310
|
</svg>`;
|
|
223
311
|
|
|
224
312
|
// lib/connections/klaviyo.js
|
|
313
|
+
var REVISION = "2026-07-15";
|
|
314
|
+
var api = async (path, { fetcher = fetch, token }) => {
|
|
315
|
+
const response = await fetcher("https://a.klaviyo.com/api" + path, {
|
|
316
|
+
headers: {
|
|
317
|
+
// Bearer, not Klaviyo-API-Key — that header is for private keys, and
|
|
318
|
+
// sending it with an OAuth token fails in a way that reads like a bad
|
|
319
|
+
// token rather than a bad scheme.
|
|
320
|
+
authorization: "Bearer " + token,
|
|
321
|
+
revision: REVISION
|
|
322
|
+
},
|
|
323
|
+
signal: AbortSignal.timeout(15e3)
|
|
324
|
+
});
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
throw Object.assign(
|
|
327
|
+
new Error("Klaviyo refused the request (" + response.status + ")"),
|
|
328
|
+
{ status: response.status }
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return response.json();
|
|
332
|
+
};
|
|
225
333
|
var klaviyo_default2 = {
|
|
226
334
|
// OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
|
|
227
335
|
// exchange without a code_verifier matching the challenge the consent
|
|
@@ -250,6 +358,11 @@ var klaviyo_default2 = {
|
|
|
250
358
|
// it is a fact about someone else's records rather than a string this
|
|
251
359
|
// code computes. Deriving one from a provider key produced
|
|
252
360
|
// redirect_uri_mismatch on a connection nobody had touched.
|
|
361
|
+
// Klaviyo drops a refresh token after 90 days of NON-USE. The vendor
|
|
362
|
+
// never mentions this at runtime — you discover it when a refresh fails
|
|
363
|
+
// on a connection nobody touched — so it is declared, and it is why
|
|
364
|
+
// auth.probe has to run on a schedule rather than only before a call.
|
|
365
|
+
idleExpiry: 90 * 24 * 60 * 60,
|
|
253
366
|
redirect: "/api/connection/klaviyo/callback",
|
|
254
367
|
// Space separated. accounts:read is required by Klaviyo on every app
|
|
255
368
|
// and must stay in the list; the rest are what a contact sync needs.
|
|
@@ -281,7 +394,78 @@ var klaviyo_default2 = {
|
|
|
281
394
|
label: "Klaviyo account"
|
|
282
395
|
}
|
|
283
396
|
],
|
|
397
|
+
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
398
|
+
// live here rather than in sync. A vendor's own protocol belongs beside the
|
|
399
|
+
// vendor.
|
|
400
|
+
hooks: {
|
|
401
|
+
// Turn a fresh grant into settings worth showing. Without this the card
|
|
402
|
+
// renders an empty "Klaviyo account" field, because the merchant is never
|
|
403
|
+
// asked which account they connected — the consent already decided it and
|
|
404
|
+
// asking again would be a question we can answer ourselves.
|
|
405
|
+
"auth.connect": async ({ fetcher, tokens }) => {
|
|
406
|
+
var _a, _b, _c;
|
|
407
|
+
const body = await api("/accounts", { fetcher, token: tokens.accessToken });
|
|
408
|
+
const account = (_a = body == null ? void 0 : body.data) == null ? void 0 : _a[0];
|
|
409
|
+
return {
|
|
410
|
+
account: ((_c = (_b = account == null ? void 0 : account.attributes) == null ? void 0 : _b.contact_information) == null ? void 0 : _c.organization_name) || (account == null ? void 0 : account.id) || null,
|
|
411
|
+
accountId: (account == null ? void 0 : account.id) || null
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
// Revoke at KLAVIYO, not just locally. Forgetting our copy leaves the
|
|
415
|
+
// grant live in the merchant's account, so a disconnect that looks
|
|
416
|
+
// complete here still shows Drawbridge with access over there.
|
|
417
|
+
//
|
|
418
|
+
// Basic auth with our client, exactly like the token exchange — the token
|
|
419
|
+
// being revoked is the subject, not the credential.
|
|
420
|
+
"auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
421
|
+
const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
|
|
422
|
+
if (!token) return { revoked: false };
|
|
423
|
+
const response = await fetcher("https://a.klaviyo.com/oauth/revoke", {
|
|
424
|
+
body: new URLSearchParams({
|
|
425
|
+
token,
|
|
426
|
+
token_type_hint: (settings == null ? void 0 : settings.refreshToken) ? "refresh_token" : "access_token"
|
|
427
|
+
}),
|
|
428
|
+
headers: {
|
|
429
|
+
authorization: "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"),
|
|
430
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
431
|
+
},
|
|
432
|
+
method: "POST",
|
|
433
|
+
signal: AbortSignal.timeout(15e3)
|
|
434
|
+
});
|
|
435
|
+
return { revoked: response.ok };
|
|
436
|
+
},
|
|
437
|
+
// THE MINT IS THE PROBE. Asking "is this token still good" by inspecting
|
|
438
|
+
// what we stored answers the wrong question — a grant revoked inside
|
|
439
|
+
// Klaviyo still looks perfect in our database. Spending the refresh token
|
|
440
|
+
// is the only thing that asks Klaviyo.
|
|
441
|
+
//
|
|
442
|
+
// It also keeps the grant warm: Klaviyo expires a refresh token after 90
|
|
443
|
+
// days of NON-USE, so a connection nobody touches dies silently without
|
|
444
|
+
// this running.
|
|
445
|
+
"auth.probe": async ({ clientId, clientSecret, fetcher, manifest, settings }) => {
|
|
446
|
+
const token = await accessToken({
|
|
447
|
+
clientId,
|
|
448
|
+
clientSecret,
|
|
449
|
+
fetcher,
|
|
450
|
+
// Mint even if the stored token still looks good — a probe that
|
|
451
|
+
// short-circuits never reaches Klaviyo and reports healthy on a
|
|
452
|
+
// grant revoked an hour ago.
|
|
453
|
+
force: true,
|
|
454
|
+
manifest,
|
|
455
|
+
settings
|
|
456
|
+
});
|
|
457
|
+
return { ok: Boolean(token) };
|
|
458
|
+
}
|
|
459
|
+
},
|
|
284
460
|
icon: klaviyo_default,
|
|
461
|
+
// A grant with no list chosen is authenticated and useless. The list cannot
|
|
462
|
+
// be part of the consent flow — enumerating lists needs the token the consent
|
|
463
|
+
// returns — so it is always a second step, and the card must say so rather
|
|
464
|
+
// than showing Active over nothing.
|
|
465
|
+
incomplete: (data) => {
|
|
466
|
+
var _a;
|
|
467
|
+
return ((_a = data == null ? void 0 : data.settings) == null ? void 0 : _a.list) ? null : "Choose which Klaviyo list your contacts should sync into.";
|
|
468
|
+
},
|
|
285
469
|
label: "klaviyo",
|
|
286
470
|
requires: [
|
|
287
471
|
"KLAVIYO_OAUTH_CLIENT_ID",
|
|
@@ -309,13 +493,24 @@ var klaviyo_default2 = {
|
|
|
309
493
|
"catalog.prices": false,
|
|
310
494
|
"catalog.products": false,
|
|
311
495
|
"catalog.promotions": false,
|
|
312
|
-
"inbound.
|
|
313
|
-
"inbound.
|
|
496
|
+
"inbound.event": false,
|
|
497
|
+
"inbound.process": false,
|
|
498
|
+
"inbound.receive": false,
|
|
314
499
|
"inbound.verify": false,
|
|
315
500
|
"lifecycle.cleanup": false,
|
|
316
501
|
"lifecycle.register": false,
|
|
317
502
|
"lifecycle.rehydrate": false
|
|
318
503
|
},
|
|
504
|
+
tasks: () => [
|
|
505
|
+
// Mailchimp carries the same warning, deliberately worded the same way. A
|
|
506
|
+
// merchant who connects either one and is told nothing reasonably assumes
|
|
507
|
+
// contacts are flowing, and finds out weeks later that they are not.
|
|
508
|
+
{
|
|
509
|
+
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.",
|
|
510
|
+
title: "List sync not available yet",
|
|
511
|
+
type: "warning"
|
|
512
|
+
}
|
|
513
|
+
],
|
|
319
514
|
title: "Klaviyo"
|
|
320
515
|
};
|
|
321
516
|
|
|
@@ -367,6 +562,15 @@ var mailchimp_default2 = {
|
|
|
367
562
|
}
|
|
368
563
|
],
|
|
369
564
|
icon: mailchimp_default,
|
|
565
|
+
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
566
|
+
// needs its merge fields created on that audience before any Drawbridge total
|
|
567
|
+
// can be written to a member — unlike Klaviyo, its custom fields are not
|
|
568
|
+
// schemaless — so the audience must be picked before lifecycle.register has
|
|
569
|
+
// anything to register against.
|
|
570
|
+
incomplete: (data) => {
|
|
571
|
+
var _a;
|
|
572
|
+
return ((_a = data == null ? void 0 : data.settings) == null ? void 0 : _a.audience) ? null : "Choose which Mailchimp audience your contacts should sync into.";
|
|
573
|
+
},
|
|
370
574
|
label: "mailchimp",
|
|
371
575
|
// Uniform surface, honest answers. A key is stored and can be removed; nothing
|
|
372
576
|
// else is built yet, because audience sync has not shipped. Every false here
|
|
@@ -380,8 +584,9 @@ var mailchimp_default2 = {
|
|
|
380
584
|
"catalog.prices": false,
|
|
381
585
|
"catalog.products": false,
|
|
382
586
|
"catalog.promotions": false,
|
|
383
|
-
"inbound.
|
|
384
|
-
"inbound.
|
|
587
|
+
"inbound.event": false,
|
|
588
|
+
"inbound.process": false,
|
|
589
|
+
"inbound.receive": false,
|
|
385
590
|
"inbound.verify": false,
|
|
386
591
|
"lifecycle.cleanup": false,
|
|
387
592
|
"lifecycle.register": false,
|
|
@@ -415,7 +620,42 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
415
620
|
<path d="M258.993 193.019L249.103 230.052C249.103 230.052 238.079 225.023 225 225.85C205.833 227.059 205.628 239.171 205.824 242.21C206.865 258.756 250.376 262.381 252.821 301.171C254.745 331.688 236.656 352.574 210.592 354.21C179.313 356.19 162.089 337.711 162.089 337.711L168.716 309.472C168.716 309.472 186.052 322.569 199.921 321.686C208.993 321.119 212.228 313.738 211.903 308.514C210.536 286.921 175.102 288.185 172.862 252.695C170.985 222.811 190.57 192.554 233.803 189.821C250.46 188.762 258.993 193.028 258.993 193.028" fill="white"/>
|
|
416
621
|
</svg>`;
|
|
417
622
|
|
|
623
|
+
// lib/connections/inbound.js
|
|
624
|
+
var import_node_crypto2 = require("crypto");
|
|
625
|
+
var verifySignature = ({ body, descriptor, headers }) => {
|
|
626
|
+
const provided = headers[descriptor.headers.signature];
|
|
627
|
+
if (!provided) {
|
|
628
|
+
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
629
|
+
}
|
|
630
|
+
const digest = (0, import_node_crypto2.createHmac)(descriptor.signature.algorithm, process.env[descriptor.signature.secret]).update(body).digest(descriptor.signature.encoding);
|
|
631
|
+
const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
|
|
632
|
+
const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
|
|
633
|
+
if (digestBuffer.length !== providedBuffer.length || !(0, import_node_crypto2.timingSafeEqual)(digestBuffer, providedBuffer)) {
|
|
634
|
+
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
635
|
+
}
|
|
636
|
+
return JSON.parse(body.toString());
|
|
637
|
+
};
|
|
638
|
+
var readEventHeader = ({ descriptor, headers }) => headers[descriptor.headers.event];
|
|
639
|
+
|
|
418
640
|
// lib/connections/shopify.js
|
|
641
|
+
var inbound = {
|
|
642
|
+
headers: {
|
|
643
|
+
event: "x-shopify-topic",
|
|
644
|
+
id: "x-shopify-webhook-id",
|
|
645
|
+
shop: "x-shopify-shop-domain",
|
|
646
|
+
signature: "x-shopify-hmac-sha256"
|
|
647
|
+
},
|
|
648
|
+
signature: {
|
|
649
|
+
algorithm: "sha256",
|
|
650
|
+
encoding: "base64",
|
|
651
|
+
secret: "SHOPIFY_API_SECRET"
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
var COMPLIANCE_TOPICS = /* @__PURE__ */ new Set([
|
|
655
|
+
"customers/data_request",
|
|
656
|
+
"customers/redact",
|
|
657
|
+
"shop/redact"
|
|
658
|
+
]);
|
|
419
659
|
var shopify_default2 = {
|
|
420
660
|
// INSTALL, not oauth. The grant is an OAuth grant, but the merchant never
|
|
421
661
|
// sees a consent screen we sent them to -- they start at the App Store, and
|
|
@@ -460,30 +700,36 @@ var shopify_default2 = {
|
|
|
460
700
|
}
|
|
461
701
|
],
|
|
462
702
|
group: "ecommerce",
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
703
|
+
// The install is the whole configuration — Shopify hands back the shop and
|
|
704
|
+
// there is nothing further to choose. `shop` absent means the install did not
|
|
705
|
+
// finish, which is a credential problem rather than a setup one, so the
|
|
706
|
+
// stored status already says so.
|
|
707
|
+
incomplete: () => null,
|
|
708
|
+
// verify and event lean entirely on the shared HMAC helper — Shopify's scheme
|
|
709
|
+
// is exactly the shape it covers, so there is nothing vendor-specific to
|
|
710
|
+
// write for either. receive is the one hook that genuinely differs by
|
|
711
|
+
// channel: /events buffers whatever arrives with the shop domain stamped on;
|
|
712
|
+
// /compliance enforces the topic allowlist above, because answering one late
|
|
713
|
+
// is a legal deadline rather than a retry.
|
|
714
|
+
hooks: {
|
|
715
|
+
"inbound.event": (args) => readEventHeader({ ...args, descriptor: inbound }),
|
|
716
|
+
"inbound.receive": ({ channel, event, headers, payload }) => {
|
|
717
|
+
if (channel === "compliance" && !COMPLIANCE_TOPICS.has(event)) {
|
|
718
|
+
throw Object.assign(new Error("Unrecognized compliance topic: " + event), { status: 401 });
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
// Compliance payloads already carry shop_domain in the body — Shopify's
|
|
722
|
+
// own GDPR shape. The app-level event stream does not; that domain
|
|
723
|
+
// lives only in the header, so it is stamped on here rather than left
|
|
724
|
+
// for drawbridge-sync to reach into headers nobody hands it.
|
|
725
|
+
data: channel === "compliance" ? payload : { ...payload, shop_domain: headers[inbound.headers.shop] || null },
|
|
726
|
+
provider: { id: headers[inbound.headers.id] || null }
|
|
727
|
+
};
|
|
479
728
|
},
|
|
480
|
-
|
|
481
|
-
algorithm: "sha256",
|
|
482
|
-
encoding: "base64",
|
|
483
|
-
secret: "SHOPIFY_API_SECRET"
|
|
484
|
-
}
|
|
729
|
+
"inbound.verify": (args) => verifySignature({ ...args, descriptor: inbound })
|
|
485
730
|
},
|
|
486
731
|
icon: shopify_default,
|
|
732
|
+
inbound,
|
|
487
733
|
label: "shopify",
|
|
488
734
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
489
735
|
// exists and the app is fully configured. Requiring all four means it can
|
|
@@ -512,8 +758,9 @@ var shopify_default2 = {
|
|
|
512
758
|
"catalog.prices": false,
|
|
513
759
|
"catalog.products": true,
|
|
514
760
|
"catalog.promotions": true,
|
|
515
|
-
"inbound.
|
|
516
|
-
"inbound.
|
|
761
|
+
"inbound.event": true,
|
|
762
|
+
"inbound.process": true,
|
|
763
|
+
"inbound.receive": true,
|
|
517
764
|
"inbound.verify": true,
|
|
518
765
|
"lifecycle.cleanup": true,
|
|
519
766
|
"lifecycle.register": true,
|
|
@@ -657,6 +904,9 @@ var webhook_default = {
|
|
|
657
904
|
// It is the one card that reads wrong — our logo among vendor logos — and it
|
|
658
905
|
// wants a mark of its own when there is one.
|
|
659
906
|
icon: drawbridge_default,
|
|
907
|
+
// The destination url is supplied per step, not per connection, so there is
|
|
908
|
+
// nothing to finish here — generating the secret IS connecting.
|
|
909
|
+
incomplete: () => null,
|
|
660
910
|
label: "webhook",
|
|
661
911
|
// Gated on the encryption secret: without it the signing secret could not be
|
|
662
912
|
// stored safely, so the connection must not be offered at all.
|
|
@@ -673,8 +923,9 @@ var webhook_default = {
|
|
|
673
923
|
"catalog.prices": false,
|
|
674
924
|
"catalog.products": false,
|
|
675
925
|
"catalog.promotions": false,
|
|
676
|
-
"inbound.
|
|
677
|
-
"inbound.
|
|
926
|
+
"inbound.event": false,
|
|
927
|
+
"inbound.process": false,
|
|
928
|
+
"inbound.receive": false,
|
|
678
929
|
"inbound.verify": false,
|
|
679
930
|
"lifecycle.cleanup": false,
|
|
680
931
|
"lifecycle.register": false,
|
|
@@ -763,13 +1014,21 @@ var build = (manifest) => {
|
|
|
763
1014
|
throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field);
|
|
764
1015
|
}
|
|
765
1016
|
}
|
|
1017
|
+
if (manifest.auth.oauth.redirect !== "/api/connection/" + manifest.slug + "/callback") {
|
|
1018
|
+
throw new Error(
|
|
1019
|
+
manifest.slug + " declares auth.oauth.redirect " + manifest.auth.oauth.redirect + " but the only callback route is /api/connection/" + manifest.slug + "/callback"
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
766
1022
|
}
|
|
767
|
-
if (((_d = manifest.supports) == null ? void 0 : _d["inbound.
|
|
768
|
-
throw new Error(manifest.slug + " supports inbound.
|
|
1023
|
+
if (((_d = manifest.supports) == null ? void 0 : _d["inbound.event"]) && !((_f = (_e = manifest.inbound) == null ? void 0 : _e.headers) == null ? void 0 : _f.event)) {
|
|
1024
|
+
throw new Error(manifest.slug + " supports inbound.event but declares no inbound.headers.event");
|
|
769
1025
|
}
|
|
770
1026
|
if (((_g = manifest.supports) == null ? void 0 : _g["inbound.verify"]) && !((_i = (_h = manifest.inbound) == null ? void 0 : _h.headers) == null ? void 0 : _i.signature)) {
|
|
771
1027
|
throw new Error(manifest.slug + " supports inbound.verify but declares no inbound.headers.signature");
|
|
772
1028
|
}
|
|
1029
|
+
if (typeof (manifest == null ? void 0 : manifest.incomplete) !== "function") {
|
|
1030
|
+
throw new Error(manifest.slug + " must declare incomplete( data ) \u2014 return null when the connection is usable, or the reason it is not");
|
|
1031
|
+
}
|
|
773
1032
|
if (!Array.isArray(manifest == null ? void 0 : manifest.setup) || !manifest.setup.length) {
|
|
774
1033
|
throw new Error(manifest.slug + " needs a setup guide \u2014 an array of steps for its page");
|
|
775
1034
|
}
|
|
@@ -880,7 +1139,7 @@ var runHook = async (slug, name, args = {}) => {
|
|
|
880
1139
|
try {
|
|
881
1140
|
return { outcome: OUTCOMES.answered, result: await hook(args) };
|
|
882
1141
|
} catch (error) {
|
|
883
|
-
return { error: (error == null ? void 0 : error.message) || "failed", outcome: OUTCOMES.failed };
|
|
1142
|
+
return { error: (error == null ? void 0 : error.message) || "failed", outcome: OUTCOMES.failed, status: (error == null ? void 0 : error.status) || null };
|
|
884
1143
|
}
|
|
885
1144
|
};
|
|
886
1145
|
var stepQueues = (env = {}) => Object.fromEntries(
|
|
@@ -919,6 +1178,9 @@ var publicConnectionKeys = Object.freeze([
|
|
|
919
1178
|
"group",
|
|
920
1179
|
"id",
|
|
921
1180
|
"image",
|
|
1181
|
+
// The reason a connected vendor still is not usable — a Klaviyo grant with no
|
|
1182
|
+
// list chosen. Public because the card that shows Pending has to say why.
|
|
1183
|
+
"incomplete",
|
|
922
1184
|
"label",
|
|
923
1185
|
"setup",
|
|
924
1186
|
"settings",
|
|
@@ -946,7 +1208,7 @@ var projectConnection = (record) => {
|
|
|
946
1208
|
var resolveConnection = (item, data) => {
|
|
947
1209
|
if (!item) return item;
|
|
948
1210
|
return Object.fromEntries(
|
|
949
|
-
Object.entries(item).filter(([key]) => !["auth", "enabled", "fields", "inbound", "requires", "steps", "supports"].includes(key)).map(([key, value]) => [
|
|
1211
|
+
Object.entries(item).filter(([key]) => !["auth", "enabled", "fields", "hooks", "inbound", "requires", "steps", "supports"].includes(key)).map(([key, value]) => [
|
|
950
1212
|
key,
|
|
951
1213
|
typeof value === "function" ? value(data) : value
|
|
952
1214
|
])
|
|
@@ -961,6 +1223,7 @@ var resolveConnection = (item, data) => {
|
|
|
961
1223
|
INPUTS,
|
|
962
1224
|
OAUTH_FIELDS,
|
|
963
1225
|
OUTCOMES,
|
|
1226
|
+
accessToken,
|
|
964
1227
|
availableConnections,
|
|
965
1228
|
build,
|
|
966
1229
|
connectFields,
|
|
@@ -969,6 +1232,7 @@ var resolveConnection = (item, data) => {
|
|
|
969
1232
|
consentUrl,
|
|
970
1233
|
exchange,
|
|
971
1234
|
hookSupport,
|
|
1235
|
+
isStale,
|
|
972
1236
|
mergeSettings,
|
|
973
1237
|
pkcePair,
|
|
974
1238
|
projectConnection,
|
|
@@ -979,5 +1243,6 @@ var resolveConnection = (item, data) => {
|
|
|
979
1243
|
resolveConnection,
|
|
980
1244
|
runHook,
|
|
981
1245
|
scopesMessage,
|
|
982
|
-
stepQueues
|
|
1246
|
+
stepQueues,
|
|
1247
|
+
tokenSettings
|
|
983
1248
|
});
|