@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,14 +26,49 @@ var HOOKS = Object.freeze({
|
|
|
26
26
|
"cleanup"
|
|
27
27
|
]),
|
|
28
28
|
// Receiving from the vendor.
|
|
29
|
+
//
|
|
30
|
+
// Split across TWO SERVICES, and the split is the point. `verify` and `event`
|
|
31
|
+
// and `receive` run in drawbridge-webhooks against the vendor's open
|
|
32
|
+
// connection, where the budget is whatever that vendor's timeout is —
|
|
33
|
+
// Shopify's is about five seconds, and missing it means they retry and the
|
|
34
|
+
// delivery is buffered twice. `process` runs in drawbridge-sync off the
|
|
35
|
+
// buffer, where it can take as long as it needs and retry without the vendor
|
|
36
|
+
// ever knowing.
|
|
37
|
+
//
|
|
38
|
+
// One hook spanning that seam would hide it, and the thing it hides is the
|
|
39
|
+
// one most likely to bite: slow work written on the receiving side turns
|
|
40
|
+
// into duplicate deliveries.
|
|
29
41
|
inbound: Object.freeze([
|
|
30
|
-
// Prove the request came from the vendor
|
|
31
|
-
//
|
|
42
|
+
// Prove the request came from the vendor, AND return the payload it
|
|
43
|
+
// carries. One hook rather than two because for some vendors they are
|
|
44
|
+
// genuinely one operation: a JWT-bodied webhook (Kinde) cannot be decoded
|
|
45
|
+
// into anything trustworthy without verifying it first, and a decode that
|
|
46
|
+
// runs before the signature check is exactly the bug this shape prevents.
|
|
47
|
+
//
|
|
48
|
+
// So the raw bytes stop here. Everything downstream receives the payload
|
|
49
|
+
// this returned, which means nothing downstream can act on unverified
|
|
50
|
+
// data even by mistake.
|
|
51
|
+
//
|
|
52
|
+
// It is also where a request gets refused for any other reason — an event
|
|
53
|
+
// outside the allowlist, a connection in the wrong state. Anything that
|
|
54
|
+
// can reject belongs here, so the route holds no rules of its own.
|
|
32
55
|
"verify",
|
|
33
|
-
// Name the event, from wherever this vendor puts it.
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
"
|
|
56
|
+
// Name the event, from wherever this vendor puts it. A header for
|
|
57
|
+
// Shopify, the route itself for Twilio, a claim in the payload for a
|
|
58
|
+
// JWT-bodied vendor.
|
|
59
|
+
"event",
|
|
60
|
+
// What to buffer and what to answer RIGHT NOW, while the vendor waits.
|
|
61
|
+
// Thin by obligation. Most vendors only shape a buffer doc here; one that
|
|
62
|
+
// must reply with content (Twilio answers HELP inline with TwiML, which
|
|
63
|
+
// carriers require) returns that too.
|
|
64
|
+
"receive",
|
|
65
|
+
// Do the work the event implies. Runs in drawbridge-sync, off the buffer.
|
|
66
|
+
//
|
|
67
|
+
// DECLARED here, IMPLEMENTED there — the same split as the step handlers,
|
|
68
|
+
// and for the same reason: this half needs controllers, queues and vendor
|
|
69
|
+
// SDKs, and putting those behind a published package makes every consumer
|
|
70
|
+
// carry them. The declaration is what proves the implementation exists.
|
|
71
|
+
"process"
|
|
37
72
|
]),
|
|
38
73
|
// Vendor data a campaign draws on. Named for what every store platform has,
|
|
39
74
|
// not for what Shopify calls it: Shopify says discounts, Stripe says coupons
|
|
@@ -165,6 +200,56 @@ var refresh = async ({ clientId, clientSecret, descriptor, fetcher = fetch, refr
|
|
|
165
200
|
};
|
|
166
201
|
};
|
|
167
202
|
|
|
203
|
+
// lib/connections/token.js
|
|
204
|
+
var SKEW_SECONDS = 120;
|
|
205
|
+
var isStale = (settings, now = Date.now()) => {
|
|
206
|
+
if (!(settings == null ? void 0 : settings.expiresAt)) return false;
|
|
207
|
+
return new Date(settings.expiresAt).getTime() - SKEW_SECONDS * 1e3 <= now;
|
|
208
|
+
};
|
|
209
|
+
var tokenSettings = ({ existing = {}, now = Date.now(), tokens }) => ({
|
|
210
|
+
accessToken: tokens.accessToken,
|
|
211
|
+
// A vendor that does not rotate its refresh token returns none on a refresh
|
|
212
|
+
// (Klaviyo). Keeping the existing one is what makes the NEXT refresh work —
|
|
213
|
+
// dropping it invalidates the grant one call later, nowhere near the cause.
|
|
214
|
+
...(tokens.refreshToken || existing.refreshToken) && {
|
|
215
|
+
refreshToken: tokens.refreshToken || existing.refreshToken
|
|
216
|
+
},
|
|
217
|
+
// Absent when the vendor issues non-expiring tokens, and absent is meaningful
|
|
218
|
+
// — isStale reads it as "nothing to refresh toward".
|
|
219
|
+
...tokens.expiresIn && {
|
|
220
|
+
expiresAt: new Date(now + tokens.expiresIn * 1e3).toISOString()
|
|
221
|
+
},
|
|
222
|
+
...tokens.scope && { scope: tokens.scope }
|
|
223
|
+
});
|
|
224
|
+
var accessToken = async ({
|
|
225
|
+
clientId,
|
|
226
|
+
clientSecret,
|
|
227
|
+
fetcher,
|
|
228
|
+
force = false,
|
|
229
|
+
manifest,
|
|
230
|
+
now = Date.now(),
|
|
231
|
+
save,
|
|
232
|
+
settings
|
|
233
|
+
} = {}) => {
|
|
234
|
+
if (!(settings == null ? void 0 : settings.accessToken) && !(settings == null ? void 0 : settings.refreshToken)) {
|
|
235
|
+
throw new Error("This connection holds no credential, so there is no token to use");
|
|
236
|
+
}
|
|
237
|
+
if (!force && !isStale(settings, now)) return settings.accessToken;
|
|
238
|
+
if (!settings.refreshToken) {
|
|
239
|
+
throw new Error("This connection has expired and cannot be renewed automatically. Reconnect it.");
|
|
240
|
+
}
|
|
241
|
+
const minted = await refresh({
|
|
242
|
+
clientId,
|
|
243
|
+
clientSecret,
|
|
244
|
+
descriptor: manifest.auth.oauth,
|
|
245
|
+
...fetcher && { fetcher },
|
|
246
|
+
refreshToken: settings.refreshToken
|
|
247
|
+
});
|
|
248
|
+
const next = tokenSettings({ existing: settings, now, tokens: minted });
|
|
249
|
+
if (save) await save(next);
|
|
250
|
+
return next.accessToken;
|
|
251
|
+
};
|
|
252
|
+
|
|
168
253
|
// lib/connections/icons/klaviyo.js
|
|
169
254
|
var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
170
255
|
<rect width="500" height="500" fill="white"/>
|
|
@@ -172,6 +257,26 @@ var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
172
257
|
</svg>`;
|
|
173
258
|
|
|
174
259
|
// lib/connections/klaviyo.js
|
|
260
|
+
var REVISION = "2026-07-15";
|
|
261
|
+
var api = async (path, { fetcher = fetch, token }) => {
|
|
262
|
+
const response = await fetcher("https://a.klaviyo.com/api" + path, {
|
|
263
|
+
headers: {
|
|
264
|
+
// Bearer, not Klaviyo-API-Key — that header is for private keys, and
|
|
265
|
+
// sending it with an OAuth token fails in a way that reads like a bad
|
|
266
|
+
// token rather than a bad scheme.
|
|
267
|
+
authorization: "Bearer " + token,
|
|
268
|
+
revision: REVISION
|
|
269
|
+
},
|
|
270
|
+
signal: AbortSignal.timeout(15e3)
|
|
271
|
+
});
|
|
272
|
+
if (!response.ok) {
|
|
273
|
+
throw Object.assign(
|
|
274
|
+
new Error("Klaviyo refused the request (" + response.status + ")"),
|
|
275
|
+
{ status: response.status }
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return response.json();
|
|
279
|
+
};
|
|
175
280
|
var klaviyo_default2 = {
|
|
176
281
|
// OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
|
|
177
282
|
// exchange without a code_verifier matching the challenge the consent
|
|
@@ -200,6 +305,11 @@ var klaviyo_default2 = {
|
|
|
200
305
|
// it is a fact about someone else's records rather than a string this
|
|
201
306
|
// code computes. Deriving one from a provider key produced
|
|
202
307
|
// redirect_uri_mismatch on a connection nobody had touched.
|
|
308
|
+
// Klaviyo drops a refresh token after 90 days of NON-USE. The vendor
|
|
309
|
+
// never mentions this at runtime — you discover it when a refresh fails
|
|
310
|
+
// on a connection nobody touched — so it is declared, and it is why
|
|
311
|
+
// auth.probe has to run on a schedule rather than only before a call.
|
|
312
|
+
idleExpiry: 90 * 24 * 60 * 60,
|
|
203
313
|
redirect: "/api/connection/klaviyo/callback",
|
|
204
314
|
// Space separated. accounts:read is required by Klaviyo on every app
|
|
205
315
|
// and must stay in the list; the rest are what a contact sync needs.
|
|
@@ -231,7 +341,78 @@ var klaviyo_default2 = {
|
|
|
231
341
|
label: "Klaviyo account"
|
|
232
342
|
}
|
|
233
343
|
],
|
|
344
|
+
// The three auth hooks, all pure HTTP against Klaviyo — which is why they
|
|
345
|
+
// live here rather than in sync. A vendor's own protocol belongs beside the
|
|
346
|
+
// vendor.
|
|
347
|
+
hooks: {
|
|
348
|
+
// Turn a fresh grant into settings worth showing. Without this the card
|
|
349
|
+
// renders an empty "Klaviyo account" field, because the merchant is never
|
|
350
|
+
// asked which account they connected — the consent already decided it and
|
|
351
|
+
// asking again would be a question we can answer ourselves.
|
|
352
|
+
"auth.connect": async ({ fetcher, tokens }) => {
|
|
353
|
+
var _a, _b, _c;
|
|
354
|
+
const body = await api("/accounts", { fetcher, token: tokens.accessToken });
|
|
355
|
+
const account = (_a = body == null ? void 0 : body.data) == null ? void 0 : _a[0];
|
|
356
|
+
return {
|
|
357
|
+
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,
|
|
358
|
+
accountId: (account == null ? void 0 : account.id) || null
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
// Revoke at KLAVIYO, not just locally. Forgetting our copy leaves the
|
|
362
|
+
// grant live in the merchant's account, so a disconnect that looks
|
|
363
|
+
// complete here still shows Drawbridge with access over there.
|
|
364
|
+
//
|
|
365
|
+
// Basic auth with our client, exactly like the token exchange — the token
|
|
366
|
+
// being revoked is the subject, not the credential.
|
|
367
|
+
"auth.disconnect": async ({ clientId, clientSecret, fetcher = fetch, settings }) => {
|
|
368
|
+
const token = (settings == null ? void 0 : settings.refreshToken) || (settings == null ? void 0 : settings.accessToken);
|
|
369
|
+
if (!token) return { revoked: false };
|
|
370
|
+
const response = await fetcher("https://a.klaviyo.com/oauth/revoke", {
|
|
371
|
+
body: new URLSearchParams({
|
|
372
|
+
token,
|
|
373
|
+
token_type_hint: (settings == null ? void 0 : settings.refreshToken) ? "refresh_token" : "access_token"
|
|
374
|
+
}),
|
|
375
|
+
headers: {
|
|
376
|
+
authorization: "Basic " + Buffer.from(clientId + ":" + clientSecret).toString("base64"),
|
|
377
|
+
"content-type": "application/x-www-form-urlencoded"
|
|
378
|
+
},
|
|
379
|
+
method: "POST",
|
|
380
|
+
signal: AbortSignal.timeout(15e3)
|
|
381
|
+
});
|
|
382
|
+
return { revoked: response.ok };
|
|
383
|
+
},
|
|
384
|
+
// THE MINT IS THE PROBE. Asking "is this token still good" by inspecting
|
|
385
|
+
// what we stored answers the wrong question — a grant revoked inside
|
|
386
|
+
// Klaviyo still looks perfect in our database. Spending the refresh token
|
|
387
|
+
// is the only thing that asks Klaviyo.
|
|
388
|
+
//
|
|
389
|
+
// It also keeps the grant warm: Klaviyo expires a refresh token after 90
|
|
390
|
+
// days of NON-USE, so a connection nobody touches dies silently without
|
|
391
|
+
// this running.
|
|
392
|
+
"auth.probe": async ({ clientId, clientSecret, fetcher, manifest, settings }) => {
|
|
393
|
+
const token = await accessToken({
|
|
394
|
+
clientId,
|
|
395
|
+
clientSecret,
|
|
396
|
+
fetcher,
|
|
397
|
+
// Mint even if the stored token still looks good — a probe that
|
|
398
|
+
// short-circuits never reaches Klaviyo and reports healthy on a
|
|
399
|
+
// grant revoked an hour ago.
|
|
400
|
+
force: true,
|
|
401
|
+
manifest,
|
|
402
|
+
settings
|
|
403
|
+
});
|
|
404
|
+
return { ok: Boolean(token) };
|
|
405
|
+
}
|
|
406
|
+
},
|
|
234
407
|
icon: klaviyo_default,
|
|
408
|
+
// A grant with no list chosen is authenticated and useless. The list cannot
|
|
409
|
+
// be part of the consent flow — enumerating lists needs the token the consent
|
|
410
|
+
// returns — so it is always a second step, and the card must say so rather
|
|
411
|
+
// than showing Active over nothing.
|
|
412
|
+
incomplete: (data) => {
|
|
413
|
+
var _a;
|
|
414
|
+
return ((_a = data == null ? void 0 : data.settings) == null ? void 0 : _a.list) ? null : "Choose which Klaviyo list your contacts should sync into.";
|
|
415
|
+
},
|
|
235
416
|
label: "klaviyo",
|
|
236
417
|
requires: [
|
|
237
418
|
"KLAVIYO_OAUTH_CLIENT_ID",
|
|
@@ -259,13 +440,24 @@ var klaviyo_default2 = {
|
|
|
259
440
|
"catalog.prices": false,
|
|
260
441
|
"catalog.products": false,
|
|
261
442
|
"catalog.promotions": false,
|
|
262
|
-
"inbound.
|
|
263
|
-
"inbound.
|
|
443
|
+
"inbound.event": false,
|
|
444
|
+
"inbound.process": false,
|
|
445
|
+
"inbound.receive": false,
|
|
264
446
|
"inbound.verify": false,
|
|
265
447
|
"lifecycle.cleanup": false,
|
|
266
448
|
"lifecycle.register": false,
|
|
267
449
|
"lifecycle.rehydrate": false
|
|
268
450
|
},
|
|
451
|
+
tasks: () => [
|
|
452
|
+
// Mailchimp carries the same warning, deliberately worded the same way. A
|
|
453
|
+
// merchant who connects either one and is told nothing reasonably assumes
|
|
454
|
+
// contacts are flowing, and finds out weeks later that they are not.
|
|
455
|
+
{
|
|
456
|
+
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.",
|
|
457
|
+
title: "List sync not available yet",
|
|
458
|
+
type: "warning"
|
|
459
|
+
}
|
|
460
|
+
],
|
|
269
461
|
title: "Klaviyo"
|
|
270
462
|
};
|
|
271
463
|
|
|
@@ -317,6 +509,15 @@ var mailchimp_default2 = {
|
|
|
317
509
|
}
|
|
318
510
|
],
|
|
319
511
|
icon: mailchimp_default,
|
|
512
|
+
// A key with no audience chosen is authenticated and inert. Mailchimp also
|
|
513
|
+
// needs its merge fields created on that audience before any Drawbridge total
|
|
514
|
+
// can be written to a member — unlike Klaviyo, its custom fields are not
|
|
515
|
+
// schemaless — so the audience must be picked before lifecycle.register has
|
|
516
|
+
// anything to register against.
|
|
517
|
+
incomplete: (data) => {
|
|
518
|
+
var _a;
|
|
519
|
+
return ((_a = data == null ? void 0 : data.settings) == null ? void 0 : _a.audience) ? null : "Choose which Mailchimp audience your contacts should sync into.";
|
|
520
|
+
},
|
|
320
521
|
label: "mailchimp",
|
|
321
522
|
// Uniform surface, honest answers. A key is stored and can be removed; nothing
|
|
322
523
|
// else is built yet, because audience sync has not shipped. Every false here
|
|
@@ -330,8 +531,9 @@ var mailchimp_default2 = {
|
|
|
330
531
|
"catalog.prices": false,
|
|
331
532
|
"catalog.products": false,
|
|
332
533
|
"catalog.promotions": false,
|
|
333
|
-
"inbound.
|
|
334
|
-
"inbound.
|
|
534
|
+
"inbound.event": false,
|
|
535
|
+
"inbound.process": false,
|
|
536
|
+
"inbound.receive": false,
|
|
335
537
|
"inbound.verify": false,
|
|
336
538
|
"lifecycle.cleanup": false,
|
|
337
539
|
"lifecycle.register": false,
|
|
@@ -365,7 +567,42 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
365
567
|
<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"/>
|
|
366
568
|
</svg>`;
|
|
367
569
|
|
|
570
|
+
// lib/connections/inbound.js
|
|
571
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
572
|
+
var verifySignature = ({ body, descriptor, headers }) => {
|
|
573
|
+
const provided = headers[descriptor.headers.signature];
|
|
574
|
+
if (!provided) {
|
|
575
|
+
throw Object.assign(new Error("Missing webhook signature"), { status: 401 });
|
|
576
|
+
}
|
|
577
|
+
const digest = createHmac(descriptor.signature.algorithm, process.env[descriptor.signature.secret]).update(body).digest(descriptor.signature.encoding);
|
|
578
|
+
const digestBuffer = Buffer.from(digest, descriptor.signature.encoding);
|
|
579
|
+
const providedBuffer = Buffer.from(provided, descriptor.signature.encoding);
|
|
580
|
+
if (digestBuffer.length !== providedBuffer.length || !timingSafeEqual(digestBuffer, providedBuffer)) {
|
|
581
|
+
throw Object.assign(new Error("Invalid webhook signature"), { status: 401 });
|
|
582
|
+
}
|
|
583
|
+
return JSON.parse(body.toString());
|
|
584
|
+
};
|
|
585
|
+
var readEventHeader = ({ descriptor, headers }) => headers[descriptor.headers.event];
|
|
586
|
+
|
|
368
587
|
// lib/connections/shopify.js
|
|
588
|
+
var inbound = {
|
|
589
|
+
headers: {
|
|
590
|
+
event: "x-shopify-topic",
|
|
591
|
+
id: "x-shopify-webhook-id",
|
|
592
|
+
shop: "x-shopify-shop-domain",
|
|
593
|
+
signature: "x-shopify-hmac-sha256"
|
|
594
|
+
},
|
|
595
|
+
signature: {
|
|
596
|
+
algorithm: "sha256",
|
|
597
|
+
encoding: "base64",
|
|
598
|
+
secret: "SHOPIFY_API_SECRET"
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
var COMPLIANCE_TOPICS = /* @__PURE__ */ new Set([
|
|
602
|
+
"customers/data_request",
|
|
603
|
+
"customers/redact",
|
|
604
|
+
"shop/redact"
|
|
605
|
+
]);
|
|
369
606
|
var shopify_default2 = {
|
|
370
607
|
// INSTALL, not oauth. The grant is an OAuth grant, but the merchant never
|
|
371
608
|
// sees a consent screen we sent them to -- they start at the App Store, and
|
|
@@ -410,30 +647,36 @@ var shopify_default2 = {
|
|
|
410
647
|
}
|
|
411
648
|
],
|
|
412
649
|
group: "ecommerce",
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
//
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
650
|
+
// The install is the whole configuration — Shopify hands back the shop and
|
|
651
|
+
// there is nothing further to choose. `shop` absent means the install did not
|
|
652
|
+
// finish, which is a credential problem rather than a setup one, so the
|
|
653
|
+
// stored status already says so.
|
|
654
|
+
incomplete: () => null,
|
|
655
|
+
// verify and event lean entirely on the shared HMAC helper — Shopify's scheme
|
|
656
|
+
// is exactly the shape it covers, so there is nothing vendor-specific to
|
|
657
|
+
// write for either. receive is the one hook that genuinely differs by
|
|
658
|
+
// channel: /events buffers whatever arrives with the shop domain stamped on;
|
|
659
|
+
// /compliance enforces the topic allowlist above, because answering one late
|
|
660
|
+
// is a legal deadline rather than a retry.
|
|
661
|
+
hooks: {
|
|
662
|
+
"inbound.event": (args) => readEventHeader({ ...args, descriptor: inbound }),
|
|
663
|
+
"inbound.receive": ({ channel, event, headers, payload }) => {
|
|
664
|
+
if (channel === "compliance" && !COMPLIANCE_TOPICS.has(event)) {
|
|
665
|
+
throw Object.assign(new Error("Unrecognized compliance topic: " + event), { status: 401 });
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
// Compliance payloads already carry shop_domain in the body — Shopify's
|
|
669
|
+
// own GDPR shape. The app-level event stream does not; that domain
|
|
670
|
+
// lives only in the header, so it is stamped on here rather than left
|
|
671
|
+
// for drawbridge-sync to reach into headers nobody hands it.
|
|
672
|
+
data: channel === "compliance" ? payload : { ...payload, shop_domain: headers[inbound.headers.shop] || null },
|
|
673
|
+
provider: { id: headers[inbound.headers.id] || null }
|
|
674
|
+
};
|
|
429
675
|
},
|
|
430
|
-
|
|
431
|
-
algorithm: "sha256",
|
|
432
|
-
encoding: "base64",
|
|
433
|
-
secret: "SHOPIFY_API_SECRET"
|
|
434
|
-
}
|
|
676
|
+
"inbound.verify": (args) => verifySignature({ ...args, descriptor: inbound })
|
|
435
677
|
},
|
|
436
678
|
icon: shopify_default,
|
|
679
|
+
inbound,
|
|
437
680
|
label: "shopify",
|
|
438
681
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
439
682
|
// exists and the app is fully configured. Requiring all four means it can
|
|
@@ -462,8 +705,9 @@ var shopify_default2 = {
|
|
|
462
705
|
"catalog.prices": false,
|
|
463
706
|
"catalog.products": true,
|
|
464
707
|
"catalog.promotions": true,
|
|
465
|
-
"inbound.
|
|
466
|
-
"inbound.
|
|
708
|
+
"inbound.event": true,
|
|
709
|
+
"inbound.process": true,
|
|
710
|
+
"inbound.receive": true,
|
|
467
711
|
"inbound.verify": true,
|
|
468
712
|
"lifecycle.cleanup": true,
|
|
469
713
|
"lifecycle.register": true,
|
|
@@ -607,6 +851,9 @@ var webhook_default = {
|
|
|
607
851
|
// It is the one card that reads wrong — our logo among vendor logos — and it
|
|
608
852
|
// wants a mark of its own when there is one.
|
|
609
853
|
icon: drawbridge_default,
|
|
854
|
+
// The destination url is supplied per step, not per connection, so there is
|
|
855
|
+
// nothing to finish here — generating the secret IS connecting.
|
|
856
|
+
incomplete: () => null,
|
|
610
857
|
label: "webhook",
|
|
611
858
|
// Gated on the encryption secret: without it the signing secret could not be
|
|
612
859
|
// stored safely, so the connection must not be offered at all.
|
|
@@ -623,8 +870,9 @@ var webhook_default = {
|
|
|
623
870
|
"catalog.prices": false,
|
|
624
871
|
"catalog.products": false,
|
|
625
872
|
"catalog.promotions": false,
|
|
626
|
-
"inbound.
|
|
627
|
-
"inbound.
|
|
873
|
+
"inbound.event": false,
|
|
874
|
+
"inbound.process": false,
|
|
875
|
+
"inbound.receive": false,
|
|
628
876
|
"inbound.verify": false,
|
|
629
877
|
"lifecycle.cleanup": false,
|
|
630
878
|
"lifecycle.register": false,
|
|
@@ -713,13 +961,21 @@ var build = (manifest) => {
|
|
|
713
961
|
throw new Error(manifest.slug + " is oauth and must declare auth.oauth." + field);
|
|
714
962
|
}
|
|
715
963
|
}
|
|
964
|
+
if (manifest.auth.oauth.redirect !== "/api/connection/" + manifest.slug + "/callback") {
|
|
965
|
+
throw new Error(
|
|
966
|
+
manifest.slug + " declares auth.oauth.redirect " + manifest.auth.oauth.redirect + " but the only callback route is /api/connection/" + manifest.slug + "/callback"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
716
969
|
}
|
|
717
|
-
if (((_d = manifest.supports) == null ? void 0 : _d["inbound.
|
|
718
|
-
throw new Error(manifest.slug + " supports inbound.
|
|
970
|
+
if (((_d = manifest.supports) == null ? void 0 : _d["inbound.event"]) && !((_f = (_e = manifest.inbound) == null ? void 0 : _e.headers) == null ? void 0 : _f.event)) {
|
|
971
|
+
throw new Error(manifest.slug + " supports inbound.event but declares no inbound.headers.event");
|
|
719
972
|
}
|
|
720
973
|
if (((_g = manifest.supports) == null ? void 0 : _g["inbound.verify"]) && !((_i = (_h = manifest.inbound) == null ? void 0 : _h.headers) == null ? void 0 : _i.signature)) {
|
|
721
974
|
throw new Error(manifest.slug + " supports inbound.verify but declares no inbound.headers.signature");
|
|
722
975
|
}
|
|
976
|
+
if (typeof (manifest == null ? void 0 : manifest.incomplete) !== "function") {
|
|
977
|
+
throw new Error(manifest.slug + " must declare incomplete( data ) \u2014 return null when the connection is usable, or the reason it is not");
|
|
978
|
+
}
|
|
723
979
|
if (!Array.isArray(manifest == null ? void 0 : manifest.setup) || !manifest.setup.length) {
|
|
724
980
|
throw new Error(manifest.slug + " needs a setup guide \u2014 an array of steps for its page");
|
|
725
981
|
}
|
|
@@ -830,7 +1086,7 @@ var runHook = async (slug, name, args = {}) => {
|
|
|
830
1086
|
try {
|
|
831
1087
|
return { outcome: OUTCOMES.answered, result: await hook(args) };
|
|
832
1088
|
} catch (error) {
|
|
833
|
-
return { error: (error == null ? void 0 : error.message) || "failed", outcome: OUTCOMES.failed };
|
|
1089
|
+
return { error: (error == null ? void 0 : error.message) || "failed", outcome: OUTCOMES.failed, status: (error == null ? void 0 : error.status) || null };
|
|
834
1090
|
}
|
|
835
1091
|
};
|
|
836
1092
|
var stepQueues = (env = {}) => Object.fromEntries(
|
|
@@ -869,6 +1125,9 @@ var publicConnectionKeys = Object.freeze([
|
|
|
869
1125
|
"group",
|
|
870
1126
|
"id",
|
|
871
1127
|
"image",
|
|
1128
|
+
// The reason a connected vendor still is not usable — a Klaviyo grant with no
|
|
1129
|
+
// list chosen. Public because the card that shows Pending has to say why.
|
|
1130
|
+
"incomplete",
|
|
872
1131
|
"label",
|
|
873
1132
|
"setup",
|
|
874
1133
|
"settings",
|
|
@@ -896,7 +1155,7 @@ var projectConnection = (record) => {
|
|
|
896
1155
|
var resolveConnection = (item, data) => {
|
|
897
1156
|
if (!item) return item;
|
|
898
1157
|
return Object.fromEntries(
|
|
899
|
-
Object.entries(item).filter(([key]) => !["auth", "enabled", "fields", "inbound", "requires", "steps", "supports"].includes(key)).map(([key, value]) => [
|
|
1158
|
+
Object.entries(item).filter(([key]) => !["auth", "enabled", "fields", "hooks", "inbound", "requires", "steps", "supports"].includes(key)).map(([key, value]) => [
|
|
900
1159
|
key,
|
|
901
1160
|
typeof value === "function" ? value(data) : value
|
|
902
1161
|
])
|
|
@@ -910,6 +1169,7 @@ export {
|
|
|
910
1169
|
INPUTS,
|
|
911
1170
|
OAUTH_FIELDS,
|
|
912
1171
|
OUTCOMES,
|
|
1172
|
+
accessToken,
|
|
913
1173
|
availableConnections,
|
|
914
1174
|
build,
|
|
915
1175
|
connectFields,
|
|
@@ -918,6 +1178,7 @@ export {
|
|
|
918
1178
|
consentUrl,
|
|
919
1179
|
exchange,
|
|
920
1180
|
hookSupport,
|
|
1181
|
+
isStale,
|
|
921
1182
|
mergeSettings,
|
|
922
1183
|
pkcePair,
|
|
923
1184
|
projectConnection,
|
|
@@ -928,5 +1189,6 @@ export {
|
|
|
928
1189
|
resolveConnection,
|
|
929
1190
|
runHook,
|
|
930
1191
|
scopesMessage,
|
|
931
|
-
stepQueues
|
|
1192
|
+
stepQueues,
|
|
1193
|
+
tokenSettings
|
|
932
1194
|
};
|
package/package.json
CHANGED