@drawbridge/drawbridge-utils 0.0.176 → 0.0.178
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/admin-B9ZaLvan.d.cts +697 -0
- package/dist/admin-C3HtEM6h.d.ts +697 -0
- package/dist/billing-Bc4yo9XG.d.cts +175 -0
- package/dist/billing-mNsKflmQ.d.ts +175 -0
- package/dist/billing.d.cts +1 -1
- package/dist/billing.d.ts +1 -1
- package/dist/connections/index.cjs +3391 -271
- package/dist/connections/index.d.cts +13 -4
- package/dist/connections/index.d.ts +13 -4
- package/dist/connections/index.js +3388 -270
- package/dist/features.cjs +3080 -241
- package/dist/features.d.cts +13 -4
- package/dist/features.d.ts +13 -4
- package/dist/features.js +3120 -275
- package/dist/http.cjs +10 -1
- package/dist/http.d.cts +10 -1
- package/dist/http.d.ts +10 -1
- package/dist/http.js +10 -1
- package/dist/{index-qY18QITf.d.cts → index-C2rxasGZ.d.cts} +2670 -354
- package/dist/{index-B8JhYfvU.d.ts → index-DOYCXtd7.d.ts} +2670 -354
- package/dist/oauth/index.d.cts +1 -1
- package/dist/oauth/index.d.ts +1 -1
- package/dist/oauth-BJDh0sdM.d.cts +527 -0
- package/dist/oauth-DveZMLHx.d.ts +527 -0
- package/dist/partner-BOZltuh2.d.ts +94 -0
- package/dist/partner-ed2OfW1J.d.cts +94 -0
- package/dist/plans.cjs +3079 -240
- package/dist/plans.d.cts +13 -4
- package/dist/plans.d.ts +13 -4
- package/dist/plans.js +3120 -275
- package/dist/pricing.cjs +3112 -273
- package/dist/pricing.d.cts +13 -4
- package/dist/pricing.d.ts +13 -4
- package/dist/pricing.js +3117 -272
- package/dist/providers.cjs +3082 -262
- package/dist/providers.d.cts +12 -3
- package/dist/providers.d.ts +12 -3
- package/dist/providers.js +3086 -260
- package/dist/sendgrid.cjs +10 -1
- package/dist/sendgrid.js +10 -1
- package/dist/shopify/admin.cjs +562 -0
- package/dist/shopify/admin.d.cts +3 -0
- package/dist/shopify/admin.d.ts +3 -0
- package/dist/shopify/admin.js +528 -0
- package/dist/shopify/billing.cjs +166 -0
- package/dist/shopify/billing.d.cts +3 -0
- package/dist/shopify/billing.d.ts +3 -0
- package/dist/shopify/billing.js +140 -0
- package/dist/shopify/constants.cjs +63 -0
- package/dist/shopify/constants.d.cts +58 -0
- package/dist/shopify/constants.d.ts +58 -0
- package/dist/shopify/constants.js +32 -0
- package/dist/shopify/oauth.cjs +509 -0
- package/dist/shopify/oauth.d.cts +7 -0
- package/dist/shopify/oauth.d.ts +7 -0
- package/dist/shopify/oauth.js +466 -0
- package/dist/shopify/partner.cjs +156 -0
- package/dist/shopify/partner.d.cts +3 -0
- package/dist/shopify/partner.d.ts +3 -0
- package/dist/shopify/partner.js +130 -0
- package/dist/shopify/storefront.cjs +611 -0
- package/dist/shopify/storefront.d.cts +3 -0
- package/dist/shopify/storefront.d.ts +3 -0
- package/dist/shopify/storefront.js +576 -0
- package/dist/storefront-C8FKOGeD.d.cts +659 -0
- package/dist/storefront-DJFGLqPl.d.ts +659 -0
- package/dist/twilio.cjs +10 -1
- package/dist/twilio.js +10 -1
- package/package.json +98 -68
package/dist/pricing.cjs
CHANGED
|
@@ -469,6 +469,10 @@ var HOOKS = Object.freeze({
|
|
|
469
469
|
"code",
|
|
470
470
|
// Create the buyer at the vendor, so an order can be attributed to them.
|
|
471
471
|
"customer",
|
|
472
|
+
// HOW MANY OF THIS PRODUCT ARE LEFT, at the vendor. Read at the moment a
|
|
473
|
+
// campaign's product list is rendered rather than stored, because stock is
|
|
474
|
+
// the one product fact that is stale the instant it is written down.
|
|
475
|
+
"inventory",
|
|
472
476
|
// An order arrived at the vendor: attribute it, record it, update totals.
|
|
473
477
|
"order",
|
|
474
478
|
// Pull product data across on a vendor update.
|
|
@@ -536,7 +540,19 @@ var HOOKS = Object.freeze({
|
|
|
536
540
|
organizations: Object.freeze(["add", "list", "remove"]),
|
|
537
541
|
// What the merchant pays the VENDOR. Pairs with auth.install.billing, which
|
|
538
542
|
// declares the plan they are being charged on.
|
|
539
|
-
|
|
543
|
+
//
|
|
544
|
+
// `verify` is the odd one of the three: not something a merchant does, but
|
|
545
|
+
// the question "is this store's billing actually metered right now". It is
|
|
546
|
+
// here because it is the same subject — and because the answer was being
|
|
547
|
+
// derived in TWO places, the vendor's health check and drawbridge-sync's
|
|
548
|
+
// approval webhook, whose copy said in its own comment that it matched the
|
|
549
|
+
// other and did not.
|
|
550
|
+
// `charge` and `verify` are the two that are not merchant actions: one
|
|
551
|
+
// sends a usage charge to the vendor's meter, the other asks whether that
|
|
552
|
+
// meter is actually working. Both are here because both are the same
|
|
553
|
+
// subject — what the merchant pays the vendor — and both were being done
|
|
554
|
+
// from drawbridge-sync with a direct import of the vendor's client.
|
|
555
|
+
plan: Object.freeze(["cancel", "charge", "reselect", "verify"]),
|
|
540
556
|
// The embedded app's own session, stored by us on the vendor's behalf.
|
|
541
557
|
session: Object.freeze(["clear", "read", "save"]),
|
|
542
558
|
// The one-time code that hands a merchant from the embedded app into
|
|
@@ -621,7 +637,7 @@ var ANSWER_KEYS = Object.freeze([
|
|
|
621
637
|
"usage"
|
|
622
638
|
].sort());
|
|
623
639
|
var WRITE_OPERATIONS = Object.freeze(["create", "delete", "update"]);
|
|
624
|
-
var INSTALL_SIGNATURE = Object.freeze({ options: ["read"
|
|
640
|
+
var INSTALL_SIGNATURE = Object.freeze({ options: ["read"], props: ["context"] });
|
|
625
641
|
var LIFECYCLE_SIGNATURE = Object.freeze({ options: ["dispatch"], props: ["context"] });
|
|
626
642
|
var RESOURCE_SIGNATURE = Object.freeze({
|
|
627
643
|
options: ["fetcher", "shopify"],
|
|
@@ -638,15 +654,30 @@ var HOOK_SLOT_PROPS = Object.freeze({
|
|
|
638
654
|
// the second bag would be silently discarded, which is exactly the
|
|
639
655
|
// stubbed-fetcher-reaches-production failure the split exists to prevent.
|
|
640
656
|
"auth.token": false,
|
|
641
|
-
// COMMERCE. `cart` is
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
657
|
+
// COMMERCE. `cart` is the odd one: it runs for a SHOPPER on a public page,
|
|
658
|
+
// not for an organization inside a workflow, so it takes no `connection` and
|
|
659
|
+
// no `step`. It reads the storefront with the store's own storefront token
|
|
660
|
+
// and mints a checkout token from DRAWBRIDGE'S app credentials, which is why
|
|
661
|
+
// `clientId`/`clientSecret` are here — as props, the way auth.disconnect
|
|
662
|
+
// already carries them, because a credential is a fact of the run rather
|
|
663
|
+
// than a service.
|
|
664
|
+
"commerce.cart": Object.freeze({
|
|
665
|
+
options: ["fetcher", "shopify"],
|
|
666
|
+
props: ["clientId", "clientSecret", "context", "settings"]
|
|
667
|
+
}),
|
|
646
668
|
"commerce.code": Object.freeze({ options: ["adminToken", "shopify"], props: ["connection", "context", "step"] }),
|
|
647
669
|
"commerce.customer": Object.freeze({ options: ["adminToken", "read", "shopify"], props: ["connection", "context"] }),
|
|
670
|
+
"commerce.inventory": Object.freeze({ options: ["adminToken", "shopify"], props: ["connection", "context"] }),
|
|
648
671
|
"commerce.order": Object.freeze({ options: ["conversionRate", "logger", "mintId", "read"], props: ["connection", "context"] }),
|
|
649
|
-
|
|
672
|
+
// THREE OPERATIONS, one slot — record, sync, variants. `settings` and
|
|
673
|
+
// `adminToken` arrived with the pull half: the storefront read needs the
|
|
674
|
+
// store's storefront token and the product-feedback send needs its admin one,
|
|
675
|
+
// and both are resolved by the caller because minting either needs a
|
|
676
|
+
// controller.
|
|
677
|
+
"commerce.product": Object.freeze({
|
|
678
|
+
options: ["adminToken", "mintId", "read", "shopify"],
|
|
679
|
+
props: ["connection", "context", "settings", "workflow"]
|
|
680
|
+
}),
|
|
650
681
|
// THE ADDRESS IS `contact` IN BOTH, and the shell already says why: it is the
|
|
651
682
|
// person, not the lead document. `remove` called it `email` and `sync` called
|
|
652
683
|
// it `doc` on one vendor and `lead` on three — three names for two facts.
|
|
@@ -659,19 +690,58 @@ var HOOK_SLOT_PROPS = Object.freeze({
|
|
|
659
690
|
"inbound.process": Object.freeze({ options: ["dispatch"], props: ["context"] }),
|
|
660
691
|
"inbound.receive": Object.freeze({ options: [], props: ["channel", "event", "headers", "payload"] }),
|
|
661
692
|
"inbound.verify": Object.freeze({ options: [], props: ["body", "channel", "headers", "secret", "url"] }),
|
|
662
|
-
// INSTALL — thirteen slots,
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
693
|
+
// INSTALL — thirteen slots, and they are the thirteen routes that used to be
|
|
694
|
+
// api/route/shopify-embedded.js. `context` is the request's own body in all
|
|
695
|
+
// of them; the options are where they differ, and every difference below was
|
|
696
|
+
// forced by a line in those routes rather than chosen.
|
|
697
|
+
//
|
|
698
|
+
// SEVEN NEED NOTHING BUT `read`. The two session slots that write, the two
|
|
699
|
+
// token slots that write, the two clears, and both account slots answer from
|
|
700
|
+
// the shared install record alone.
|
|
701
|
+
//
|
|
702
|
+
// SIX TAKE MORE, and the widenings are worth naming because each is a fact
|
|
703
|
+
// Drawbridge owns that the manifest must not:
|
|
704
|
+
//
|
|
705
|
+
// conversionRate, planTitle what Drawbridge charges this organization.
|
|
706
|
+
// The org list renders both, and importing the
|
|
707
|
+
// plan catalogue here is the cycle
|
|
708
|
+
// features -> connections -> shopify -> plans
|
|
709
|
+
// -> features that was just broken.
|
|
710
|
+
// currencies which settlement currencies Drawbridge can
|
|
711
|
+
// bill. A store outside them fails the
|
|
712
|
+
// connection validator INSIDE the transaction,
|
|
713
|
+
// which surfaces as an opaque 500, so `add`
|
|
714
|
+
// checks first and says so.
|
|
715
|
+
// mintId the connection's id, known before the write
|
|
716
|
+
// that creates it, because the organization's
|
|
717
|
+
// own refs have to name it in the same
|
|
718
|
+
// transaction.
|
|
719
|
+
// shopify the vendor. Four slots call the store: two
|
|
720
|
+
// read the live plan, two release it.
|
|
721
|
+
// mark THE ONE WRITE A DESCRIPTION CANNOT CARRY, and
|
|
722
|
+
// `reselect` is the only slot with one. See its
|
|
723
|
+
// implementation for why the order is the point.
|
|
668
724
|
"install.account.link": INSTALL_SIGNATURE,
|
|
669
725
|
"install.account.unlink": INSTALL_SIGNATURE,
|
|
670
|
-
"install.organizations.add":
|
|
671
|
-
|
|
726
|
+
"install.organizations.add": Object.freeze({
|
|
727
|
+
options: ["currencies", "mintId", "read", "shopify"],
|
|
728
|
+
props: ["context"]
|
|
729
|
+
}),
|
|
730
|
+
"install.organizations.list": Object.freeze({
|
|
731
|
+
options: ["conversionRate", "planTitle", "read", "shopify"],
|
|
732
|
+
props: ["context"]
|
|
733
|
+
}),
|
|
672
734
|
"install.organizations.remove": INSTALL_SIGNATURE,
|
|
673
|
-
"install.plan.cancel":
|
|
674
|
-
|
|
735
|
+
"install.plan.cancel": Object.freeze({ options: ["read", "shopify"], props: ["context"] }),
|
|
736
|
+
// `manifest` because the handle this charge bills under is the vendor's own
|
|
737
|
+
// declaration (auth.install.billing.handle), and checking the job against the
|
|
738
|
+
// declaration is what the enqueuer-checks-itself version could not do.
|
|
739
|
+
"install.plan.charge": Object.freeze({
|
|
740
|
+
options: ["shopify"],
|
|
741
|
+
props: ["clientId", "clientSecret", "context", "manifest"]
|
|
742
|
+
}),
|
|
743
|
+
"install.plan.reselect": Object.freeze({ options: ["mark", "read", "shopify"], props: ["context"] }),
|
|
744
|
+
"install.plan.verify": Object.freeze({ options: ["adminToken", "read", "shopify"], props: ["context"] }),
|
|
675
745
|
"install.session.clear": INSTALL_SIGNATURE,
|
|
676
746
|
"install.session.read": INSTALL_SIGNATURE,
|
|
677
747
|
"install.session.save": INSTALL_SIGNATURE,
|
|
@@ -722,6 +792,16 @@ var TRIGGERS = Object.freeze({
|
|
|
722
792
|
"range.ended": Object.freeze({ event: "range.ended", type: "range" }),
|
|
723
793
|
"range.started": Object.freeze({ event: "range.started", type: "range" }),
|
|
724
794
|
"schedule.day": Object.freeze({ event: "day", type: "schedule" }),
|
|
795
|
+
// THE THREE SHOPIFY TRIGGERS, which were never in this vocabulary because the
|
|
796
|
+
// four workflows they fire were hand-written in drawbridge-sync's register job
|
|
797
|
+
// rather than provisioned from the manifest. `webhook` is a fourth trigger
|
|
798
|
+
// TYPE, and it is the honest name: these fire when the vendor calls us, which
|
|
799
|
+
// is neither an event Drawbridge raised nor a schedule it keeps.
|
|
800
|
+
"shopify.order.create": Object.freeze({ event: "shopify.order.create", type: "webhook" }),
|
|
801
|
+
"shopify.product.update": Object.freeze({ event: "shopify.product.update", type: "webhook" }),
|
|
802
|
+
// The token audit, dispatched programmatically by the register job when a
|
|
803
|
+
// store's grant is exchanged or rotated.
|
|
804
|
+
"shopify.token": Object.freeze({ event: "shopify.token", type: "event" }),
|
|
725
805
|
"schedule.month": Object.freeze({ event: "month", type: "schedule" }),
|
|
726
806
|
"schedule.week": Object.freeze({ event: "week", type: "schedule" }),
|
|
727
807
|
"segment.contact.add": Object.freeze({ event: "segment.contact.add", type: "event" }),
|
|
@@ -774,7 +854,7 @@ var OUTCOMES = Object.freeze({
|
|
|
774
854
|
unimplemented: "unimplemented",
|
|
775
855
|
unsupported: "unsupported"
|
|
776
856
|
});
|
|
777
|
-
var STATUSES = Object.freeze(["active", "
|
|
857
|
+
var STATUSES = Object.freeze(["active", "error", "pending"]);
|
|
778
858
|
var PLATFORM_CREDENTIALS = Object.freeze([
|
|
779
859
|
// Seals a merchant's stored connection settings. The webhook connection is
|
|
780
860
|
// gated on it because its whole credential is a secret WE mint and store, so
|
|
@@ -830,6 +910,52 @@ var ERROR_SOURCES = Object.freeze({
|
|
|
830
910
|
"shopify.register.webhooks": true
|
|
831
911
|
});
|
|
832
912
|
|
|
913
|
+
// lib/encrypt.js
|
|
914
|
+
var import_crypto2 = __toESM(require("crypto"), 1);
|
|
915
|
+
|
|
916
|
+
// lib/token.js
|
|
917
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
918
|
+
var generate = (bytes = 32, encoding = "base64url") => {
|
|
919
|
+
const buf = import_crypto.default.randomBytes(bytes);
|
|
920
|
+
return encoding ? buf.toString(encoding) : buf;
|
|
921
|
+
};
|
|
922
|
+
var hash = (value, key) => {
|
|
923
|
+
if (!value) return null;
|
|
924
|
+
if (key) {
|
|
925
|
+
return import_crypto.default.createHmac("sha256", key).update(String(value)).digest("base64url");
|
|
926
|
+
}
|
|
927
|
+
return import_crypto.default.createHash("sha256").update(String(value)).digest("base64url");
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// lib/encrypt.js
|
|
931
|
+
var ALGORITHM = "aes-256-gcm";
|
|
932
|
+
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
933
|
+
var encrypt = (value) => {
|
|
934
|
+
const iv = generate(12, null);
|
|
935
|
+
const cipher = import_crypto2.default.createCipheriv(ALGORITHM, getKey(), iv);
|
|
936
|
+
const data2 = Buffer.concat([
|
|
937
|
+
cipher.update(JSON.stringify(value), "utf8"),
|
|
938
|
+
cipher.final()
|
|
939
|
+
]);
|
|
940
|
+
const tag = cipher.getAuthTag();
|
|
941
|
+
return [iv, tag, data2].map((b) => b.toString("hex")).join(":");
|
|
942
|
+
};
|
|
943
|
+
var decrypt = (value) => {
|
|
944
|
+
if (typeof value !== "string") return value;
|
|
945
|
+
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
946
|
+
const decipher = import_crypto2.default.createDecipheriv(
|
|
947
|
+
ALGORITHM,
|
|
948
|
+
getKey(),
|
|
949
|
+
Buffer.from(ivHex, "hex")
|
|
950
|
+
);
|
|
951
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
952
|
+
const result = Buffer.concat([
|
|
953
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
954
|
+
decipher.final()
|
|
955
|
+
]);
|
|
956
|
+
return JSON.parse(result.toString("utf8"));
|
|
957
|
+
};
|
|
958
|
+
|
|
833
959
|
// lib/connections/oauth.js
|
|
834
960
|
var import_node_crypto = require("crypto");
|
|
835
961
|
var credentials = ({ basic, clientId, clientSecret }) => basic ? {
|
|
@@ -856,10 +982,10 @@ var authToken = async ({
|
|
|
856
982
|
const renewing = !code2;
|
|
857
983
|
if (renewing && !refreshToken) throw new Error("Nothing has been consented to yet, so there is no refresh token to spend");
|
|
858
984
|
if (!renewing && descriptor.pkce && !verifier) throw new Error("This connection requires PKCE, so the code verifier is not optional");
|
|
859
|
-
const
|
|
985
|
+
const client2 = credentials({ basic, clientId, clientSecret });
|
|
860
986
|
const response = await fetcher(descriptor.endpoints.token, {
|
|
861
987
|
body: new URLSearchParams({
|
|
862
|
-
...
|
|
988
|
+
...client2.body,
|
|
863
989
|
...renewing ? { grant_type: "refresh_token", refresh_token: refreshToken } : {
|
|
864
990
|
code: decodeURIComponent(String(code2 || "").trim()),
|
|
865
991
|
grant_type: "authorization_code",
|
|
@@ -867,7 +993,7 @@ var authToken = async ({
|
|
|
867
993
|
...descriptor.pkce && { code_verifier: verifier }
|
|
868
994
|
}
|
|
869
995
|
}),
|
|
870
|
-
headers: { "content-type": "application/x-www-form-urlencoded", ...
|
|
996
|
+
headers: { "content-type": "application/x-www-form-urlencoded", ...client2.headers },
|
|
871
997
|
method: "POST",
|
|
872
998
|
signal: AbortSignal.timeout(15e3)
|
|
873
999
|
});
|
|
@@ -1262,9 +1388,18 @@ var scaffold_default = {
|
|
|
1262
1388
|
commerce: {
|
|
1263
1389
|
// NEW in phase 1 — the Checkout Kit cart path, which writes nothing and is
|
|
1264
1390
|
// the last api surface Shopify owns outside a hook.
|
|
1265
|
-
|
|
1391
|
+
// NO `connection` AND NO `step`: this one runs for a SHOPPER on a public
|
|
1392
|
+
// page, not for an organization inside a workflow. It reads the storefront
|
|
1393
|
+
// with the store's storefront token and mints a checkout token from
|
|
1394
|
+
// DRAWBRIDGE'S app credentials, which is why the credential pair rides in
|
|
1395
|
+
// as props — the way auth.disconnect already carries it.
|
|
1396
|
+
cart: async ({ clientId, clientSecret, context, settings }, { fetcher, shopify } = {}) => ANSWER,
|
|
1266
1397
|
code: async ({ connection: connection2, context, step }, { adminToken, shopify } = {}) => ANSWER,
|
|
1267
1398
|
customer: async ({ connection: connection2, context }, { adminToken, read, shopify } = {}) => ANSWER,
|
|
1399
|
+
// STOCK, READ AT RENDER TIME rather than stored. It is the one product
|
|
1400
|
+
// fact that is stale the instant it is written down, so the campaign
|
|
1401
|
+
// product list asks the vendor and caches the answer for a minute.
|
|
1402
|
+
inventory: async ({ connection: connection2, context }, { adminToken, shopify } = {}) => ANSWER,
|
|
1268
1403
|
order: async ({ connection: connection2, context }, { logger: logger2, mintId, read } = {}) => ANSWER,
|
|
1269
1404
|
// THIS IS WHERE `product.insert` LANDS.
|
|
1270
1405
|
//
|
|
@@ -1286,7 +1421,7 @@ var scaffold_default = {
|
|
|
1286
1421
|
// nothing and shows up only as `workflow.dispatch.candidates matched : 0` in
|
|
1287
1422
|
// a log. Phase 1 closes it and build() checks every trigger against it.
|
|
1288
1423
|
product: {
|
|
1289
|
-
hook: async ({ connection: connection2, context, workflow }, { mintId, read } = {}) => ANSWER,
|
|
1424
|
+
hook: async ({ connection: connection2, context, settings, workflow }, { adminToken, mintId, read, shopify } = {}) => ANSWER,
|
|
1290
1425
|
step: () => ({
|
|
1291
1426
|
description: "Syncs product data from the vendor when a product changes.",
|
|
1292
1427
|
key: "Vendor Product Sync",
|
|
@@ -1434,8 +1569,19 @@ var scaffold_default = {
|
|
|
1434
1569
|
// Vendor billing, the only group that is not generic. Pairs with
|
|
1435
1570
|
// auth.install.billing, which declares what the merchant is being charged.
|
|
1436
1571
|
plan: {
|
|
1437
|
-
cancel: async ({ context }, {
|
|
1438
|
-
|
|
1572
|
+
cancel: async ({ context }, { read, shopify } = {}) => ANSWER,
|
|
1573
|
+
// NOT A MERCHANT ACTION EITHER — one attributed order's usage charge,
|
|
1574
|
+
// sent to the vendor's meter. The credential pair rides in as props
|
|
1575
|
+
// because the charge authenticates as the APP, not as the store.
|
|
1576
|
+
charge: async ({ clientId, clientSecret, context, manifest }, { shopify } = {}) => ANSWER,
|
|
1577
|
+
// `mark` is the one write a description cannot carry: the marker must
|
|
1578
|
+
// be persisted BEFORE the approval is released, and a described write
|
|
1579
|
+
// lands after the hook returns.
|
|
1580
|
+
reselect: async ({ context }, { mark, read, shopify } = {}) => ANSWER,
|
|
1581
|
+
// NOT A MERCHANT ACTION — the question "is this store's billing
|
|
1582
|
+
// actually metered right now", asked by the daily health check and by
|
|
1583
|
+
// the vendor's own approval webhook, which used to hand-copy it.
|
|
1584
|
+
verify: async ({ context }, { adminToken, read, shopify } = {}) => ANSWER
|
|
1439
1585
|
},
|
|
1440
1586
|
// The embedded app's own session, stored by us on the vendor's behalf.
|
|
1441
1587
|
session: {
|
|
@@ -1921,16 +2067,19 @@ var attentive_default2 = {
|
|
|
1921
2067
|
"Anyone who has opted out in Drawbridge is sent to Attentive as an unsubscribe rather than omitted, so a person who asked not to be contacted stays suppressed in both systems instead of quietly reappearing.",
|
|
1922
2068
|
"Attentive accepts these updates and applies them in the background, so a contact appears in your segment shortly after the sync rather than the instant it runs."
|
|
1923
2069
|
],
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
// up for — and a sentence here is an ADDITION to those, for a vendor whose
|
|
1928
|
-
// failures need saying differently. This one's do not.
|
|
2070
|
+
// WHAT THE MERCHANT READS WHEN A CONNECT FAILS. Two states, because the
|
|
2071
|
+
// consent screen has two ways to end: they said no, or something between
|
|
2072
|
+
// here and the vendor went wrong.
|
|
1929
2073
|
//
|
|
1930
|
-
//
|
|
1931
|
-
//
|
|
1932
|
-
//
|
|
1933
|
-
errors:
|
|
2074
|
+
// THE VENDOR IS NAMED IN BOTH. The dashboard's generic copy says "the
|
|
2075
|
+
// authorization"; a merchant connecting two accounts in one sitting needs
|
|
2076
|
+
// to know which one this was.
|
|
2077
|
+
errors: {
|
|
2078
|
+
connect: {
|
|
2079
|
+
denied: "The Attentive authorization was declined, so nothing was connected.",
|
|
2080
|
+
invalid: "We couldn't complete the Attentive connection. Try connecting again."
|
|
2081
|
+
}
|
|
2082
|
+
},
|
|
1934
2083
|
excerpt: "Sync your Drawbridge contacts into an Attentive segment.",
|
|
1935
2084
|
// NAMES THE SEGMENT STEP, because `status` below gates on it: a grant with
|
|
1936
2085
|
// no segment chosen sits at Pending, and a guide that stops at the consent
|
|
@@ -2458,6 +2607,15 @@ var readEventHeader = ({ descriptor, headers }) => ({ result: { event: headers[d
|
|
|
2458
2607
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
2459
2608
|
var request = async ({
|
|
2460
2609
|
body,
|
|
2610
|
+
// THE TRANSPORT, injectable and defaulted to the real one.
|
|
2611
|
+
//
|
|
2612
|
+
// Every other vendor client in this package takes a `fetcher` — it is how
|
|
2613
|
+
// klaviyo, mailchimp and attentive are tested without a network, and it is a
|
|
2614
|
+
// declared HOOK_OPTION. The Shopify client had no seam at all, so the only way
|
|
2615
|
+
// to test a hook that used it was to inject the whole SDK namespace; that
|
|
2616
|
+
// injection existed to work around an import cycle, and when the cycle went
|
|
2617
|
+
// the tests lost their only hold. This is the seam they should have had.
|
|
2618
|
+
fetcher = fetch,
|
|
2461
2619
|
headers = {},
|
|
2462
2620
|
method = "GET",
|
|
2463
2621
|
query,
|
|
@@ -2471,7 +2629,7 @@ var request = async ({
|
|
|
2471
2629
|
}
|
|
2472
2630
|
;
|
|
2473
2631
|
const isForm = type === "form";
|
|
2474
|
-
const response = await
|
|
2632
|
+
const response = await fetcher(fullUrl.toString(), {
|
|
2475
2633
|
method,
|
|
2476
2634
|
headers: {
|
|
2477
2635
|
"Content-Type": isForm ? "application/x-www-form-urlencoded" : "application/json",
|
|
@@ -2913,15 +3071,12 @@ var drawbridge_default2 = {
|
|
|
2913
3071
|
description: [
|
|
2914
3072
|
"Drawbridge sends your notification emails and your entrants' emails, and recalculates segment membership on a schedule. It is always on and needs nothing connected."
|
|
2915
3073
|
],
|
|
2916
|
-
//
|
|
2917
|
-
//
|
|
2918
|
-
//
|
|
2919
|
-
//
|
|
2920
|
-
// failures need saying differently. This one's do not.
|
|
3074
|
+
// NOTHING IS EVER CONNECTED, so no connect can fail. This is the platform's
|
|
3075
|
+
// own manifest — always on, private, never in the catalogue — and it exists
|
|
3076
|
+
// to give Drawbridge's own steps a hook to point at. `denied` and `invalid`
|
|
3077
|
+
// describe a consent screen this connection does not have.
|
|
2921
3078
|
//
|
|
2922
|
-
//
|
|
2923
|
-
// manifests and there was no way to tell "nothing to add" from "nobody
|
|
2924
|
-
// wrote it".
|
|
3079
|
+
// `false` is the recorded decision, not silence.
|
|
2925
3080
|
errors: false,
|
|
2926
3081
|
excerpt: "The steps Drawbridge runs itself.",
|
|
2927
3082
|
guide: [
|
|
@@ -3681,10 +3836,10 @@ var klaviyo_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
3681
3836
|
|
|
3682
3837
|
// lib/connections/scopes.js
|
|
3683
3838
|
var list = (value) => (Array.isArray(value) ? value.flatMap((entry) => String(entry).split(/\s+/)) : String(value ?? "").split(/\s+/)).map((entry) => entry.trim()).filter(Boolean);
|
|
3684
|
-
var missingScopes = ({ granted, required }) => {
|
|
3839
|
+
var missingScopes = ({ granted, required: required2 }) => {
|
|
3685
3840
|
if (granted === null || granted === void 0 || granted === "") return null;
|
|
3686
3841
|
const held = new Set(list(granted));
|
|
3687
|
-
return list(
|
|
3842
|
+
return list(required2).filter((scope) => !held.has(scope));
|
|
3688
3843
|
};
|
|
3689
3844
|
|
|
3690
3845
|
// lib/connections/manifests/klaviyo.js
|
|
@@ -4583,16 +4738,19 @@ var mailchimp_default2 = {
|
|
|
4583
4738
|
"Anyone who has opted out in Drawbridge is synced as unsubscribed rather than omitted, so a person who asked not to be contacted stays suppressed in both systems instead of quietly reappearing.",
|
|
4584
4739
|
"Someone who unsubscribed inside Mailchimp keeps that choice: a resync only sets the status of a subscriber Mailchimp has never seen before."
|
|
4585
4740
|
],
|
|
4586
|
-
//
|
|
4587
|
-
//
|
|
4588
|
-
//
|
|
4589
|
-
// up for — and a sentence here is an ADDITION to those, for a vendor whose
|
|
4590
|
-
// failures need saying differently. This one's do not.
|
|
4741
|
+
// WHAT THE MERCHANT READS WHEN A CONNECT FAILS. Two states, because the
|
|
4742
|
+
// consent screen has two ways to end: they said no, or something between
|
|
4743
|
+
// here and the vendor went wrong.
|
|
4591
4744
|
//
|
|
4592
|
-
//
|
|
4593
|
-
//
|
|
4594
|
-
//
|
|
4595
|
-
errors:
|
|
4745
|
+
// THE VENDOR IS NAMED IN BOTH. The dashboard's generic copy says "the
|
|
4746
|
+
// authorization"; a merchant connecting two accounts in one sitting needs
|
|
4747
|
+
// to know which one this was.
|
|
4748
|
+
errors: {
|
|
4749
|
+
connect: {
|
|
4750
|
+
denied: "The Mailchimp authorization was declined, so nothing was connected.",
|
|
4751
|
+
invalid: "We couldn't complete the Mailchimp connection. Try connecting again."
|
|
4752
|
+
}
|
|
4753
|
+
},
|
|
4596
4754
|
excerpt: "Sync your Drawbridge contacts into a Mailchimp audience.",
|
|
4597
4755
|
// NAMES THE AUDIENCE STEP, because `status` below gates on it: a grant with
|
|
4598
4756
|
// no audience chosen sits at Pending, and a guide that stops at the consent
|
|
@@ -4691,7 +4849,7 @@ var mailchimp_default2 = {
|
|
|
4691
4849
|
if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", request: request2, skipped: true };
|
|
4692
4850
|
const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
|
|
4693
4851
|
if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
|
|
4694
|
-
const
|
|
4852
|
+
const hash2 = subscriberHash(email);
|
|
4695
4853
|
const [firstName, ...restOfName] = String((lead == null ? void 0 : lead.name) || "").trim().split(/\s+/).filter(Boolean);
|
|
4696
4854
|
const lastName = restOfName.join(" ");
|
|
4697
4855
|
const phone = ((_e = lead == null ? void 0 : lead.phone) == null ? void 0 : _e.number) || null;
|
|
@@ -4700,7 +4858,7 @@ var mailchimp_default2 = {
|
|
|
4700
4858
|
...lastName && { LNAME: lastName },
|
|
4701
4859
|
...phone && { PHONE: phone }
|
|
4702
4860
|
};
|
|
4703
|
-
const member = await api3("/lists/" + audience + "/members/" +
|
|
4861
|
+
const member = await api3("/lists/" + audience + "/members/" + hash2, {
|
|
4704
4862
|
dc: settings == null ? void 0 : settings.dc,
|
|
4705
4863
|
fetcher,
|
|
4706
4864
|
method: "PUT",
|
|
@@ -4741,7 +4899,7 @@ var mailchimp_default2 = {
|
|
|
4741
4899
|
status: joined.has(entry.id) ? "active" : "inactive"
|
|
4742
4900
|
}));
|
|
4743
4901
|
if (tags.length > 0) {
|
|
4744
|
-
await api3("/lists/" + audience + "/members/" +
|
|
4902
|
+
await api3("/lists/" + audience + "/members/" + hash2 + "/tags", {
|
|
4745
4903
|
dc: settings == null ? void 0 : settings.dc,
|
|
4746
4904
|
fetcher,
|
|
4747
4905
|
method: "POST",
|
|
@@ -4752,12 +4910,12 @@ var mailchimp_default2 = {
|
|
|
4752
4910
|
}
|
|
4753
4911
|
return {
|
|
4754
4912
|
// Merged into `context` for later steps in this run.
|
|
4755
|
-
context: { mailchimpMemberId: (member == null ? void 0 : member.id) ||
|
|
4913
|
+
context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash2 },
|
|
4756
4914
|
message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
|
|
4757
4915
|
request: request2,
|
|
4758
4916
|
// Recorded on the run for support to read back, not a write
|
|
4759
4917
|
// instruction — the hook has already written what it needed to.
|
|
4760
|
-
response: { mailchimpMemberId: (member == null ? void 0 : member.id) ||
|
|
4918
|
+
response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash2 }
|
|
4761
4919
|
};
|
|
4762
4920
|
}
|
|
4763
4921
|
},
|
|
@@ -5127,167 +5285,2058 @@ var shopify_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill=
|
|
|
5127
5285
|
<path d="M300.325 382.771L368 365.96C368 365.96 338.904 169.185 338.689 167.892C338.473 166.599 337.396 165.737 336.318 165.737C335.24 165.737 316.274 165.306 316.274 165.306C316.274 165.306 304.636 154.099 300.325 149.788V382.771Z" fill="white"/>
|
|
5128
5286
|
</svg>`;
|
|
5129
5287
|
|
|
5130
|
-
// lib/
|
|
5131
|
-
var
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
return local + "@" + domain;
|
|
5144
|
-
};
|
|
5288
|
+
// lib/shopify/admin.js
|
|
5289
|
+
var admin_exports = {};
|
|
5290
|
+
__export(admin_exports, {
|
|
5291
|
+
adminFetch: () => adminFetch,
|
|
5292
|
+
createDiscountCode: () => createDiscountCode,
|
|
5293
|
+
createUsageRecord: () => createUsageRecord,
|
|
5294
|
+
createUsageSubscription: () => createUsageSubscription,
|
|
5295
|
+
getActiveAppSubscriptions: () => getActiveAppSubscriptions,
|
|
5296
|
+
getDiscounts: () => getDiscounts,
|
|
5297
|
+
getOrCreateCustomer: () => getOrCreateCustomer,
|
|
5298
|
+
getProductInventory: () => getProductInventory,
|
|
5299
|
+
sendProductResourceFeedback: () => sendProductResourceFeedback
|
|
5300
|
+
});
|
|
5145
5301
|
|
|
5146
|
-
// lib/
|
|
5147
|
-
var
|
|
5148
|
-
var
|
|
5149
|
-
var
|
|
5150
|
-
var
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5302
|
+
// lib/shopify/constants.js
|
|
5303
|
+
var SHOPIFY_STOREFRONT_API_VERSION = "2025-01";
|
|
5304
|
+
var SHOPIFY_ADMIN_API_VERSION = "2026-04";
|
|
5305
|
+
var SHOPIFY_APP_API_VERSION = "unstable";
|
|
5306
|
+
var SHOPIFY_REQUIRED_SCOPES = [
|
|
5307
|
+
"read_customers",
|
|
5308
|
+
"write_customers",
|
|
5309
|
+
"read_discounts",
|
|
5310
|
+
"write_discounts",
|
|
5311
|
+
"read_orders",
|
|
5312
|
+
"read_products",
|
|
5313
|
+
"read_publications",
|
|
5314
|
+
"write_resource_feedbacks",
|
|
5315
|
+
"unauthenticated_read_checkouts",
|
|
5316
|
+
"unauthenticated_read_product_inventory",
|
|
5317
|
+
"unauthenticated_read_product_listings",
|
|
5318
|
+
"unauthenticated_write_checkouts"
|
|
5319
|
+
];
|
|
5320
|
+
var REFRESH_TOKEN_LIFETIME_MS = 90 * 24 * 60 * 60 * 1e3;
|
|
5321
|
+
var ACCESS_TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
5322
|
+
var PARTNER_API_VERSION = process.env.SHOPIFY_PARTNER_API_VERSION || "2026-07";
|
|
5165
5323
|
|
|
5166
|
-
// lib/
|
|
5167
|
-
var
|
|
5168
|
-
var
|
|
5169
|
-
|
|
5170
|
-
const
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5324
|
+
// lib/shopify/admin.js
|
|
5325
|
+
var adminUrl = (domain) => `https://${domain}/admin/api/${SHOPIFY_ADMIN_API_VERSION}`;
|
|
5326
|
+
var adminFetch = async ({ adminAccessToken, domain, fetcher, query, variables }) => {
|
|
5327
|
+
var _a;
|
|
5328
|
+
const { data: data2, errors } = await request({
|
|
5329
|
+
fetcher,
|
|
5330
|
+
method: "POST",
|
|
5331
|
+
url: adminUrl(domain) + "/graphql.json",
|
|
5332
|
+
headers: {
|
|
5333
|
+
"X-Shopify-Access-Token": adminAccessToken
|
|
5334
|
+
},
|
|
5335
|
+
body: {
|
|
5336
|
+
query,
|
|
5337
|
+
...variables && { variables }
|
|
5178
5338
|
}
|
|
5179
|
-
);
|
|
5339
|
+
});
|
|
5340
|
+
if (errors) {
|
|
5341
|
+
throw new Error("Shopify GraphQL error: " + ((_a = errors[0]) == null ? void 0 : _a.message));
|
|
5342
|
+
}
|
|
5343
|
+
;
|
|
5344
|
+
return data2;
|
|
5180
5345
|
};
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
}
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5346
|
+
var DISCOUNT_FIELDS = `
|
|
5347
|
+
title
|
|
5348
|
+
status
|
|
5349
|
+
startsAt
|
|
5350
|
+
endsAt
|
|
5351
|
+
usageLimit
|
|
5352
|
+
asyncUsageCount
|
|
5353
|
+
appliesOncePerCustomer
|
|
5354
|
+
context { __typename }
|
|
5355
|
+
`;
|
|
5356
|
+
var discountsQuery = (search, cursor, limit) => `{
|
|
5357
|
+
codeDiscountNodes(first: ${limit}${search ? `, query: "title:*${search}*"` : ""}${cursor ? `, after: "${cursor}"` : ""}) {
|
|
5358
|
+
edges {
|
|
5359
|
+
node {
|
|
5360
|
+
id
|
|
5361
|
+
codeDiscount {
|
|
5362
|
+
... on DiscountCodeBasic { ${DISCOUNT_FIELDS} }
|
|
5363
|
+
... on DiscountCodeBxgy { ${DISCOUNT_FIELDS} }
|
|
5364
|
+
... on DiscountCodeFreeShipping { ${DISCOUNT_FIELDS} }
|
|
5365
|
+
... on DiscountCodeApp { ${DISCOUNT_FIELDS} }
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5368
|
+
}
|
|
5369
|
+
pageInfo {
|
|
5370
|
+
endCursor
|
|
5371
|
+
hasNextPage
|
|
5372
|
+
hasPreviousPage
|
|
5373
|
+
startCursor
|
|
5374
|
+
}
|
|
5375
|
+
}
|
|
5376
|
+
}`;
|
|
5377
|
+
var getDiscounts = async ({ fetcher, adminAccessToken, domain, search, cursor, limit }) => {
|
|
5378
|
+
const data2 = await adminFetch({
|
|
5379
|
+
fetcher,
|
|
5380
|
+
adminAccessToken,
|
|
5381
|
+
domain,
|
|
5382
|
+
query: discountsQuery(search, cursor, limit)
|
|
5383
|
+
});
|
|
5384
|
+
return data2 == null ? void 0 : data2.codeDiscountNodes;
|
|
5385
|
+
};
|
|
5386
|
+
var discountGid = (id) => String(id).startsWith("gid://") ? String(id) : "gid://shopify/DiscountCodeNode/" + id;
|
|
5387
|
+
var createDiscountCode = async ({ fetcher, adminAccessToken, domain, discountId, code: code2 }) => {
|
|
5388
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
5389
|
+
const added = await adminFetch({
|
|
5390
|
+
fetcher,
|
|
5391
|
+
adminAccessToken,
|
|
5392
|
+
domain,
|
|
5393
|
+
query: `
|
|
5394
|
+
mutation DiscountRedeemCodeBulkAdd($discountId: ID!, $codes: [DiscountRedeemCodeInput!]!) {
|
|
5395
|
+
discountRedeemCodeBulkAdd(discountId: $discountId, codes: $codes) {
|
|
5396
|
+
bulkCreation {
|
|
5397
|
+
id
|
|
5398
|
+
}
|
|
5399
|
+
userErrors {
|
|
5400
|
+
field
|
|
5401
|
+
message
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
`,
|
|
5406
|
+
variables: {
|
|
5407
|
+
codes: [{ code: code2 }],
|
|
5408
|
+
discountId: discountGid(discountId)
|
|
5409
|
+
}
|
|
5410
|
+
});
|
|
5411
|
+
const addErrors = (_a = added == null ? void 0 : added.discountRedeemCodeBulkAdd) == null ? void 0 : _a.userErrors;
|
|
5412
|
+
if (addErrors == null ? void 0 : addErrors.length) {
|
|
5413
|
+
throw new Error("Shopify discount code create failed: " + addErrors[0].message);
|
|
5223
5414
|
}
|
|
5224
5415
|
;
|
|
5225
|
-
|
|
5226
|
-
|
|
5416
|
+
const bulkCreationId = (_c = (_b = added == null ? void 0 : added.discountRedeemCodeBulkAdd) == null ? void 0 : _b.bulkCreation) == null ? void 0 : _c.id;
|
|
5417
|
+
if (!bulkCreationId) {
|
|
5418
|
+
throw new Error("Shopify discount code create failed: no bulk creation id returned");
|
|
5227
5419
|
}
|
|
5228
5420
|
;
|
|
5229
|
-
|
|
5421
|
+
for (let attempt = 0; attempt < 15; attempt++) {
|
|
5422
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
5423
|
+
const polled = await adminFetch({
|
|
5424
|
+
fetcher,
|
|
5425
|
+
adminAccessToken,
|
|
5426
|
+
domain,
|
|
5427
|
+
query: `
|
|
5428
|
+
query DiscountRedeemCodeBulkCreationPoll($id: ID!) {
|
|
5429
|
+
discountRedeemCodeBulkCreation(id: $id) {
|
|
5430
|
+
done
|
|
5431
|
+
codes(first: 1) {
|
|
5432
|
+
nodes {
|
|
5433
|
+
code
|
|
5434
|
+
discountRedeemCode {
|
|
5435
|
+
id
|
|
5436
|
+
}
|
|
5437
|
+
errors {
|
|
5438
|
+
message
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
`,
|
|
5445
|
+
variables: {
|
|
5446
|
+
id: bulkCreationId
|
|
5447
|
+
}
|
|
5448
|
+
});
|
|
5449
|
+
const creation = polled == null ? void 0 : polled.discountRedeemCodeBulkCreation;
|
|
5450
|
+
if (!(creation == null ? void 0 : creation.done)) continue;
|
|
5451
|
+
const node = (_e = (_d = creation.codes) == null ? void 0 : _d.nodes) == null ? void 0 : _e[0];
|
|
5452
|
+
if (!((_f = node == null ? void 0 : node.discountRedeemCode) == null ? void 0 : _f.id)) {
|
|
5453
|
+
throw new Error("Shopify discount code create failed: " + (((_h = (_g = node == null ? void 0 : node.errors) == null ? void 0 : _g[0]) == null ? void 0 : _h.message) || "code was not created"));
|
|
5454
|
+
}
|
|
5455
|
+
;
|
|
5456
|
+
return {
|
|
5457
|
+
code: node.code,
|
|
5458
|
+
// Numeric tail keeps the stored shopifyDiscountId in the shape the
|
|
5459
|
+
// legacy REST endpoint returned — it feeds a merchant-visible
|
|
5460
|
+
// workflow template variable, so no gid:// leakage.
|
|
5461
|
+
id: node.discountRedeemCode.id.split("/").pop()
|
|
5462
|
+
};
|
|
5463
|
+
}
|
|
5464
|
+
;
|
|
5465
|
+
throw new Error("Shopify discount code create timed out waiting for bulk creation " + bulkCreationId);
|
|
5230
5466
|
};
|
|
5231
|
-
var
|
|
5232
|
-
|
|
5233
|
-
|
|
5467
|
+
var getOrCreateCustomer = async ({ fetcher, adminAccessToken, domain, email, firstName, lastName, source }) => {
|
|
5468
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
5469
|
+
const lookupQuery = `
|
|
5470
|
+
query customerByEmail( $query : String! ) {
|
|
5471
|
+
customers( first : 1, query : $query ) {
|
|
5472
|
+
edges {
|
|
5473
|
+
node {
|
|
5474
|
+
id
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
`;
|
|
5480
|
+
const data2 = await adminFetch({
|
|
5481
|
+
fetcher,
|
|
5482
|
+
adminAccessToken,
|
|
5483
|
+
domain,
|
|
5484
|
+
query: lookupQuery,
|
|
5485
|
+
variables: { query: "email:" + email }
|
|
5486
|
+
});
|
|
5487
|
+
const existing = (_c = (_b = (_a = data2 == null ? void 0 : data2.customers) == null ? void 0 : _a.edges) == null ? void 0 : _b[0]) == null ? void 0 : _c.node;
|
|
5488
|
+
if (existing) return existing;
|
|
5489
|
+
const input = { email };
|
|
5490
|
+
if (firstName) input.firstName = firstName;
|
|
5491
|
+
if (lastName) input.lastName = lastName;
|
|
5492
|
+
if (source) {
|
|
5493
|
+
input.metafields = [{
|
|
5494
|
+
namespace: "drwbrdg",
|
|
5495
|
+
key: "source",
|
|
5496
|
+
type: "single_line_text_field",
|
|
5497
|
+
value: source
|
|
5498
|
+
}];
|
|
5234
5499
|
}
|
|
5235
5500
|
;
|
|
5236
|
-
|
|
5237
|
-
|
|
5501
|
+
const result = await adminFetch({
|
|
5502
|
+
fetcher,
|
|
5503
|
+
adminAccessToken,
|
|
5504
|
+
domain,
|
|
5505
|
+
query: `
|
|
5506
|
+
mutation customerCreate( $input : CustomerInput! ) {
|
|
5507
|
+
customerCreate( input : $input ) {
|
|
5508
|
+
customer {
|
|
5509
|
+
id
|
|
5510
|
+
}
|
|
5511
|
+
userErrors {
|
|
5512
|
+
field
|
|
5513
|
+
message
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
`,
|
|
5518
|
+
variables: { input }
|
|
5519
|
+
});
|
|
5520
|
+
const userErrors = (_d = result == null ? void 0 : result.customerCreate) == null ? void 0 : _d.userErrors;
|
|
5521
|
+
if (userErrors == null ? void 0 : userErrors.length) {
|
|
5522
|
+
const emailTaken = userErrors.some((e) => {
|
|
5523
|
+
var _a2;
|
|
5524
|
+
return (_a2 = e.message) == null ? void 0 : _a2.includes("Email has already been taken");
|
|
5525
|
+
});
|
|
5526
|
+
if (emailTaken) {
|
|
5527
|
+
const retry = await adminFetch({
|
|
5528
|
+
fetcher,
|
|
5529
|
+
adminAccessToken,
|
|
5530
|
+
domain,
|
|
5531
|
+
query: lookupQuery,
|
|
5532
|
+
variables: { query: "email:" + email }
|
|
5533
|
+
});
|
|
5534
|
+
const found = (_g = (_f = (_e = retry == null ? void 0 : retry.customers) == null ? void 0 : _e.edges) == null ? void 0 : _f[0]) == null ? void 0 : _g.node;
|
|
5535
|
+
if (found) return found;
|
|
5536
|
+
}
|
|
5537
|
+
;
|
|
5538
|
+
throw new Error("Shopify customer create failed: " + userErrors[0].message);
|
|
5539
|
+
}
|
|
5540
|
+
;
|
|
5541
|
+
return (_h = result == null ? void 0 : result.customerCreate) == null ? void 0 : _h.customer;
|
|
5238
5542
|
};
|
|
5239
|
-
var
|
|
5240
|
-
var
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5543
|
+
var getActiveAppSubscriptions = async ({ fetcher, adminAccessToken, domain }) => {
|
|
5544
|
+
var _a, _b;
|
|
5545
|
+
const data2 = await adminFetch({
|
|
5546
|
+
fetcher,
|
|
5547
|
+
adminAccessToken,
|
|
5548
|
+
domain,
|
|
5549
|
+
query: `{
|
|
5550
|
+
currentAppInstallation {
|
|
5551
|
+
activeSubscriptions {
|
|
5552
|
+
id
|
|
5553
|
+
name
|
|
5554
|
+
status
|
|
5555
|
+
test
|
|
5556
|
+
lineItems {
|
|
5557
|
+
id
|
|
5558
|
+
plan {
|
|
5559
|
+
pricingDetails {
|
|
5560
|
+
__typename
|
|
5561
|
+
}
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
}
|
|
5566
|
+
shop {
|
|
5567
|
+
myshopifyDomain
|
|
5568
|
+
}
|
|
5569
|
+
}`
|
|
5570
|
+
});
|
|
5571
|
+
return {
|
|
5572
|
+
shopDomain: ((_a = data2 == null ? void 0 : data2.shop) == null ? void 0 : _a.myshopifyDomain) || null,
|
|
5573
|
+
subscriptions: (((_b = data2 == null ? void 0 : data2.currentAppInstallation) == null ? void 0 : _b.activeSubscriptions) || []).map((subscription) => {
|
|
5574
|
+
var _a2;
|
|
5575
|
+
return {
|
|
5576
|
+
...subscription,
|
|
5577
|
+
metered: ((subscription == null ? void 0 : subscription.lineItems) || []).some(
|
|
5578
|
+
(item) => {
|
|
5579
|
+
var _a3, _b2;
|
|
5580
|
+
return ((_b2 = (_a3 = item == null ? void 0 : item.plan) == null ? void 0 : _a3.pricingDetails) == null ? void 0 : _b2.__typename) === "AppUsagePricing";
|
|
5581
|
+
}
|
|
5582
|
+
),
|
|
5583
|
+
// The usage line's own id — the address appUsageRecordCreate charges
|
|
5584
|
+
// against. Null when the approval carries no usage component.
|
|
5585
|
+
usageLineItemId: ((_a2 = ((subscription == null ? void 0 : subscription.lineItems) || []).find(
|
|
5586
|
+
(item) => {
|
|
5587
|
+
var _a3, _b2;
|
|
5588
|
+
return ((_b2 = (_a3 = item == null ? void 0 : item.plan) == null ? void 0 : _a3.pricingDetails) == null ? void 0 : _b2.__typename) === "AppUsagePricing";
|
|
5589
|
+
}
|
|
5590
|
+
)) == null ? void 0 : _a2.id) || null
|
|
5591
|
+
};
|
|
5592
|
+
})
|
|
5593
|
+
};
|
|
5594
|
+
};
|
|
5595
|
+
var createUsageSubscription = async ({ fetcher, adminAccessToken, cappedAmount, currencyCode = "USD", domain, name, recurringPrice = 0, returnUrl, terms, test = false }) => {
|
|
5596
|
+
if (!name || !returnUrl || !terms || !(Number(cappedAmount) > 0)) {
|
|
5597
|
+
throw new Error("createUsageSubscription requires name, returnUrl, terms, and a positive cappedAmount");
|
|
5598
|
+
}
|
|
5599
|
+
;
|
|
5600
|
+
const data2 = await adminFetch({
|
|
5601
|
+
fetcher,
|
|
5602
|
+
adminAccessToken,
|
|
5603
|
+
domain,
|
|
5604
|
+
query: `mutation createUsageSubscription( $lineItems : [AppSubscriptionLineItemInput!]!, $name : String!, $returnUrl : URL!, $test : Boolean ) {
|
|
5605
|
+
appSubscriptionCreate( lineItems : $lineItems, name : $name, returnUrl : $returnUrl, test : $test ) {
|
|
5606
|
+
appSubscription { id name status test lineItems { id plan { pricingDetails { __typename } } } }
|
|
5607
|
+
confirmationUrl
|
|
5608
|
+
userErrors { field message }
|
|
5609
|
+
}
|
|
5610
|
+
}`,
|
|
5611
|
+
variables: {
|
|
5612
|
+
lineItems: [
|
|
5613
|
+
{
|
|
5614
|
+
plan: {
|
|
5615
|
+
appRecurringPricingDetails: {
|
|
5616
|
+
interval: "EVERY_30_DAYS",
|
|
5617
|
+
price: { amount: Number(recurringPrice), currencyCode }
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
},
|
|
5621
|
+
{
|
|
5622
|
+
plan: {
|
|
5623
|
+
appUsagePricingDetails: {
|
|
5624
|
+
cappedAmount: { amount: Number(cappedAmount), currencyCode },
|
|
5625
|
+
terms
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5629
|
+
],
|
|
5630
|
+
name,
|
|
5631
|
+
returnUrl,
|
|
5632
|
+
test
|
|
5633
|
+
}
|
|
5634
|
+
});
|
|
5635
|
+
const payload = data2 == null ? void 0 : data2.appSubscriptionCreate;
|
|
5636
|
+
const userErrors = (payload == null ? void 0 : payload.userErrors) || [];
|
|
5637
|
+
if (userErrors.length) {
|
|
5638
|
+
throw new Error("Shopify subscription create failed: " + userErrors.map((entry) => entry.message).join("; "));
|
|
5639
|
+
}
|
|
5640
|
+
;
|
|
5641
|
+
if (!(payload == null ? void 0 : payload.confirmationUrl)) {
|
|
5642
|
+
throw new Error("Shopify subscription create failed: no confirmation url returned");
|
|
5256
5643
|
}
|
|
5644
|
+
;
|
|
5645
|
+
return {
|
|
5646
|
+
confirmationUrl: payload.confirmationUrl,
|
|
5647
|
+
subscription: payload.appSubscription
|
|
5648
|
+
};
|
|
5257
5649
|
};
|
|
5258
|
-
var
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
|
|
5263
|
-
|
|
5264
|
-
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5650
|
+
var createUsageRecord = async ({ fetcher, adminAccessToken, amount, currencyCode = "USD", description, domain, idempotencyKey, subscriptionLineItemId }) => {
|
|
5651
|
+
var _a;
|
|
5652
|
+
if (!subscriptionLineItemId || !description || !(Number(amount) > 0)) {
|
|
5653
|
+
throw new Error("createUsageRecord requires subscriptionLineItemId, description, and a positive amount");
|
|
5654
|
+
}
|
|
5655
|
+
;
|
|
5656
|
+
const data2 = await adminFetch({
|
|
5657
|
+
fetcher,
|
|
5658
|
+
adminAccessToken,
|
|
5659
|
+
domain,
|
|
5660
|
+
query: `mutation createUsageRecord( $description : String!, $idempotencyKey : String, $price : MoneyInput!, $subscriptionLineItemId : ID! ) {
|
|
5661
|
+
appUsageRecordCreate( description : $description, idempotencyKey : $idempotencyKey, price : $price, subscriptionLineItemId : $subscriptionLineItemId ) {
|
|
5662
|
+
appUsageRecord { id }
|
|
5663
|
+
userErrors { field message }
|
|
5664
|
+
}
|
|
5665
|
+
}`,
|
|
5666
|
+
variables: {
|
|
5667
|
+
description,
|
|
5668
|
+
idempotencyKey: idempotencyKey ? String(idempotencyKey) : null,
|
|
5669
|
+
price: { amount: Number(amount), currencyCode },
|
|
5670
|
+
subscriptionLineItemId
|
|
5671
|
+
}
|
|
5672
|
+
});
|
|
5673
|
+
const payload = data2 == null ? void 0 : data2.appUsageRecordCreate;
|
|
5674
|
+
const userErrors = (payload == null ? void 0 : payload.userErrors) || [];
|
|
5675
|
+
if (userErrors.length || !((_a = payload == null ? void 0 : payload.appUsageRecord) == null ? void 0 : _a.id)) {
|
|
5676
|
+
throw new Error("Shopify usage record failed: " + (userErrors.map((entry) => entry.message).join("; ") || "no record returned"));
|
|
5677
|
+
}
|
|
5678
|
+
;
|
|
5679
|
+
return { id: payload.appUsageRecord.id };
|
|
5680
|
+
};
|
|
5681
|
+
var getProductInventory = async ({ fetcher, adminAccessToken, domain, productId }) => {
|
|
5682
|
+
var _a, _b;
|
|
5683
|
+
const data2 = await adminFetch({
|
|
5684
|
+
fetcher,
|
|
5685
|
+
adminAccessToken,
|
|
5686
|
+
domain,
|
|
5687
|
+
query: `{
|
|
5688
|
+
product( id : "${productId}" ) {
|
|
5689
|
+
variants( first : 100 ) {
|
|
5690
|
+
edges {
|
|
5691
|
+
node {
|
|
5692
|
+
id
|
|
5693
|
+
inventoryQuantity
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
}
|
|
5698
|
+
}`
|
|
5699
|
+
});
|
|
5700
|
+
const edges = ((_b = (_a = data2 == null ? void 0 : data2.product) == null ? void 0 : _a.variants) == null ? void 0 : _b.edges) || [];
|
|
5701
|
+
return edges.reduce((accumulator, edge) => {
|
|
5702
|
+
var _a2, _b2;
|
|
5703
|
+
const id = (_a2 = edge == null ? void 0 : edge.node) == null ? void 0 : _a2.id;
|
|
5704
|
+
const quantity = (_b2 = edge == null ? void 0 : edge.node) == null ? void 0 : _b2.inventoryQuantity;
|
|
5705
|
+
if (id) {
|
|
5706
|
+
accumulator[id] = Number.isFinite(quantity) ? quantity : 0;
|
|
5707
|
+
}
|
|
5708
|
+
;
|
|
5709
|
+
return accumulator;
|
|
5710
|
+
}, {});
|
|
5711
|
+
};
|
|
5712
|
+
var getProductUpdatedAt = async ({ adminAccessToken, domain, fetcher, productId }) => {
|
|
5713
|
+
var _a;
|
|
5714
|
+
const data2 = await adminFetch({
|
|
5715
|
+
fetcher,
|
|
5716
|
+
adminAccessToken,
|
|
5717
|
+
domain,
|
|
5718
|
+
query: `
|
|
5719
|
+
query ProductUpdatedAt($id: ID!) {
|
|
5720
|
+
product(id: $id) {
|
|
5721
|
+
updatedAt
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
`,
|
|
5725
|
+
variables: { id: productId }
|
|
5726
|
+
});
|
|
5727
|
+
return ((_a = data2 == null ? void 0 : data2.product) == null ? void 0 : _a.updatedAt) || null;
|
|
5728
|
+
};
|
|
5729
|
+
var NOT_ON_CHANNEL = "not available to the channel";
|
|
5730
|
+
var OUTDATED_FEEDBACK = "later version";
|
|
5731
|
+
var sendProductResourceFeedback = async ({ fetcher, adminAccessToken, domain, productId, state, messages = [] }) => {
|
|
5732
|
+
var _a, _b, _c;
|
|
5733
|
+
const productUpdatedAt = await getProductUpdatedAt({ adminAccessToken, domain, fetcher, productId });
|
|
5734
|
+
if (!productUpdatedAt) return null;
|
|
5735
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
5736
|
+
const submit = async (version) => {
|
|
5737
|
+
const data2 = await adminFetch({
|
|
5738
|
+
fetcher,
|
|
5739
|
+
adminAccessToken,
|
|
5740
|
+
domain,
|
|
5741
|
+
query: `
|
|
5742
|
+
mutation ProductFeedback($feedbackInput: [ProductResourceFeedbackInput!]!) {
|
|
5743
|
+
bulkProductResourceFeedbackCreate(feedbackInput: $feedbackInput) {
|
|
5744
|
+
feedback {
|
|
5745
|
+
productId
|
|
5746
|
+
state
|
|
5747
|
+
}
|
|
5748
|
+
userErrors {
|
|
5749
|
+
field
|
|
5750
|
+
message
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
}
|
|
5754
|
+
`,
|
|
5755
|
+
variables: {
|
|
5756
|
+
feedbackInput: [
|
|
5757
|
+
{
|
|
5758
|
+
productId,
|
|
5759
|
+
state,
|
|
5760
|
+
feedbackGeneratedAt: now,
|
|
5761
|
+
productUpdatedAt: version,
|
|
5762
|
+
messages
|
|
5763
|
+
}
|
|
5764
|
+
]
|
|
5765
|
+
}
|
|
5766
|
+
});
|
|
5767
|
+
return data2 == null ? void 0 : data2.bulkProductResourceFeedbackCreate;
|
|
5768
|
+
};
|
|
5769
|
+
let result = await submit(productUpdatedAt);
|
|
5770
|
+
if ((((_b = (_a = result == null ? void 0 : result.userErrors) == null ? void 0 : _a[0]) == null ? void 0 : _b.message) || "").toLowerCase().includes(OUTDATED_FEEDBACK)) {
|
|
5771
|
+
result = await submit(now);
|
|
5772
|
+
}
|
|
5773
|
+
;
|
|
5774
|
+
const feedbackErrors = result == null ? void 0 : result.userErrors;
|
|
5775
|
+
if (feedbackErrors == null ? void 0 : feedbackErrors.length) {
|
|
5776
|
+
const message = feedbackErrors[0].message || "";
|
|
5777
|
+
if (message.toLowerCase().includes(NOT_ON_CHANNEL)) return null;
|
|
5778
|
+
throw new Error("Shopify product feedback failed: " + message);
|
|
5779
|
+
}
|
|
5780
|
+
;
|
|
5781
|
+
return ((_c = result == null ? void 0 : result.feedback) == null ? void 0 : _c[0]) || null;
|
|
5782
|
+
};
|
|
5783
|
+
|
|
5784
|
+
// lib/shopify/billing.js
|
|
5785
|
+
var billing_exports = {};
|
|
5786
|
+
__export(billing_exports, {
|
|
5787
|
+
sendAppEvent: () => sendAppEvent
|
|
5788
|
+
});
|
|
5789
|
+
var APP_API = "https://api.shopify.com";
|
|
5790
|
+
var APP_API_VERSION = process.env.SHOPIFY_APP_API_VERSION || SHOPIFY_APP_API_VERSION;
|
|
5791
|
+
var cachedTokens = {};
|
|
5792
|
+
var fetchAppToken = async ({ clientId, clientSecret, fetcher }) => {
|
|
5793
|
+
const data2 = await request({
|
|
5794
|
+
fetcher,
|
|
5795
|
+
method: "POST",
|
|
5796
|
+
url: APP_API + "/auth/access_token",
|
|
5797
|
+
body: {
|
|
5798
|
+
client_id: clientId,
|
|
5799
|
+
client_secret: clientSecret,
|
|
5800
|
+
grant_type: "client_credentials"
|
|
5801
|
+
}
|
|
5802
|
+
});
|
|
5803
|
+
if (!(data2 == null ? void 0 : data2.access_token)) {
|
|
5804
|
+
throw new Error("Shopify app token request failed: no access token returned");
|
|
5805
|
+
}
|
|
5806
|
+
;
|
|
5807
|
+
const ttl = data2.expires_in ? data2.expires_in * 1e3 : 60 * 60 * 1e3;
|
|
5808
|
+
cachedTokens[clientId] = {
|
|
5809
|
+
expiresAt: Date.now() + ttl - 60 * 1e3,
|
|
5810
|
+
token: data2.access_token
|
|
5811
|
+
};
|
|
5812
|
+
return data2.access_token;
|
|
5813
|
+
};
|
|
5814
|
+
var getAppToken = async ({ clientId, clientSecret, fetcher }) => {
|
|
5815
|
+
const cached = cachedTokens[clientId];
|
|
5816
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
5817
|
+
return cached.token;
|
|
5818
|
+
}
|
|
5819
|
+
;
|
|
5820
|
+
return fetchAppToken({ clientId, clientSecret, fetcher });
|
|
5821
|
+
};
|
|
5822
|
+
var toShopGid = (shopId) => String(shopId).startsWith("gid://") ? String(shopId) : "gid://shopify/Shop/" + shopId;
|
|
5823
|
+
var sendAppEvent = async ({ fetcher, clientId, clientSecret, eventHandle, idempotencyKey, reference: reference2, revision, shopId, timestamp, value }) => {
|
|
5824
|
+
if (!shopId || !eventHandle || !(Number(value) > 0)) {
|
|
5825
|
+
throw new Error("sendAppEvent requires shopId, eventHandle, and a positive value");
|
|
5826
|
+
}
|
|
5827
|
+
;
|
|
5828
|
+
if (!clientId || !clientSecret) {
|
|
5829
|
+
throw new Error("sendAppEvent requires clientId and clientSecret");
|
|
5830
|
+
}
|
|
5831
|
+
;
|
|
5832
|
+
const suffix = revision ? ".r" + revision : "";
|
|
5833
|
+
const key = idempotencyKey && String(idempotencyKey).slice(0, Math.max(0, 64 - suffix.length)) + suffix;
|
|
5834
|
+
const body = {
|
|
5835
|
+
attributes: {
|
|
5836
|
+
value: Number(value),
|
|
5837
|
+
...reference2 && {
|
|
5838
|
+
reference: String(reference2).slice(0, 128)
|
|
5839
|
+
}
|
|
5840
|
+
},
|
|
5841
|
+
event_handle: eventHandle,
|
|
5842
|
+
shop_id: toShopGid(shopId),
|
|
5843
|
+
timestamp: timestamp || (/* @__PURE__ */ new Date()).toISOString(),
|
|
5844
|
+
...key && {
|
|
5845
|
+
idempotency_key: key
|
|
5846
|
+
}
|
|
5847
|
+
};
|
|
5848
|
+
const send2 = async (token) => request({
|
|
5849
|
+
method: "POST",
|
|
5850
|
+
url: APP_API + "/app/" + APP_API_VERSION + "/events",
|
|
5851
|
+
headers: {
|
|
5852
|
+
"Authorization": "Bearer " + token
|
|
5853
|
+
},
|
|
5854
|
+
body
|
|
5855
|
+
});
|
|
5856
|
+
try {
|
|
5857
|
+
return await send2(await getAppToken({ clientId, clientSecret, fetcher }));
|
|
5858
|
+
} catch (error) {
|
|
5859
|
+
if ((error == null ? void 0 : error.status) === 401) {
|
|
5860
|
+
delete cachedTokens[clientId];
|
|
5861
|
+
return send2(await fetchAppToken({ clientId, clientSecret, fetcher }));
|
|
5862
|
+
}
|
|
5863
|
+
;
|
|
5864
|
+
throw error;
|
|
5865
|
+
}
|
|
5866
|
+
};
|
|
5867
|
+
|
|
5868
|
+
// lib/shopify/oauth.js
|
|
5869
|
+
var oauth_exports = {};
|
|
5870
|
+
__export(oauth_exports, {
|
|
5871
|
+
SHOPIFY_REQUIRED_SCOPES: () => SHOPIFY_REQUIRED_SCOPES,
|
|
5872
|
+
createStorefrontToken: () => createStorefrontToken,
|
|
5873
|
+
getAdminToken: () => getAdminToken,
|
|
5874
|
+
getShop: () => getShop,
|
|
5875
|
+
missingScopes: () => missingScopes2,
|
|
5876
|
+
ping: () => ping,
|
|
5877
|
+
refreshAdminToken: () => refreshAdminToken,
|
|
5878
|
+
resolveConnectionSettings: () => resolveConnectionSettings
|
|
5879
|
+
});
|
|
5880
|
+
var required = (name, value) => {
|
|
5881
|
+
if (!value) {
|
|
5882
|
+
throw new Error("Shopify " + name + " is required");
|
|
5883
|
+
}
|
|
5884
|
+
;
|
|
5885
|
+
return value;
|
|
5886
|
+
};
|
|
5887
|
+
var missingScopes2 = (granted) => {
|
|
5888
|
+
const grantedSet = new Set(
|
|
5889
|
+
(Array.isArray(granted) ? granted : String(granted || "").split(",")).map((scope) => scope.trim()).filter(Boolean)
|
|
5890
|
+
);
|
|
5891
|
+
const satisfied = (scope) => grantedSet.has(scope) || grantedSet.has(scope.replace(/(^|_)read_/, "$1write_"));
|
|
5892
|
+
return SHOPIFY_REQUIRED_SCOPES.filter((scope) => !satisfied(scope));
|
|
5893
|
+
};
|
|
5894
|
+
var ping = async ({ fetcher, adminAccessToken, domain }) => {
|
|
5895
|
+
var _a;
|
|
5896
|
+
const response = await fetch(
|
|
5897
|
+
`https://${domain}/admin/api/${SHOPIFY_ADMIN_API_VERSION}/graphql.json`,
|
|
5898
|
+
{
|
|
5899
|
+
body: JSON.stringify({ query: "{ shop { name } }" }),
|
|
5900
|
+
headers: {
|
|
5901
|
+
"Content-Type": "application/json",
|
|
5902
|
+
"X-Shopify-Access-Token": adminAccessToken
|
|
5903
|
+
},
|
|
5904
|
+
method: "POST"
|
|
5905
|
+
}
|
|
5906
|
+
);
|
|
5907
|
+
if (!response.ok) {
|
|
5908
|
+
const text = await response.text().catch(() => "");
|
|
5909
|
+
const error = new Error(response.status + ": " + text);
|
|
5910
|
+
error.status = response.status;
|
|
5911
|
+
throw error;
|
|
5912
|
+
}
|
|
5913
|
+
;
|
|
5914
|
+
const { errors } = await response.json().catch(() => ({}));
|
|
5915
|
+
if (errors == null ? void 0 : errors.length) {
|
|
5916
|
+
throw new Error("GraphQL: " + (((_a = errors[0]) == null ? void 0 : _a.message) || "unknown error"));
|
|
5917
|
+
}
|
|
5918
|
+
;
|
|
5919
|
+
};
|
|
5920
|
+
var OAUTH_ERROR_TITLE_PATTERN = /Oauth error (\w+)/i;
|
|
5921
|
+
var parseOAuthErrorCode = (text) => {
|
|
5922
|
+
var _a;
|
|
5923
|
+
try {
|
|
5924
|
+
const parsed = JSON.parse(text);
|
|
5925
|
+
if (parsed == null ? void 0 : parsed.error) return parsed.error;
|
|
5926
|
+
} catch {
|
|
5927
|
+
}
|
|
5928
|
+
;
|
|
5929
|
+
return ((_a = text.match(OAUTH_ERROR_TITLE_PATTERN)) == null ? void 0 : _a[1]) || null;
|
|
5930
|
+
};
|
|
5931
|
+
var shopifyOAuthFetch = async (url, body) => {
|
|
5932
|
+
const response = await fetch(url, {
|
|
5933
|
+
method: "POST",
|
|
5934
|
+
headers: {
|
|
5935
|
+
"Content-Type": "application/json"
|
|
5936
|
+
},
|
|
5937
|
+
body: JSON.stringify(body)
|
|
5938
|
+
});
|
|
5939
|
+
if (!response.ok) {
|
|
5940
|
+
const text = await response.text().catch(() => "");
|
|
5941
|
+
const error = new Error(response.status + ": " + text);
|
|
5942
|
+
error.status = response.status;
|
|
5943
|
+
error.code = parseOAuthErrorCode(text);
|
|
5944
|
+
throw error;
|
|
5945
|
+
}
|
|
5946
|
+
;
|
|
5947
|
+
return response.json();
|
|
5948
|
+
};
|
|
5949
|
+
var getShop = async ({ fetcher, adminAccessToken, shop }) => {
|
|
5950
|
+
const data2 = await adminFetch({
|
|
5951
|
+
fetcher,
|
|
5952
|
+
adminAccessToken,
|
|
5953
|
+
domain: shop,
|
|
5954
|
+
query: `{
|
|
5955
|
+
shop {
|
|
5956
|
+
currencyCode
|
|
5957
|
+
id
|
|
5958
|
+
myshopifyDomain
|
|
5959
|
+
name
|
|
5960
|
+
}
|
|
5961
|
+
}`
|
|
5962
|
+
});
|
|
5963
|
+
const shopData = data2 == null ? void 0 : data2.shop;
|
|
5964
|
+
if (!(shopData == null ? void 0 : shopData.id)) {
|
|
5965
|
+
throw new Error("Shopify shop lookup failed: no shop returned");
|
|
5966
|
+
}
|
|
5967
|
+
;
|
|
5968
|
+
return {
|
|
5969
|
+
currency: shopData.currencyCode || null,
|
|
5970
|
+
id: shopData.id.split("/").pop(),
|
|
5971
|
+
myshopifyDomain: shopData.myshopifyDomain,
|
|
5972
|
+
name: shopData.name
|
|
5973
|
+
};
|
|
5974
|
+
};
|
|
5975
|
+
var createStorefrontToken = async ({ fetcher, adminAccessToken, shop }) => {
|
|
5976
|
+
var _a, _b, _c, _d, _e;
|
|
5977
|
+
try {
|
|
5978
|
+
const listed = await adminFetch({
|
|
5979
|
+
fetcher,
|
|
5980
|
+
adminAccessToken,
|
|
5981
|
+
domain: shop,
|
|
5982
|
+
query: `{
|
|
5983
|
+
shop {
|
|
5984
|
+
storefrontAccessTokens(first: 100) {
|
|
5985
|
+
edges {
|
|
5986
|
+
node {
|
|
5987
|
+
id
|
|
5988
|
+
title
|
|
5989
|
+
}
|
|
5990
|
+
}
|
|
5991
|
+
}
|
|
5992
|
+
}
|
|
5993
|
+
}`
|
|
5994
|
+
});
|
|
5995
|
+
const existing = (((_b = (_a = listed == null ? void 0 : listed.shop) == null ? void 0 : _a.storefrontAccessTokens) == null ? void 0 : _b.edges) || []).map((edge) => edge == null ? void 0 : edge.node);
|
|
5996
|
+
const orphaned = existing.filter((token) => (token == null ? void 0 : token.title) === "Drawbridge");
|
|
5997
|
+
await Promise.allSettled(
|
|
5998
|
+
orphaned.map((token) => adminFetch({
|
|
5999
|
+
adminAccessToken,
|
|
6000
|
+
domain: shop,
|
|
6001
|
+
query: `
|
|
6002
|
+
mutation StorefrontAccessTokenDelete($input: StorefrontAccessTokenDeleteInput!) {
|
|
6003
|
+
storefrontAccessTokenDelete(input: $input) {
|
|
6004
|
+
deletedStorefrontAccessTokenId
|
|
6005
|
+
userErrors {
|
|
6006
|
+
field
|
|
6007
|
+
message
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
`,
|
|
6012
|
+
variables: {
|
|
6013
|
+
input: {
|
|
6014
|
+
id: token.id
|
|
6015
|
+
}
|
|
6016
|
+
}
|
|
6017
|
+
}))
|
|
6018
|
+
);
|
|
6019
|
+
} catch {
|
|
6020
|
+
}
|
|
6021
|
+
const created = await adminFetch({
|
|
6022
|
+
fetcher,
|
|
6023
|
+
adminAccessToken,
|
|
6024
|
+
domain: shop,
|
|
6025
|
+
query: `
|
|
6026
|
+
mutation StorefrontAccessTokenCreate($input: StorefrontAccessTokenInput!) {
|
|
6027
|
+
storefrontAccessTokenCreate(input: $input) {
|
|
6028
|
+
storefrontAccessToken {
|
|
6029
|
+
accessToken
|
|
6030
|
+
}
|
|
6031
|
+
userErrors {
|
|
6032
|
+
field
|
|
6033
|
+
message
|
|
6034
|
+
}
|
|
6035
|
+
}
|
|
6036
|
+
}
|
|
6037
|
+
`,
|
|
6038
|
+
variables: {
|
|
6039
|
+
input: {
|
|
6040
|
+
title: "Drawbridge"
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
});
|
|
6044
|
+
const createErrors = (_c = created == null ? void 0 : created.storefrontAccessTokenCreate) == null ? void 0 : _c.userErrors;
|
|
6045
|
+
if (createErrors == null ? void 0 : createErrors.length) {
|
|
6046
|
+
throw new Error("Shopify storefront token create failed: " + createErrors[0].message);
|
|
6047
|
+
}
|
|
6048
|
+
;
|
|
6049
|
+
return ((_e = (_d = created == null ? void 0 : created.storefrontAccessTokenCreate) == null ? void 0 : _d.storefrontAccessToken) == null ? void 0 : _e.accessToken) || null;
|
|
6050
|
+
};
|
|
6051
|
+
var resolveTokenContext = async ({ connection: connection2, controller }) => {
|
|
6052
|
+
const decrypted = (connection2 == null ? void 0 : connection2.settings) ? decrypt(connection2.settings) : {};
|
|
6053
|
+
if ((decrypted == null ? void 0 : decrypted.ref) !== "shop") {
|
|
6054
|
+
return {
|
|
6055
|
+
raw: (connection2 == null ? void 0 : connection2.settings) || null,
|
|
6056
|
+
session: null,
|
|
6057
|
+
settings: decrypted,
|
|
6058
|
+
shop: null,
|
|
6059
|
+
target: "connection"
|
|
6060
|
+
};
|
|
6061
|
+
}
|
|
6062
|
+
;
|
|
6063
|
+
const shop = decrypted.shop;
|
|
6064
|
+
if (!controller || !shop) {
|
|
6065
|
+
return { raw: null, session: {}, settings: {}, shop, target: "shop" };
|
|
6066
|
+
}
|
|
6067
|
+
;
|
|
6068
|
+
const record = await controller.get({ collection: "shop", query: { shop } });
|
|
6069
|
+
const session = (record == null ? void 0 : record.settings) ? decrypt(record.settings) : {};
|
|
6070
|
+
const storefront = (record == null ? void 0 : record.storefront) ? decrypt(record.storefront) : null;
|
|
6071
|
+
return {
|
|
6072
|
+
raw: (record == null ? void 0 : record.settings) || null,
|
|
6073
|
+
session,
|
|
6074
|
+
settings: {
|
|
6075
|
+
adminAccessToken: session.accessToken || null,
|
|
6076
|
+
refreshToken: session.refreshToken || null,
|
|
6077
|
+
refreshTokenExpiresAt: session.refreshTokenExpires || null,
|
|
6078
|
+
storefrontAccessToken: (storefront == null ? void 0 : storefront.token) || null,
|
|
6079
|
+
tokenExpiresAt: session.expires || null
|
|
6080
|
+
},
|
|
6081
|
+
shop,
|
|
6082
|
+
target: "shop"
|
|
6083
|
+
};
|
|
6084
|
+
};
|
|
6085
|
+
var resolveConnectionSettings = async ({ fetcher, connection: connection2, controller }) => {
|
|
6086
|
+
const { settings } = await resolveTokenContext({ connection: connection2, controller });
|
|
6087
|
+
return settings;
|
|
6088
|
+
};
|
|
6089
|
+
var refreshAdminToken = async ({ fetcher, clientId, clientSecret, connection: connection2, controller }) => {
|
|
6090
|
+
var _a;
|
|
6091
|
+
const { raw, session, settings, shop, target } = await resolveTokenContext({ connection: connection2, controller });
|
|
6092
|
+
const domain = (connection2 == null ? void 0 : connection2.shop) || ((_a = settings.source) == null ? void 0 : _a.domain) || settings.domain || shop;
|
|
6093
|
+
const { refreshToken } = settings;
|
|
6094
|
+
const data2 = await shopifyOAuthFetch(
|
|
6095
|
+
`https://${domain}/admin/oauth/access_token`,
|
|
6096
|
+
{
|
|
6097
|
+
grant_type: "refresh_token",
|
|
6098
|
+
// Throw before the grant: rotation RETIRES the old pair the moment it
|
|
6099
|
+
// returns, so a credential-less call that Shopify rejects opaquely is
|
|
6100
|
+
// the one failure mode worth being loud about.
|
|
6101
|
+
client_id: required("clientId", clientId),
|
|
6102
|
+
client_secret: required("clientSecret", clientSecret),
|
|
6103
|
+
refresh_token: refreshToken
|
|
6104
|
+
}
|
|
6105
|
+
);
|
|
6106
|
+
const adminAccessToken = data2.access_token;
|
|
6107
|
+
const newRefreshToken = data2.refresh_token;
|
|
6108
|
+
const expiresIn = data2.expires_in;
|
|
6109
|
+
const tokenExpiresAt = expiresIn ? new Date(Date.now() + expiresIn * 1e3) : null;
|
|
6110
|
+
const refreshTokenExpiresAt = new Date(Date.now() + REFRESH_TOKEN_LIFETIME_MS);
|
|
6111
|
+
let liveToken = adminAccessToken;
|
|
6112
|
+
if (target === "shop") {
|
|
6113
|
+
const written = await controller.update({
|
|
6114
|
+
collection: "shop",
|
|
6115
|
+
data: {
|
|
6116
|
+
$set: {
|
|
6117
|
+
settings: encrypt({
|
|
6118
|
+
...session,
|
|
6119
|
+
accessToken: adminAccessToken,
|
|
6120
|
+
expires: tokenExpiresAt,
|
|
6121
|
+
refreshToken: newRefreshToken,
|
|
6122
|
+
refreshTokenExpires: refreshTokenExpiresAt
|
|
6123
|
+
})
|
|
6124
|
+
}
|
|
6125
|
+
},
|
|
6126
|
+
query: {
|
|
6127
|
+
shop,
|
|
6128
|
+
...raw && { settings: raw }
|
|
6129
|
+
}
|
|
6130
|
+
});
|
|
6131
|
+
if (!written) {
|
|
6132
|
+
const current = await controller.get({ collection: "shop", query: { shop } });
|
|
6133
|
+
if (!(current == null ? void 0 : current.settings)) {
|
|
6134
|
+
throw new Error("Shopify token refresh write-back failed: shop record missing for " + shop);
|
|
6135
|
+
}
|
|
6136
|
+
;
|
|
6137
|
+
const currentSession = decrypt(current.settings);
|
|
6138
|
+
if ((currentSession == null ? void 0 : currentSession.accessToken) && currentSession.accessToken !== settings.adminAccessToken) {
|
|
6139
|
+
liveToken = currentSession.accessToken;
|
|
6140
|
+
} else {
|
|
6141
|
+
throw new Error("Shopify token refresh write-back failed for " + shop);
|
|
6142
|
+
}
|
|
6143
|
+
;
|
|
6144
|
+
}
|
|
6145
|
+
;
|
|
6146
|
+
} else {
|
|
6147
|
+
const written = await controller.update({
|
|
6148
|
+
collection: "connection",
|
|
6149
|
+
data: {
|
|
6150
|
+
$set: {
|
|
6151
|
+
settings: encrypt({
|
|
6152
|
+
...settings,
|
|
6153
|
+
adminAccessToken,
|
|
6154
|
+
refreshToken: newRefreshToken,
|
|
6155
|
+
refreshTokenExpiresAt,
|
|
6156
|
+
tokenExpiresAt
|
|
6157
|
+
})
|
|
6158
|
+
}
|
|
6159
|
+
},
|
|
6160
|
+
query: {
|
|
6161
|
+
id: connection2.id,
|
|
6162
|
+
...raw && { settings: raw }
|
|
6163
|
+
}
|
|
6164
|
+
});
|
|
6165
|
+
if (!written) {
|
|
6166
|
+
const current = await controller.get({ collection: "connection", query: { id: connection2.id } });
|
|
6167
|
+
const currentSettings = (current == null ? void 0 : current.settings) ? decrypt(current.settings) : null;
|
|
6168
|
+
if ((currentSettings == null ? void 0 : currentSettings.adminAccessToken) && currentSettings.adminAccessToken !== settings.adminAccessToken) {
|
|
6169
|
+
liveToken = currentSettings.adminAccessToken;
|
|
6170
|
+
} else {
|
|
6171
|
+
throw new Error("Shopify token refresh write-back failed for connection " + (connection2 == null ? void 0 : connection2.id));
|
|
6172
|
+
}
|
|
6173
|
+
;
|
|
6174
|
+
}
|
|
6175
|
+
;
|
|
6176
|
+
}
|
|
6177
|
+
;
|
|
6178
|
+
await ping({ adminAccessToken: liveToken, domain });
|
|
6179
|
+
return liveToken;
|
|
6180
|
+
};
|
|
6181
|
+
var getAdminToken = async ({ fetcher, clientId, clientSecret, connection: connection2, controller }) => {
|
|
6182
|
+
required("clientId", clientId);
|
|
6183
|
+
required("clientSecret", clientSecret);
|
|
6184
|
+
const settings = await resolveConnectionSettings({ connection: connection2, controller });
|
|
6185
|
+
const { adminAccessToken, refreshToken, tokenExpiresAt } = settings;
|
|
6186
|
+
if (!adminAccessToken) {
|
|
6187
|
+
return null;
|
|
6188
|
+
}
|
|
6189
|
+
;
|
|
6190
|
+
if (!refreshToken) {
|
|
6191
|
+
return adminAccessToken;
|
|
6192
|
+
}
|
|
6193
|
+
;
|
|
6194
|
+
const needsRefresh = !tokenExpiresAt || new Date(tokenExpiresAt) < new Date(Date.now() + ACCESS_TOKEN_REFRESH_BUFFER_MS);
|
|
6195
|
+
if (needsRefresh) {
|
|
6196
|
+
return refreshAdminToken({ clientId, clientSecret, connection: connection2, controller });
|
|
6197
|
+
}
|
|
6198
|
+
;
|
|
6199
|
+
return adminAccessToken;
|
|
6200
|
+
};
|
|
6201
|
+
|
|
6202
|
+
// lib/shopify/partner.js
|
|
6203
|
+
var partner_exports = {};
|
|
6204
|
+
__export(partner_exports, {
|
|
6205
|
+
getUsageChargeEvents: () => getUsageChargeEvents
|
|
6206
|
+
});
|
|
6207
|
+
var PARTNER_API = "https://partners.shopify.com";
|
|
6208
|
+
var getUsageChargeEvents = async ({ fetcher, appId, first = 10, occurredAtMin, organizationId, partnerToken, shopId }) => {
|
|
6209
|
+
var _a, _b, _c;
|
|
6210
|
+
if (!partnerToken || !organizationId || !appId) {
|
|
6211
|
+
throw new Error("getUsageChargeEvents requires partnerToken, organizationId, and appId");
|
|
6212
|
+
}
|
|
6213
|
+
;
|
|
6214
|
+
const data2 = await request({
|
|
6215
|
+
fetcher,
|
|
6216
|
+
method: "POST",
|
|
6217
|
+
url: PARTNER_API + "/" + organizationId + "/api/" + PARTNER_API_VERSION + "/graphql.json",
|
|
6218
|
+
headers: {
|
|
6219
|
+
"X-Shopify-Access-Token": partnerToken
|
|
6220
|
+
},
|
|
6221
|
+
body: {
|
|
6222
|
+
query: `query usageCharges( $filter : EventFilterInput!, $first : Int! ) {
|
|
6223
|
+
events( filter : $filter, orderBy : OCCURRED_AT_DESC, first : $first ) {
|
|
6224
|
+
edges {
|
|
6225
|
+
node {
|
|
6226
|
+
id
|
|
6227
|
+
occurredAt
|
|
6228
|
+
eventType
|
|
6229
|
+
shop { id myshopifyDomain }
|
|
6230
|
+
... on Charge {
|
|
6231
|
+
chargeId
|
|
6232
|
+
chargeType
|
|
6233
|
+
amount { amount currencyCode }
|
|
6234
|
+
usageQuantity
|
|
6235
|
+
planHandle
|
|
6236
|
+
description
|
|
6237
|
+
}
|
|
6238
|
+
}
|
|
6239
|
+
}
|
|
6240
|
+
pageInfo { hasNextPage endCursor }
|
|
6241
|
+
}
|
|
6242
|
+
}`,
|
|
6243
|
+
variables: {
|
|
6244
|
+
filter: {
|
|
6245
|
+
eventTypes: ["CHARGE_USAGE"],
|
|
6246
|
+
subjectId: String(appId),
|
|
6247
|
+
...occurredAtMin && { occurredAtMin },
|
|
6248
|
+
...shopId && { shopId: String(shopId) }
|
|
6249
|
+
},
|
|
6250
|
+
first
|
|
6251
|
+
}
|
|
6252
|
+
}
|
|
6253
|
+
});
|
|
6254
|
+
if ((_a = data2 == null ? void 0 : data2.errors) == null ? void 0 : _a.length) {
|
|
6255
|
+
throw new Error("Shopify partner events query failed: " + data2.errors.map((entry) => entry.message).join("; "));
|
|
6256
|
+
}
|
|
6257
|
+
;
|
|
6258
|
+
return (((_c = (_b = data2 == null ? void 0 : data2.data) == null ? void 0 : _b.events) == null ? void 0 : _c.edges) || []).map((edge) => {
|
|
6259
|
+
var _a2, _b2, _c2, _d, _e, _f, _g, _h, _i, _j;
|
|
6260
|
+
return {
|
|
6261
|
+
amount: ((_a2 = edge.node) == null ? void 0 : _a2.amount) || null,
|
|
6262
|
+
chargeId: ((_b2 = edge.node) == null ? void 0 : _b2.chargeId) || null,
|
|
6263
|
+
// USAGE vs the recurring plan charge an approval emits — the
|
|
6264
|
+
// disambiguator callers filter on.
|
|
6265
|
+
chargeType: ((_c2 = edge.node) == null ? void 0 : _c2.chargeType) || null,
|
|
6266
|
+
description: ((_d = edge.node) == null ? void 0 : _d.description) || null,
|
|
6267
|
+
eventType: ((_e = edge.node) == null ? void 0 : _e.eventType) || null,
|
|
6268
|
+
id: ((_f = edge.node) == null ? void 0 : _f.id) || null,
|
|
6269
|
+
occurredAt: ((_g = edge.node) == null ? void 0 : _g.occurredAt) || null,
|
|
6270
|
+
planHandle: ((_h = edge.node) == null ? void 0 : _h.planHandle) || null,
|
|
6271
|
+
shop: ((_i = edge.node) == null ? void 0 : _i.shop) || null,
|
|
6272
|
+
usageQuantity: ((_j = edge.node) == null ? void 0 : _j.usageQuantity) ?? null
|
|
6273
|
+
};
|
|
6274
|
+
});
|
|
6275
|
+
};
|
|
6276
|
+
|
|
6277
|
+
// lib/shopify/storefront.js
|
|
6278
|
+
var storefront_exports = {};
|
|
6279
|
+
__export(storefront_exports, {
|
|
6280
|
+
cartCreate: () => cartCreate,
|
|
6281
|
+
cartGet: () => cartGet,
|
|
6282
|
+
cartLinesAdd: () => cartLinesAdd,
|
|
6283
|
+
cartLinesRemove: () => cartLinesRemove,
|
|
6284
|
+
cartLinesUpdate: () => cartLinesUpdate,
|
|
6285
|
+
createCheckoutToken: () => createCheckoutToken,
|
|
6286
|
+
getProduct: () => getProduct,
|
|
6287
|
+
getProductVariantsPage: () => getProductVariantsPage,
|
|
6288
|
+
getProducts: () => getProducts,
|
|
6289
|
+
probeVariantsOutOfStock: () => probeVariantsOutOfStock
|
|
6290
|
+
});
|
|
6291
|
+
var SORT_KEY_MAP = {
|
|
6292
|
+
bestSelling: "BEST_SELLING",
|
|
6293
|
+
createdAt: "CREATED_AT",
|
|
6294
|
+
id: "ID",
|
|
6295
|
+
price: "PRICE",
|
|
6296
|
+
productType: "PRODUCT_TYPE",
|
|
6297
|
+
title: "TITLE",
|
|
6298
|
+
updatedAt: "UPDATED_AT",
|
|
6299
|
+
vendor: "VENDOR"
|
|
6300
|
+
};
|
|
6301
|
+
var resolveProductSort = (sort) => {
|
|
6302
|
+
const entry = sort && Object.entries(sort)[0];
|
|
6303
|
+
if (!entry) return null;
|
|
6304
|
+
const [field2, direction] = entry;
|
|
6305
|
+
const sortKey = SORT_KEY_MAP[field2];
|
|
6306
|
+
if (!sortKey) return null;
|
|
6307
|
+
return {
|
|
6308
|
+
sortKey,
|
|
6309
|
+
reverse: Number(direction) < 0
|
|
6310
|
+
};
|
|
6311
|
+
};
|
|
6312
|
+
var storefrontUrl = (domain) => `https://${domain}/api/${SHOPIFY_STOREFRONT_API_VERSION}/graphql.json`;
|
|
6313
|
+
var storefrontFetch = async ({ fetcher, domain, storefrontAccessToken, query, variables }) => {
|
|
6314
|
+
return request({
|
|
6315
|
+
method: "POST",
|
|
6316
|
+
url: storefrontUrl(domain),
|
|
6317
|
+
headers: {
|
|
6318
|
+
"X-Shopify-Storefront-Access-Token": storefrontAccessToken
|
|
6319
|
+
},
|
|
6320
|
+
body: {
|
|
6321
|
+
query,
|
|
6322
|
+
...variables && { variables }
|
|
6323
|
+
}
|
|
6324
|
+
});
|
|
6325
|
+
};
|
|
6326
|
+
var shopCountryCache = {};
|
|
6327
|
+
var getShopCountry = async ({ domain, fetcher, storefrontAccessToken }) => {
|
|
6328
|
+
var _a, _b;
|
|
6329
|
+
if (shopCountryCache[domain]) return shopCountryCache[domain];
|
|
6330
|
+
const { data: data2 } = await storefrontFetch({
|
|
6331
|
+
fetcher,
|
|
6332
|
+
domain,
|
|
6333
|
+
storefrontAccessToken,
|
|
6334
|
+
query: `{
|
|
6335
|
+
shop {
|
|
6336
|
+
paymentSettings {
|
|
6337
|
+
countryCode
|
|
6338
|
+
}
|
|
6339
|
+
}
|
|
6340
|
+
}`
|
|
6341
|
+
});
|
|
6342
|
+
const countryCode = ((_b = (_a = data2 == null ? void 0 : data2.shop) == null ? void 0 : _a.paymentSettings) == null ? void 0 : _b.countryCode) || null;
|
|
6343
|
+
if (countryCode) shopCountryCache[domain] = countryCode;
|
|
6344
|
+
return countryCode;
|
|
6345
|
+
};
|
|
6346
|
+
var OUT_OF_STOCK_WARNING_CODES = [
|
|
6347
|
+
"MERCHANDISE_OUT_OF_STOCK",
|
|
6348
|
+
"MERCHANDISE_NOT_ENOUGH_STOCK"
|
|
6349
|
+
];
|
|
6350
|
+
var probeVariantsOutOfStock = async ({ fetcher, domain, storefrontAccessToken, variantIds, countryCode }) => {
|
|
6351
|
+
var _a, _b;
|
|
6352
|
+
if (!(variantIds == null ? void 0 : variantIds.length)) return [];
|
|
6353
|
+
const resolvedCountry = countryCode || await getShopCountry({ domain, fetcher, storefrontAccessToken });
|
|
6354
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6355
|
+
fetcher,
|
|
6356
|
+
domain,
|
|
6357
|
+
storefrontAccessToken,
|
|
6358
|
+
query: `
|
|
6359
|
+
mutation probe( $input : CartInput! ){
|
|
6360
|
+
cartCreate( input : $input ){
|
|
6361
|
+
cart {
|
|
6362
|
+
lines( first : 250 ){
|
|
6363
|
+
edges {
|
|
6364
|
+
node {
|
|
6365
|
+
id
|
|
6366
|
+
merchandise {
|
|
6367
|
+
... on ProductVariant {
|
|
6368
|
+
id
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6371
|
+
}
|
|
6372
|
+
}
|
|
6373
|
+
}
|
|
6374
|
+
}
|
|
6375
|
+
warnings {
|
|
6376
|
+
code
|
|
6377
|
+
message
|
|
6378
|
+
target
|
|
6379
|
+
}
|
|
6380
|
+
}
|
|
6381
|
+
}
|
|
6382
|
+
`,
|
|
6383
|
+
variables: {
|
|
6384
|
+
input: {
|
|
6385
|
+
lines: variantIds.map((merchandiseId) => ({
|
|
6386
|
+
merchandiseId,
|
|
6387
|
+
quantity: 1
|
|
6388
|
+
})),
|
|
6389
|
+
...resolvedCountry && {
|
|
6390
|
+
buyerIdentity: {
|
|
6391
|
+
countryCode: resolvedCountry
|
|
6392
|
+
}
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
});
|
|
6397
|
+
if (errors) return [];
|
|
6398
|
+
const payload = data2 == null ? void 0 : data2.cartCreate;
|
|
6399
|
+
const soldOut = new Set(
|
|
6400
|
+
((payload == null ? void 0 : payload.warnings) || []).filter((warning) => OUT_OF_STOCK_WARNING_CODES.includes(warning == null ? void 0 : warning.code) && (warning == null ? void 0 : warning.target)).map((warning) => warning.target)
|
|
6401
|
+
);
|
|
6402
|
+
return (((_b = (_a = payload == null ? void 0 : payload.cart) == null ? void 0 : _a.lines) == null ? void 0 : _b.edges) || []).filter(({ node }) => soldOut.has(node.id)).map(({ node }) => {
|
|
6403
|
+
var _a2;
|
|
6404
|
+
return (_a2 = node == null ? void 0 : node.merchandise) == null ? void 0 : _a2.id;
|
|
6405
|
+
}).filter(Boolean);
|
|
6406
|
+
};
|
|
6407
|
+
var productsQuery = (search, cursor, limit, sort) => {
|
|
6408
|
+
const resolved = resolveProductSort(sort);
|
|
6409
|
+
const sortArgs = resolved ? `, sortKey: ${resolved.sortKey}, reverse: ${resolved.reverse}` : "";
|
|
6410
|
+
return `{
|
|
6411
|
+
products(first: ${limit}${search ? `, query: "title:*${search}*"` : ""}${cursor ? `, after: "${cursor}"` : ""}${sortArgs}) {
|
|
6412
|
+
edges {
|
|
6413
|
+
node {
|
|
6414
|
+
id
|
|
6415
|
+
title
|
|
6416
|
+
description
|
|
6417
|
+
handle
|
|
6418
|
+
productType
|
|
6419
|
+
featuredImage {
|
|
6420
|
+
url
|
|
6421
|
+
}
|
|
6422
|
+
variants(first: 10) {
|
|
6423
|
+
edges {
|
|
6424
|
+
node {
|
|
6425
|
+
id
|
|
6426
|
+
title
|
|
6427
|
+
image {
|
|
6428
|
+
url
|
|
6429
|
+
}
|
|
6430
|
+
price {
|
|
6431
|
+
amount
|
|
6432
|
+
currencyCode
|
|
6433
|
+
}
|
|
6434
|
+
compareAtPrice {
|
|
6435
|
+
amount
|
|
6436
|
+
currencyCode
|
|
6437
|
+
}
|
|
6438
|
+
availableForSale
|
|
6439
|
+
requiresShipping
|
|
6440
|
+
}
|
|
6441
|
+
}
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
}
|
|
6445
|
+
pageInfo {
|
|
6446
|
+
endCursor
|
|
6447
|
+
hasNextPage
|
|
6448
|
+
hasPreviousPage
|
|
6449
|
+
startCursor
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
}`;
|
|
6453
|
+
};
|
|
6454
|
+
var productQuery = (productId) => `{
|
|
6455
|
+
product(id: "${productId}") {
|
|
6456
|
+
id
|
|
6457
|
+
title
|
|
6458
|
+
description
|
|
6459
|
+
handle
|
|
6460
|
+
productType
|
|
6461
|
+
featuredImage {
|
|
6462
|
+
url
|
|
6463
|
+
}
|
|
6464
|
+
variants(first: 10) {
|
|
6465
|
+
edges {
|
|
6466
|
+
node {
|
|
6467
|
+
id
|
|
6468
|
+
title
|
|
6469
|
+
image {
|
|
6470
|
+
url
|
|
6471
|
+
}
|
|
6472
|
+
price {
|
|
6473
|
+
amount
|
|
6474
|
+
currencyCode
|
|
6475
|
+
}
|
|
6476
|
+
compareAtPrice {
|
|
6477
|
+
amount
|
|
6478
|
+
currencyCode
|
|
6479
|
+
}
|
|
6480
|
+
availableForSale
|
|
6481
|
+
requiresShipping
|
|
6482
|
+
}
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
}
|
|
6486
|
+
}`;
|
|
6487
|
+
var variantsQuery = (productId, cursor) => `{
|
|
6488
|
+
product(id: "${productId}") {
|
|
6489
|
+
variants(first: 250${cursor ? `, after: "${cursor}"` : ""}) {
|
|
6490
|
+
edges {
|
|
6491
|
+
node {
|
|
6492
|
+
id
|
|
6493
|
+
title
|
|
6494
|
+
image {
|
|
6495
|
+
url
|
|
6496
|
+
}
|
|
6497
|
+
price {
|
|
6498
|
+
amount
|
|
6499
|
+
currencyCode
|
|
6500
|
+
}
|
|
6501
|
+
compareAtPrice {
|
|
6502
|
+
amount
|
|
6503
|
+
currencyCode
|
|
6504
|
+
}
|
|
6505
|
+
availableForSale
|
|
6506
|
+
requiresShipping
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
pageInfo {
|
|
6510
|
+
hasNextPage
|
|
6511
|
+
endCursor
|
|
6512
|
+
}
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
}`;
|
|
6516
|
+
var getProducts = async ({ fetcher, domain, storefrontAccessToken, search, cursor, limit, sort }) => {
|
|
6517
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6518
|
+
fetcher,
|
|
6519
|
+
domain,
|
|
6520
|
+
storefrontAccessToken,
|
|
6521
|
+
query: productsQuery(search, cursor, limit, sort)
|
|
6522
|
+
});
|
|
6523
|
+
if (errors) {
|
|
6524
|
+
throw new Error("Failed to fetch Shopify products");
|
|
6525
|
+
}
|
|
6526
|
+
;
|
|
6527
|
+
return data2 == null ? void 0 : data2.products;
|
|
6528
|
+
};
|
|
6529
|
+
var getProduct = async ({ fetcher, domain, storefrontAccessToken, productId }) => {
|
|
6530
|
+
var _a;
|
|
6531
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6532
|
+
fetcher,
|
|
6533
|
+
domain,
|
|
6534
|
+
storefrontAccessToken,
|
|
6535
|
+
query: productQuery(productId)
|
|
6536
|
+
});
|
|
6537
|
+
if (errors) {
|
|
6538
|
+
throw new Error("Shopify storefront error: " + ((_a = errors[0]) == null ? void 0 : _a.message));
|
|
6539
|
+
}
|
|
6540
|
+
;
|
|
6541
|
+
if (!(data2 == null ? void 0 : data2.product)) {
|
|
6542
|
+
throw new Error("Shopify product not found");
|
|
6543
|
+
}
|
|
6544
|
+
;
|
|
6545
|
+
return data2.product;
|
|
6546
|
+
};
|
|
6547
|
+
var getProductVariantsPage = async ({ fetcher, domain, storefrontAccessToken, productId, cursor }) => {
|
|
6548
|
+
var _a;
|
|
6549
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6550
|
+
fetcher,
|
|
6551
|
+
domain,
|
|
6552
|
+
storefrontAccessToken,
|
|
6553
|
+
query: variantsQuery(productId, cursor)
|
|
6554
|
+
});
|
|
6555
|
+
if (errors) {
|
|
6556
|
+
throw new Error("Shopify storefront error: " + ((_a = errors[0]) == null ? void 0 : _a.message));
|
|
6557
|
+
}
|
|
6558
|
+
;
|
|
6559
|
+
if (!(data2 == null ? void 0 : data2.product)) {
|
|
6560
|
+
throw new Error("Shopify product not found");
|
|
6561
|
+
}
|
|
6562
|
+
;
|
|
6563
|
+
return data2.product.variants;
|
|
6564
|
+
};
|
|
6565
|
+
var CART_FIELDS = `
|
|
6566
|
+
id
|
|
6567
|
+
checkoutUrl
|
|
6568
|
+
lines(first: 100) {
|
|
6569
|
+
edges {
|
|
6570
|
+
node {
|
|
6571
|
+
id
|
|
6572
|
+
quantity
|
|
6573
|
+
merchandise {
|
|
6574
|
+
... on ProductVariant {
|
|
6575
|
+
id
|
|
6576
|
+
title
|
|
6577
|
+
availableForSale
|
|
6578
|
+
image {
|
|
6579
|
+
url
|
|
6580
|
+
}
|
|
6581
|
+
price {
|
|
6582
|
+
amount
|
|
6583
|
+
currencyCode
|
|
6584
|
+
}
|
|
6585
|
+
product {
|
|
6586
|
+
title
|
|
6587
|
+
}
|
|
6588
|
+
}
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
`;
|
|
6594
|
+
var mapCart = (cart) => {
|
|
6595
|
+
var _a;
|
|
6596
|
+
if (!cart) return null;
|
|
6597
|
+
return {
|
|
6598
|
+
checkoutUrl: cart.checkoutUrl,
|
|
6599
|
+
id: cart.id,
|
|
6600
|
+
lines: (((_a = cart.lines) == null ? void 0 : _a.edges) || []).map(({ node }) => {
|
|
6601
|
+
var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
6602
|
+
return {
|
|
6603
|
+
availableForSale: ((_a2 = node == null ? void 0 : node.merchandise) == null ? void 0 : _a2.availableForSale) ?? true,
|
|
6604
|
+
currency: ((_c = (_b = node == null ? void 0 : node.merchandise) == null ? void 0 : _b.price) == null ? void 0 : _c.currencyCode) || null,
|
|
6605
|
+
id: node.id,
|
|
6606
|
+
image: ((_e = (_d = node == null ? void 0 : node.merchandise) == null ? void 0 : _d.image) == null ? void 0 : _e.url) || null,
|
|
6607
|
+
price: ((_g = (_f = node == null ? void 0 : node.merchandise) == null ? void 0 : _f.price) == null ? void 0 : _g.amount) || null,
|
|
6608
|
+
productTitle: ((_i = (_h = node == null ? void 0 : node.merchandise) == null ? void 0 : _h.product) == null ? void 0 : _i.title) || null,
|
|
6609
|
+
quantity: node.quantity,
|
|
6610
|
+
variantId: (_j = node == null ? void 0 : node.merchandise) == null ? void 0 : _j.id,
|
|
6611
|
+
variantTitle: ((_k = node == null ? void 0 : node.merchandise) == null ? void 0 : _k.title) || null
|
|
6612
|
+
};
|
|
6613
|
+
})
|
|
6614
|
+
};
|
|
6615
|
+
};
|
|
6616
|
+
var firstUserError = (userErrors) => {
|
|
6617
|
+
const error = userErrors == null ? void 0 : userErrors[0];
|
|
6618
|
+
if (!error) return null;
|
|
6619
|
+
const message = new Error(error.message || "Shopify cart error");
|
|
6620
|
+
message.userError = true;
|
|
6621
|
+
return message;
|
|
6622
|
+
};
|
|
6623
|
+
var createCheckoutToken = async ({ fetcher, clientId, clientSecret } = {}) => {
|
|
6624
|
+
if (!clientId || !clientSecret) {
|
|
6625
|
+
throw new Error("createCheckoutToken requires clientId and clientSecret");
|
|
6626
|
+
}
|
|
6627
|
+
;
|
|
6628
|
+
const response = await request({
|
|
6629
|
+
fetcher,
|
|
6630
|
+
method: "POST",
|
|
6631
|
+
url: "https://api.shopify.com/auth/access_token",
|
|
6632
|
+
body: {
|
|
6633
|
+
client_id: clientId,
|
|
6634
|
+
client_secret: clientSecret,
|
|
6635
|
+
grant_type: "client_credentials"
|
|
6636
|
+
}
|
|
6637
|
+
});
|
|
6638
|
+
const token = response == null ? void 0 : response.access_token;
|
|
6639
|
+
if (!token) {
|
|
6640
|
+
throw new Error("Failed to create Shopify checkout token");
|
|
6641
|
+
}
|
|
6642
|
+
;
|
|
6643
|
+
return token;
|
|
6644
|
+
};
|
|
6645
|
+
var cartCreate = async ({ fetcher, domain, storefrontAccessToken, lines }) => {
|
|
6646
|
+
var _a, _b;
|
|
6647
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6648
|
+
fetcher,
|
|
6649
|
+
domain,
|
|
6650
|
+
storefrontAccessToken,
|
|
6651
|
+
query: `
|
|
6652
|
+
mutation cartCreate($input: CartInput!) {
|
|
6653
|
+
cartCreate(input: $input) {
|
|
6654
|
+
cart {
|
|
6655
|
+
${CART_FIELDS}
|
|
6656
|
+
}
|
|
6657
|
+
userErrors {
|
|
6658
|
+
field
|
|
6659
|
+
message
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
}
|
|
6663
|
+
`,
|
|
6664
|
+
variables: {
|
|
6665
|
+
input: {
|
|
6666
|
+
lines
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
});
|
|
6670
|
+
if (errors) {
|
|
6671
|
+
throw new Error("Failed to create Shopify cart");
|
|
6672
|
+
}
|
|
6673
|
+
;
|
|
6674
|
+
const userError = firstUserError((_a = data2 == null ? void 0 : data2.cartCreate) == null ? void 0 : _a.userErrors);
|
|
6675
|
+
if (userError) throw userError;
|
|
6676
|
+
return mapCart((_b = data2 == null ? void 0 : data2.cartCreate) == null ? void 0 : _b.cart);
|
|
6677
|
+
};
|
|
6678
|
+
var cartGet = async ({ fetcher, domain, storefrontAccessToken, cartId }) => {
|
|
6679
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6680
|
+
fetcher,
|
|
6681
|
+
domain,
|
|
6682
|
+
storefrontAccessToken,
|
|
6683
|
+
query: `
|
|
6684
|
+
query cart($id: ID!) {
|
|
6685
|
+
cart(id: $id) {
|
|
6686
|
+
${CART_FIELDS}
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
`,
|
|
6690
|
+
variables: {
|
|
6691
|
+
id: cartId
|
|
6692
|
+
}
|
|
6693
|
+
});
|
|
6694
|
+
if (errors) {
|
|
6695
|
+
throw new Error("Failed to fetch Shopify cart");
|
|
6696
|
+
}
|
|
6697
|
+
;
|
|
6698
|
+
return mapCart(data2 == null ? void 0 : data2.cart);
|
|
6699
|
+
};
|
|
6700
|
+
var cartLinesAdd = async ({ fetcher, domain, storefrontAccessToken, cartId, lines }) => {
|
|
6701
|
+
var _a, _b;
|
|
6702
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6703
|
+
fetcher,
|
|
6704
|
+
domain,
|
|
6705
|
+
storefrontAccessToken,
|
|
6706
|
+
query: `
|
|
6707
|
+
mutation cartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) {
|
|
6708
|
+
cartLinesAdd(cartId: $cartId, lines: $lines) {
|
|
6709
|
+
cart {
|
|
6710
|
+
${CART_FIELDS}
|
|
6711
|
+
}
|
|
6712
|
+
userErrors {
|
|
6713
|
+
field
|
|
6714
|
+
message
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
`,
|
|
6719
|
+
variables: {
|
|
6720
|
+
cartId,
|
|
6721
|
+
lines
|
|
6722
|
+
}
|
|
6723
|
+
});
|
|
6724
|
+
if (errors) {
|
|
6725
|
+
throw new Error("Failed to add Shopify cart lines");
|
|
6726
|
+
}
|
|
6727
|
+
;
|
|
6728
|
+
const userError = firstUserError((_a = data2 == null ? void 0 : data2.cartLinesAdd) == null ? void 0 : _a.userErrors);
|
|
6729
|
+
if (userError) throw userError;
|
|
6730
|
+
return mapCart((_b = data2 == null ? void 0 : data2.cartLinesAdd) == null ? void 0 : _b.cart);
|
|
6731
|
+
};
|
|
6732
|
+
var cartLinesRemove = async ({ fetcher, domain, storefrontAccessToken, cartId, lineIds }) => {
|
|
6733
|
+
var _a, _b;
|
|
6734
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6735
|
+
fetcher,
|
|
6736
|
+
domain,
|
|
6737
|
+
storefrontAccessToken,
|
|
6738
|
+
query: `
|
|
6739
|
+
mutation cartLinesRemove($cartId: ID!, $lineIds: [ID!]!) {
|
|
6740
|
+
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
|
|
6741
|
+
cart {
|
|
6742
|
+
${CART_FIELDS}
|
|
6743
|
+
}
|
|
6744
|
+
userErrors {
|
|
6745
|
+
field
|
|
6746
|
+
message
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
}
|
|
6750
|
+
`,
|
|
6751
|
+
variables: {
|
|
6752
|
+
cartId,
|
|
6753
|
+
lineIds
|
|
6754
|
+
}
|
|
6755
|
+
});
|
|
6756
|
+
if (errors) {
|
|
6757
|
+
throw new Error("Failed to remove Shopify cart lines");
|
|
6758
|
+
}
|
|
6759
|
+
;
|
|
6760
|
+
const userError = firstUserError((_a = data2 == null ? void 0 : data2.cartLinesRemove) == null ? void 0 : _a.userErrors);
|
|
6761
|
+
if (userError) throw userError;
|
|
6762
|
+
return mapCart((_b = data2 == null ? void 0 : data2.cartLinesRemove) == null ? void 0 : _b.cart);
|
|
6763
|
+
};
|
|
6764
|
+
var cartLinesUpdate = async ({ fetcher, domain, storefrontAccessToken, cartId, lines }) => {
|
|
6765
|
+
var _a, _b;
|
|
6766
|
+
const { data: data2, errors } = await storefrontFetch({
|
|
6767
|
+
fetcher,
|
|
6768
|
+
domain,
|
|
6769
|
+
storefrontAccessToken,
|
|
6770
|
+
query: `
|
|
6771
|
+
mutation cartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) {
|
|
6772
|
+
cartLinesUpdate(cartId: $cartId, lines: $lines) {
|
|
6773
|
+
cart {
|
|
6774
|
+
${CART_FIELDS}
|
|
6775
|
+
}
|
|
6776
|
+
userErrors {
|
|
6777
|
+
field
|
|
6778
|
+
message
|
|
6779
|
+
}
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
`,
|
|
6783
|
+
variables: {
|
|
6784
|
+
cartId,
|
|
6785
|
+
lines
|
|
6786
|
+
}
|
|
6787
|
+
});
|
|
6788
|
+
if (errors) {
|
|
6789
|
+
throw new Error("Failed to update Shopify cart lines");
|
|
6790
|
+
}
|
|
6791
|
+
;
|
|
6792
|
+
const userError = firstUserError((_a = data2 == null ? void 0 : data2.cartLinesUpdate) == null ? void 0 : _a.userErrors);
|
|
6793
|
+
if (userError) throw userError;
|
|
6794
|
+
return mapCart((_b = data2 == null ? void 0 : data2.cartLinesUpdate) == null ? void 0 : _b.cart);
|
|
6795
|
+
};
|
|
6796
|
+
|
|
6797
|
+
// lib/email.js
|
|
6798
|
+
var GMAIL_DOMAINS = /* @__PURE__ */ new Set(["gmail.com", "googlemail.com"]);
|
|
6799
|
+
var toCanonicalEmail = (value) => {
|
|
6800
|
+
if (!value || typeof value !== "string") return null;
|
|
6801
|
+
const email = value.trim().toLowerCase();
|
|
6802
|
+
const at = email.lastIndexOf("@");
|
|
6803
|
+
if (at < 1 || at === email.length - 1) return null;
|
|
6804
|
+
let local = email.slice(0, at);
|
|
6805
|
+
const domain = email.slice(at + 1);
|
|
6806
|
+
const plus = local.indexOf("+");
|
|
6807
|
+
if (plus > 0) local = local.slice(0, plus);
|
|
6808
|
+
if (GMAIL_DOMAINS.has(domain)) local = local.replaceAll(".", "");
|
|
6809
|
+
if (!local) return null;
|
|
6810
|
+
return local + "@" + domain;
|
|
6811
|
+
};
|
|
6812
|
+
|
|
6813
|
+
// lib/oauth/index.js
|
|
6814
|
+
var lifetimes = {
|
|
6815
|
+
access: 60 * 60 * 8,
|
|
6816
|
+
// 8 hours
|
|
6817
|
+
authorizationCode: 10 * 60,
|
|
6818
|
+
// 10 minutes
|
|
6819
|
+
code: 15 * 60,
|
|
6820
|
+
// 15 minutes (email OTC)
|
|
6821
|
+
refresh: 30 * 24 * 60 * 60
|
|
6822
|
+
// 30 days
|
|
6823
|
+
};
|
|
6824
|
+
var tokenTypes = {
|
|
6825
|
+
access: "access",
|
|
6826
|
+
oauthState: "oauth.state",
|
|
6827
|
+
pat: "pat",
|
|
6828
|
+
refresh: "refresh"
|
|
6829
|
+
};
|
|
6830
|
+
var scopes = [
|
|
6831
|
+
"profile:read",
|
|
6832
|
+
"profile:write",
|
|
6833
|
+
"organization:read",
|
|
6834
|
+
"organization:write",
|
|
6835
|
+
"campaigns:read",
|
|
6836
|
+
"campaigns:write",
|
|
6837
|
+
"contacts:read",
|
|
6838
|
+
"contacts:write",
|
|
6839
|
+
"workflows:read",
|
|
6840
|
+
"workflows:write",
|
|
6841
|
+
"connections:read",
|
|
6842
|
+
"connections:write",
|
|
6843
|
+
"billing:read",
|
|
6844
|
+
"billing:write",
|
|
6845
|
+
"admin"
|
|
6846
|
+
];
|
|
6847
|
+
var developer = [
|
|
6848
|
+
"profile:read",
|
|
6849
|
+
"organization:read",
|
|
6850
|
+
"campaigns:read",
|
|
6851
|
+
"campaigns:write",
|
|
6852
|
+
"contacts:read",
|
|
6853
|
+
"contacts:write",
|
|
6854
|
+
"workflows:read",
|
|
6855
|
+
"workflows:write",
|
|
6856
|
+
"connections:read",
|
|
6857
|
+
"connections:write"
|
|
6858
|
+
];
|
|
6859
|
+
var scopeLabels = {
|
|
6860
|
+
"profile:read": "Read own profile",
|
|
6861
|
+
"profile:write": "Update own profile",
|
|
6862
|
+
"organization:read": "Read organization info",
|
|
6863
|
+
"organization:write": "Manage organization",
|
|
6864
|
+
"campaigns:read": "Read campaigns",
|
|
6865
|
+
"campaigns:write": "Manage campaigns",
|
|
6866
|
+
"contacts:read": "Read contacts",
|
|
6867
|
+
"contacts:write": "Manage contacts",
|
|
6868
|
+
"workflows:read": "Read workflows",
|
|
6869
|
+
"workflows:write": "Manage workflows",
|
|
6870
|
+
"connections:read": "Read integrations",
|
|
6871
|
+
"connections:write": "Manage integrations",
|
|
6872
|
+
"billing:read": "Read billing",
|
|
6873
|
+
"billing:write": "Manage billing",
|
|
6874
|
+
admin: "Admin access"
|
|
6875
|
+
};
|
|
6876
|
+
var toOption = (scope) => ({
|
|
6877
|
+
key: scopeLabels[scope] || scope,
|
|
6878
|
+
value: scope
|
|
6879
|
+
});
|
|
6880
|
+
var developerOptions = developer.map(toOption);
|
|
6881
|
+
var scopeOptions = scopes.map(toOption);
|
|
6882
|
+
var hashToken = (raw) => hash(raw, process.env.HMAC_OAUTH_TOKEN_KEY);
|
|
6883
|
+
|
|
6884
|
+
// lib/sanitize.js
|
|
6885
|
+
var import_disposable_email_domains = __toESM(require("disposable-email-domains/index.js"), 1);
|
|
6886
|
+
var disposableBlacklist = new Set(import_disposable_email_domains.default.map((d) => d.toLowerCase()));
|
|
6887
|
+
var sanitizeDomain = (value, pattern) => {
|
|
6888
|
+
if (!value) return value;
|
|
6889
|
+
const cleaned = value.trim().toLowerCase().replace(/^https?:\/\//i, "").replace(/\/+$/, "");
|
|
6890
|
+
if (pattern) {
|
|
6891
|
+
const match = cleaned.match(pattern);
|
|
6892
|
+
return match ? match[0] : cleaned;
|
|
6893
|
+
}
|
|
6894
|
+
return cleaned;
|
|
6895
|
+
};
|
|
6896
|
+
|
|
6897
|
+
// lib/slugify.js
|
|
6898
|
+
var import_slugify = __toESM(require("slugify"), 1);
|
|
6899
|
+
var slugify = (value, nochars = false) => {
|
|
6900
|
+
const remove = nochars ? /[-?*+~.()'"!:@#^_{}\[\];,/\\]/g : /[?*+~.()'"!:@#^_{}\[\];,/\\]/g;
|
|
6901
|
+
const replacement = nochars ? "" : "-";
|
|
6902
|
+
return (0, import_slugify.default)(
|
|
6903
|
+
value,
|
|
6904
|
+
{
|
|
6905
|
+
remove,
|
|
6906
|
+
replacement,
|
|
6907
|
+
lower: true,
|
|
6908
|
+
trim: true
|
|
6909
|
+
}
|
|
6910
|
+
);
|
|
6911
|
+
};
|
|
6912
|
+
|
|
6913
|
+
// lib/connections/manifests/shopify.js
|
|
6914
|
+
var client = Object.freeze({
|
|
6915
|
+
admin: admin_exports,
|
|
6916
|
+
billing: billing_exports,
|
|
6917
|
+
oauth: oauth_exports,
|
|
6918
|
+
partner: partner_exports,
|
|
6919
|
+
storefront: storefront_exports
|
|
6920
|
+
});
|
|
6921
|
+
var toLine = ({
|
|
6922
|
+
price,
|
|
6923
|
+
product_id: productId,
|
|
6924
|
+
quantity,
|
|
6925
|
+
title,
|
|
6926
|
+
variant_id: variantId,
|
|
6927
|
+
variant_title: variantTitle
|
|
6928
|
+
}) => ({
|
|
6929
|
+
price: parseFloat(price) || 0,
|
|
6930
|
+
productId: productId ? "gid://shopify/Product/" + productId : null,
|
|
6931
|
+
quantity: quantity || 1,
|
|
6932
|
+
title: title || null,
|
|
6933
|
+
variantId: variantId ? "gid://shopify/ProductVariant/" + variantId : null,
|
|
6934
|
+
variantTitle: variantTitle || null
|
|
6935
|
+
});
|
|
6936
|
+
var attributeLineItems = (lineItems = []) => lineItems.reduce(
|
|
6937
|
+
(acc, item) => {
|
|
6938
|
+
const attrs = (item.properties || []).reduce(
|
|
6939
|
+
(map, { name, value }) => {
|
|
6940
|
+
map[name] = value;
|
|
6941
|
+
return map;
|
|
6942
|
+
},
|
|
6943
|
+
{}
|
|
6944
|
+
);
|
|
6945
|
+
if (!attrs["_drwbrdg_ca"]) return acc;
|
|
6946
|
+
if (!Object.keys(acc.attrMap).length) acc.attrMap = attrs;
|
|
6947
|
+
const line = toLine(item);
|
|
6948
|
+
acc.attributedGross += line.price * line.quantity;
|
|
6949
|
+
acc.attributedLines.push(line);
|
|
6950
|
+
return acc;
|
|
6951
|
+
},
|
|
6952
|
+
{ attrMap: {}, attributedGross: 0, attributedLines: [] }
|
|
6953
|
+
);
|
|
6954
|
+
var generateDiscountCode = (0, import_nanoid.customAlphabet)("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 8);
|
|
6955
|
+
var blockedReason = (discount) => {
|
|
6956
|
+
var _a;
|
|
6957
|
+
if ((discount == null ? void 0 : discount.status) === "EXPIRED") return "This discount has expired.";
|
|
6958
|
+
const buyers = (_a = discount == null ? void 0 : discount.context) == null ? void 0 : _a.__typename;
|
|
6959
|
+
if (buyers && buyers !== "DiscountBuyerSelectionAll") {
|
|
6960
|
+
return "This discount is limited to specific buyers in Shopify, so a code issued to an entrant won't work. Set it to all customers to use it here.";
|
|
6961
|
+
}
|
|
6962
|
+
;
|
|
6963
|
+
if (typeof (discount == null ? void 0 : discount.usageLimit) === "number" && discount.usageLimit > 0 && ((discount == null ? void 0 : discount.asyncUsageCount) || 0) >= discount.usageLimit) {
|
|
6964
|
+
return "This discount has reached its total usage limit.";
|
|
6965
|
+
}
|
|
6966
|
+
;
|
|
6967
|
+
return null;
|
|
6968
|
+
};
|
|
6969
|
+
var discountWarning = (discount) => {
|
|
6970
|
+
if ((discount == null ? void 0 : discount.status) === "SCHEDULED") {
|
|
6971
|
+
return "This discount hasn't started yet, so codes issued before it does won't work until then.";
|
|
6972
|
+
}
|
|
6973
|
+
;
|
|
6974
|
+
if (discount == null ? void 0 : discount.appliesOncePerCustomer) return "Each customer can use this discount only once.";
|
|
6975
|
+
return null;
|
|
6976
|
+
};
|
|
6977
|
+
var ORDER_EVENT_HANDLE = slugify("drawbridge-orders");
|
|
6978
|
+
var REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
6979
|
+
var OAUTH_ERROR_SOURCE = "oauth";
|
|
6980
|
+
var BILLING_ERROR_SOURCE = "billing";
|
|
6981
|
+
var BILLING_ERROR_MESSAGE = "Order billing isn't set up for this store \u2014 reselect and approve the plan from the Shopify connection page.";
|
|
6982
|
+
var OAUTH_GRANT_REVOKED_CODES = ["application_cannot_be_found", "invalid_grant"];
|
|
6983
|
+
var inbound = {
|
|
6984
|
+
headers: {
|
|
6985
|
+
event: "x-shopify-topic",
|
|
6986
|
+
id: "x-shopify-webhook-id",
|
|
6987
|
+
shop: "x-shopify-shop-domain",
|
|
6988
|
+
signature: "x-shopify-hmac-sha256"
|
|
6989
|
+
},
|
|
6990
|
+
signature: {
|
|
6991
|
+
algorithm: "sha256",
|
|
6992
|
+
encoding: "base64",
|
|
6993
|
+
secret: "SHOPIFY_API_SECRET"
|
|
6994
|
+
}
|
|
6995
|
+
};
|
|
6996
|
+
var COMPLIANCE_TOPICS = /* @__PURE__ */ new Set([
|
|
6997
|
+
"customers/data_request",
|
|
6998
|
+
"customers/redact",
|
|
6999
|
+
"shop/redact"
|
|
7000
|
+
]);
|
|
7001
|
+
var refusal = (message, status) => Object.assign(new Error(message), { status });
|
|
7002
|
+
var LINK_TOKEN_TTL_MS = 30 * 60 * 1e3;
|
|
7003
|
+
var CONNECTABLE_STATUSES = ["active", "unsubscribed"];
|
|
7004
|
+
var merchantOrganizations = ({ read, user }) => read.aggregate({
|
|
7005
|
+
collection: "organization",
|
|
7006
|
+
pipeline: [
|
|
7007
|
+
// A member qualifies only at `manage`; an owner always qualifies.
|
|
7008
|
+
{
|
|
7009
|
+
$lookup: {
|
|
7010
|
+
as: "membership",
|
|
7011
|
+
from: "member",
|
|
7012
|
+
let: { organization: "$id" },
|
|
7013
|
+
pipeline: [
|
|
7014
|
+
{
|
|
7015
|
+
$match: {
|
|
7016
|
+
$expr: { $eq: ["$organization", "$$organization"] },
|
|
7017
|
+
"capabilities.connections": "manage",
|
|
7018
|
+
status: "accepted",
|
|
7019
|
+
user
|
|
7020
|
+
}
|
|
7021
|
+
},
|
|
7022
|
+
{ $limit: 1 },
|
|
7023
|
+
{ $project: { _id: 0, organization: 1 } }
|
|
7024
|
+
]
|
|
7025
|
+
}
|
|
7026
|
+
},
|
|
7027
|
+
{
|
|
7028
|
+
$match: {
|
|
7029
|
+
status: { $ne: "canceled" },
|
|
7030
|
+
$or: [{ owner: user }, { "membership.0": { $exists: true } }]
|
|
7031
|
+
}
|
|
7032
|
+
},
|
|
7033
|
+
// `organization.subscription` is a subscription RECORD ID — resolved here so
|
|
7034
|
+
// the list can read the real plan and conversion rate. Without it every org
|
|
7035
|
+
// rendered as free tier.
|
|
7036
|
+
{ $lookup: { as: "subscription", foreignField: "id", from: "subscription", localField: "subscription" } },
|
|
7037
|
+
{ $unwind: { path: "$subscription", preserveNullAndEmptyArrays: true } },
|
|
7038
|
+
{
|
|
7039
|
+
$lookup: {
|
|
7040
|
+
as: "connection",
|
|
7041
|
+
foreignField: "organization",
|
|
7042
|
+
from: "connection",
|
|
7043
|
+
localField: "id",
|
|
7044
|
+
pipeline: [
|
|
7045
|
+
{ $match: { slug: "shopify" } },
|
|
7046
|
+
{ $limit: 1 },
|
|
7047
|
+
{ $project: { _id: 0, id: 1, shop: 1, status: 1 } }
|
|
7048
|
+
]
|
|
7049
|
+
}
|
|
7050
|
+
},
|
|
7051
|
+
{ $unwind: { path: "$connection", preserveNullAndEmptyArrays: true } },
|
|
7052
|
+
{ $project: { _id: 0, billingStatus: "$status", connection: 1, id: 1, name: "$title", subscription: 1 } },
|
|
7053
|
+
{ $sort: { name: 1 } }
|
|
7054
|
+
]
|
|
7055
|
+
});
|
|
7056
|
+
var storePlan = async ({ record, shopify }) => {
|
|
7057
|
+
var _a;
|
|
7058
|
+
const settings = (record == null ? void 0 : record.settings) ? decrypt(record.settings) : null;
|
|
7059
|
+
if (!(settings == null ? void 0 : settings.accessToken)) return { active: false, name: null };
|
|
7060
|
+
try {
|
|
7061
|
+
const { subscriptions } = await shopify.admin.getActiveAppSubscriptions({
|
|
7062
|
+
adminAccessToken: settings.accessToken,
|
|
7063
|
+
domain: record.shop
|
|
7064
|
+
});
|
|
7065
|
+
const active = (subscriptions || []).length > 0;
|
|
7066
|
+
const metered = active && subscriptions.some((subscription) => subscription.metered === true) ? true : null;
|
|
7067
|
+
return { active, metered, name: active ? ((_a = subscriptions[0]) == null ? void 0 : _a.name) || null : null };
|
|
7068
|
+
} catch (_) {
|
|
7069
|
+
return { active: false, name: null, unavailable: true };
|
|
7070
|
+
}
|
|
7071
|
+
};
|
|
7072
|
+
var releaseApprovals = async ({ context, read, shopify }) => {
|
|
7073
|
+
var _a;
|
|
7074
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
7075
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
7076
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
7077
|
+
const settings = (record == null ? void 0 : record.settings) ? decrypt(record.settings) : null;
|
|
7078
|
+
if (!(settings == null ? void 0 : settings.accessToken)) throw refusal("Store is not installed", 409);
|
|
7079
|
+
const { subscriptions } = await shopify.admin.getActiveAppSubscriptions({
|
|
7080
|
+
adminAccessToken: settings.accessToken,
|
|
7081
|
+
domain: shop
|
|
7082
|
+
});
|
|
7083
|
+
for (const subscription of subscriptions || []) {
|
|
7084
|
+
const data2 = await shopify.admin.adminFetch({
|
|
7085
|
+
adminAccessToken: settings.accessToken,
|
|
7086
|
+
domain: shop,
|
|
7087
|
+
query: "mutation appSubscriptionCancel( $id : ID! ) { appSubscriptionCancel( id : $id ) { appSubscription { id status } userErrors { field message } } }",
|
|
7088
|
+
variables: { id: subscription.id }
|
|
7089
|
+
});
|
|
7090
|
+
const [userError] = ((_a = data2 == null ? void 0 : data2.appSubscriptionCancel) == null ? void 0 : _a.userErrors) || [];
|
|
7091
|
+
if (userError) throw refusal(userError.message, 422);
|
|
7092
|
+
}
|
|
7093
|
+
return {
|
|
7094
|
+
message: "Released " + (subscriptions || []).length + " approval(s).",
|
|
7095
|
+
request: { shop },
|
|
7096
|
+
result: { ok: true }
|
|
7097
|
+
};
|
|
7098
|
+
};
|
|
7099
|
+
var LEDGER_WINDOW_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
7100
|
+
var meterVerdict = async ({ adminAccessToken, ledgerFloor, partner, shop, shopId, shopify }) => {
|
|
7101
|
+
var _a;
|
|
7102
|
+
const { subscriptions } = await shopify.admin.getActiveAppSubscriptions({ adminAccessToken, domain: shop });
|
|
7103
|
+
let metered = (_a = subscriptions.find((subscription) => subscription.usageLineItemId)) == null ? void 0 : _a.usageLineItemId;
|
|
7104
|
+
if (!((partner == null ? void 0 : partner.partnerToken) && (partner == null ? void 0 : partner.partnerOrgId) && (partner == null ? void 0 : partner.partnerAppId) && shopId)) {
|
|
7105
|
+
return { boundaries: [], ledger: false, metered };
|
|
7106
|
+
}
|
|
7107
|
+
const accruals = await shopify.partner.getUsageChargeEvents({
|
|
7108
|
+
appId: "gid://shopify/App/" + partner.partnerAppId,
|
|
7109
|
+
first: 10,
|
|
7110
|
+
occurredAtMin: ledgerFloor.toISOString(),
|
|
7111
|
+
organizationId: partner.partnerOrgId,
|
|
7112
|
+
partnerToken: partner.partnerToken,
|
|
7113
|
+
shopId: "gid://shopify/Shop/" + shopId
|
|
7114
|
+
});
|
|
7115
|
+
const boundaries = (accruals || []).filter((event) => event.occurredAt).sort((a, b) => new Date(a.occurredAt) - new Date(b.occurredAt));
|
|
7116
|
+
const accrued = [...boundaries].reverse().find((event) => Number(event.usageQuantity) > 0);
|
|
7117
|
+
if (accrued) metered = String(accrued.chargeId || accrued.id);
|
|
7118
|
+
return { boundaries, ledger: true, metered };
|
|
7119
|
+
};
|
|
7120
|
+
var toVariant = (edge) => {
|
|
7121
|
+
var _a, _b, _c, _d;
|
|
7122
|
+
return {
|
|
7123
|
+
availableForSale: edge.node.availableForSale || false,
|
|
7124
|
+
currency: ((_a = edge.node.price) == null ? void 0 : _a.currencyCode) || null,
|
|
7125
|
+
id: edge.node.id,
|
|
7126
|
+
image: ((_b = edge.node.image) == null ? void 0 : _b.url) || null,
|
|
7127
|
+
price: parseFloat((_c = edge.node.price) == null ? void 0 : _c.amount) || null,
|
|
7128
|
+
requiresShipping: edge.node.requiresShipping || false,
|
|
7129
|
+
sale: parseFloat((_d = edge.node.compareAtPrice) == null ? void 0 : _d.amount) || null,
|
|
7130
|
+
title: edge.node.title !== "Default Title" ? edge.node.title : null
|
|
7131
|
+
};
|
|
7132
|
+
};
|
|
7133
|
+
var applyOutOfStock = async ({ domain, shopify, storefrontAccessToken, variants }) => {
|
|
7134
|
+
try {
|
|
7135
|
+
const outOfStock = new Set(await shopify.storefront.probeVariantsOutOfStock({
|
|
7136
|
+
domain,
|
|
7137
|
+
storefrontAccessToken,
|
|
7138
|
+
variantIds: variants.map((variant) => variant.id)
|
|
7139
|
+
}));
|
|
7140
|
+
return variants.map((variant) => ({
|
|
7141
|
+
...variant,
|
|
7142
|
+
availableForSale: variant.availableForSale && !outOfStock.has(variant.id)
|
|
7143
|
+
}));
|
|
7144
|
+
} catch (_) {
|
|
7145
|
+
return variants;
|
|
7146
|
+
}
|
|
7147
|
+
};
|
|
7148
|
+
var sendFeedback = async ({ adminToken, connection: connection2, messages, productId, shopify, state }) => {
|
|
7149
|
+
try {
|
|
7150
|
+
await shopify.admin.sendProductResourceFeedback({
|
|
7151
|
+
adminAccessToken: await adminToken(),
|
|
7152
|
+
domain: connection2.shop,
|
|
7153
|
+
messages,
|
|
7154
|
+
productId,
|
|
7155
|
+
state
|
|
7156
|
+
});
|
|
7157
|
+
return null;
|
|
7158
|
+
} catch (error) {
|
|
7159
|
+
return error;
|
|
7160
|
+
}
|
|
7161
|
+
};
|
|
7162
|
+
var resolveProduct = async ({ connection: connection2, context, read }) => {
|
|
7163
|
+
const byId = (context == null ? void 0 : context.product) ? await read.get({ collection: "product", query: { id: context.product } }) : null;
|
|
7164
|
+
const row2 = byId || ((context == null ? void 0 : context.providerId) && ((context == null ? void 0 : context.shop) || (connection2 == null ? void 0 : connection2.shop)) ? await read.get({
|
|
7165
|
+
collection: "product",
|
|
7166
|
+
query: {
|
|
7167
|
+
"provider.id": context.providerId,
|
|
7168
|
+
"provider.slug": "shopify",
|
|
7169
|
+
"source.domain": context.shop || connection2.shop
|
|
7170
|
+
}
|
|
7171
|
+
}) : null);
|
|
7172
|
+
return row2;
|
|
7173
|
+
};
|
|
7174
|
+
var UNAVAILABLE_FEEDBACK = "Drawbridge can't display this product. Check it is active and available in the United States.";
|
|
7175
|
+
var NOT_FOUND = "Shopify product not found";
|
|
7176
|
+
var syncProduct = async ({ adminToken, connection: connection2, context, read, settings, shopify }) => {
|
|
7177
|
+
var _a, _b, _c;
|
|
7178
|
+
const row2 = await resolveProduct({ connection: connection2, context, read });
|
|
7179
|
+
if (!row2) return { message: "No product row to sync.", request: { product: (context == null ? void 0 : context.product) || null }, skipped: true };
|
|
7180
|
+
const domain = connection2.shop;
|
|
7181
|
+
const providerId = (_a = row2.provider) == null ? void 0 : _a.id;
|
|
7182
|
+
const request2 = { product: row2.id, providerId: providerId || null, shop: domain };
|
|
7183
|
+
let product;
|
|
7184
|
+
try {
|
|
7185
|
+
product = await shopify.storefront.getProduct({
|
|
7186
|
+
domain,
|
|
7187
|
+
productId: providerId,
|
|
7188
|
+
storefrontAccessToken: settings == null ? void 0 : settings.storefrontAccessToken
|
|
7189
|
+
});
|
|
7190
|
+
} catch (error) {
|
|
7191
|
+
if (error.message !== NOT_FOUND) throw error;
|
|
7192
|
+
const failed2 = await sendFeedback({
|
|
7193
|
+
adminToken,
|
|
7194
|
+
connection: connection2,
|
|
7195
|
+
messages: [UNAVAILABLE_FEEDBACK],
|
|
7196
|
+
productId: providerId,
|
|
7197
|
+
shopify,
|
|
7198
|
+
state: "REQUIRES_ACTION"
|
|
7199
|
+
});
|
|
7200
|
+
return {
|
|
7201
|
+
failed: failed2 ? [failed2.message] : void 0,
|
|
7202
|
+
message: "Storefront cannot see this product \u2014 deactivated.",
|
|
7203
|
+
request: request2,
|
|
7204
|
+
result: { status: "inactive" },
|
|
7205
|
+
writes: [
|
|
7206
|
+
{
|
|
7207
|
+
collection: "product",
|
|
7208
|
+
data: { $set: { status: "inactive" } },
|
|
7209
|
+
operation: "update",
|
|
7210
|
+
query: { id: row2.id }
|
|
7211
|
+
},
|
|
7212
|
+
{
|
|
7213
|
+
collection: "advertisement",
|
|
7214
|
+
data: { $set: { status: "drafted" } },
|
|
7215
|
+
multiple: true,
|
|
7216
|
+
operation: "update",
|
|
7217
|
+
query: { product: row2.id }
|
|
7218
|
+
}
|
|
7219
|
+
]
|
|
7220
|
+
};
|
|
7221
|
+
}
|
|
7222
|
+
const variants = await applyOutOfStock({
|
|
7223
|
+
domain,
|
|
7224
|
+
shopify,
|
|
7225
|
+
storefrontAccessToken: settings == null ? void 0 : settings.storefrontAccessToken,
|
|
7226
|
+
variants: (((_b = product == null ? void 0 : product.variants) == null ? void 0 : _b.edges) || []).map(toVariant)
|
|
7227
|
+
});
|
|
7228
|
+
const failed = await sendFeedback({
|
|
7229
|
+
adminToken,
|
|
7230
|
+
connection: connection2,
|
|
7231
|
+
messages: [],
|
|
7232
|
+
productId: providerId,
|
|
7233
|
+
shopify,
|
|
7234
|
+
state: "ACCEPTED"
|
|
7235
|
+
});
|
|
7236
|
+
return {
|
|
7237
|
+
// EVERYTHING PAST THE FIRST PAGE, filled in silently. The first page is
|
|
7238
|
+
// written above; a product with thousands of variants becomes N short
|
|
7239
|
+
// sequential jobs rather than one long loop that risks a BullMQ stall.
|
|
7240
|
+
enqueues: [{
|
|
7241
|
+
data: { operation: "variants", product: row2.id },
|
|
7242
|
+
name: "sync",
|
|
7243
|
+
options: { jobId: "product.shopify.variants." + row2.id + "." + Date.now() },
|
|
7244
|
+
queue: "product.shopify.variants"
|
|
7245
|
+
}],
|
|
7246
|
+
failed: failed ? [failed.message] : void 0,
|
|
7247
|
+
message: "Synced " + (product.title || "product") + ".",
|
|
7248
|
+
request: request2,
|
|
7249
|
+
result: { status: "active", variants: variants.length },
|
|
7250
|
+
writes: [{
|
|
7251
|
+
collection: "product",
|
|
7252
|
+
data: {
|
|
7253
|
+
$set: {
|
|
7254
|
+
description: product.description || null,
|
|
7255
|
+
image: ((_c = product.featuredImage) == null ? void 0 : _c.url) || null,
|
|
7256
|
+
// A successful sync PROVES channel visibility, which reverses the
|
|
7257
|
+
// deactivation above. Ads stay drafted.
|
|
7258
|
+
status: "active",
|
|
7259
|
+
title: product.title || null,
|
|
7260
|
+
type: product.productType || null,
|
|
7261
|
+
url: domain && product.handle ? "https://" + domain + "/products/" + product.handle : null,
|
|
7262
|
+
variants
|
|
7263
|
+
}
|
|
7264
|
+
},
|
|
7265
|
+
operation: "update",
|
|
7266
|
+
query: { id: row2.id }
|
|
7267
|
+
}]
|
|
7268
|
+
};
|
|
7269
|
+
};
|
|
7270
|
+
var syncVariants = async ({ connection: connection2, context, read, settings, shopify }) => {
|
|
7271
|
+
var _a, _b, _c;
|
|
7272
|
+
const row2 = await resolveProduct({ connection: connection2, context, read });
|
|
7273
|
+
if (!row2) return { message: "No product row to paginate.", request: { product: (context == null ? void 0 : context.product) || null }, skipped: true };
|
|
7274
|
+
const cursor = (context == null ? void 0 : context.cursor) || null;
|
|
7275
|
+
const request2 = { cursor, product: row2.id };
|
|
7276
|
+
let page2;
|
|
7277
|
+
try {
|
|
7278
|
+
page2 = await shopify.storefront.getProductVariantsPage({
|
|
7279
|
+
cursor,
|
|
7280
|
+
domain: connection2.shop,
|
|
7281
|
+
productId: (_a = row2.provider) == null ? void 0 : _a.id,
|
|
7282
|
+
storefrontAccessToken: settings == null ? void 0 : settings.storefrontAccessToken
|
|
7283
|
+
});
|
|
7284
|
+
} catch (error) {
|
|
7285
|
+
if (error.message !== NOT_FOUND) throw error;
|
|
7286
|
+
return { message: "Storefront lost this product mid-pagination \u2014 stopping.", request: request2, skipped: true };
|
|
7287
|
+
}
|
|
7288
|
+
const variants = await applyOutOfStock({
|
|
7289
|
+
domain: connection2.shop,
|
|
7290
|
+
shopify,
|
|
7291
|
+
storefrontAccessToken: settings == null ? void 0 : settings.storefrontAccessToken,
|
|
7292
|
+
variants: (page2.edges || []).map(toVariant)
|
|
7293
|
+
});
|
|
7294
|
+
return {
|
|
7295
|
+
...((_b = page2.pageInfo) == null ? void 0 : _b.hasNextPage) && { enqueues: [{
|
|
7296
|
+
data: { cursor: page2.pageInfo.endCursor, operation: "variants", product: row2.id },
|
|
7297
|
+
name: "page",
|
|
7298
|
+
options: { jobId: "product.shopify.variants." + row2.id + "." + Date.now() },
|
|
7299
|
+
queue: "product.shopify.variants"
|
|
7300
|
+
}] },
|
|
7301
|
+
message: variants.length + " variant(s) on this page.",
|
|
7302
|
+
request: request2,
|
|
7303
|
+
result: { hasNextPage: Boolean((_c = page2.pageInfo) == null ? void 0 : _c.hasNextPage), variants: variants.length },
|
|
7304
|
+
writes: [{
|
|
7305
|
+
collection: "product",
|
|
7306
|
+
data: cursor ? { $push: { variants: { $each: variants } } } : { $set: { variants } },
|
|
7307
|
+
operation: "update",
|
|
7308
|
+
query: { id: row2.id }
|
|
7309
|
+
}]
|
|
7310
|
+
};
|
|
7311
|
+
};
|
|
7312
|
+
var shopify_default2 = {
|
|
7313
|
+
// THE WORDS ON THE BUTTONS. `listing` was `content.redirect.title`, which put
|
|
7314
|
+
// a BUTTON LABEL inside the copy object next to a url — the title and the
|
|
7315
|
+
// address it belongs to sat at different levels and could drift apart.
|
|
7316
|
+
//
|
|
7317
|
+
// It names where the link GOES rather than what it does: installing happens on
|
|
7318
|
+
// the App Store listing, and the dashboard must never imply a store can be
|
|
7319
|
+
// linked from inside it.
|
|
7320
|
+
actions: {
|
|
7321
|
+
create: null,
|
|
7322
|
+
listing: "View on the Shopify App Store",
|
|
7323
|
+
manage: null,
|
|
7324
|
+
update: null
|
|
7325
|
+
},
|
|
7326
|
+
// INSTALL, not oauth. The grant is an OAuth grant, but the merchant never
|
|
7327
|
+
// sees a consent screen we sent them to -- they start at the App Store, and
|
|
7328
|
+
// the install completes inside Shopify admin without redirecting back. A
|
|
7329
|
+
// Connect button here would be lying about where connecting happens.
|
|
7330
|
+
auth: {
|
|
7331
|
+
// ONE STORE PER ORGANIZATION. A Shopify install IS a store, and a second
|
|
7332
|
+
// one would give an organization two catalogues, two order streams and two
|
|
7333
|
+
// usage meters with nothing to say which is authoritative.
|
|
7334
|
+
//
|
|
7335
|
+
// DECLARED AND ENFORCED BY NOTHING, measured 2026-09-13: no repo reads this
|
|
7336
|
+
// key, and the connection collection's { organization, slug } index is not
|
|
7337
|
+
// unique. So a second Shopify connection would be accepted today. The gate
|
|
7338
|
+
// belongs at the connect route with a unique partial index behind it, and
|
|
7339
|
+
// that is api work — this is the declaration it will read.
|
|
5291
7340
|
exclusive: true,
|
|
5292
7341
|
generated: false,
|
|
5293
7342
|
// THE MERCHANT STARTS AT SHOPIFY, not here. They install the app from
|
|
@@ -5446,8 +7495,8 @@ var shopify_default2 = {
|
|
|
5446
7495
|
// string; this answers what is missing from it.
|
|
5447
7496
|
//
|
|
5448
7497
|
// `shopify` is injected for the same reason it is everywhere else — this
|
|
5449
|
-
//
|
|
5450
|
-
scopes: ({ scope }, { shopify } = {}) => ({ result: { missing: scope ? shopify.oauth.missingScopes(scope) : null } }),
|
|
7498
|
+
// client is imported here and defaulted, never wired in by a caller.
|
|
7499
|
+
scopes: ({ scope }, { shopify = client } = {}) => ({ result: { missing: scope ? shopify.oauth.missingScopes(scope) : null } }),
|
|
5451
7500
|
// Shopify's install grant is exchanged inside its own app flow, not
|
|
5452
7501
|
// through the shared OAuth runner.
|
|
5453
7502
|
token: false
|
|
@@ -5461,17 +7510,53 @@ var shopify_default2 = {
|
|
|
5461
7510
|
// can reference another), `dispatch` (the caller's own coordinator table,
|
|
5462
7511
|
// for the hooks that are dispatches).
|
|
5463
7512
|
commerce: {
|
|
5464
|
-
//
|
|
5465
|
-
//
|
|
5466
|
-
//
|
|
5467
|
-
//
|
|
5468
|
-
//
|
|
5469
|
-
//
|
|
5470
|
-
|
|
7513
|
+
// THE CHECKOUT KIT CART. Six operations that were six direct calls into
|
|
7514
|
+
// the storefront client from api/route/subdomain.js.
|
|
7515
|
+
//
|
|
7516
|
+
// IT WRITES NOTHING, which is exactly why it was the last surface to
|
|
7517
|
+
// become a hook — a hook is usually reached to have its effects performed
|
|
7518
|
+
// and this one has none. That reasoning was wrong: a hook is also the one
|
|
7519
|
+
// place a vendor's client is allowed to be called from, and leaving the
|
|
7520
|
+
// cart out meant a public route importing a vendor SDK directly.
|
|
7521
|
+
//
|
|
7522
|
+
// ONE HOOK, OPERATION IN THE CONTEXT, the same shape `inbound.process`
|
|
7523
|
+
// uses for its topic table. Six slots would be six names in the closed
|
|
7524
|
+
// vocabulary for one question — what does this shopper's cart look like
|
|
7525
|
+
// now — asked six ways.
|
|
7526
|
+
//
|
|
7527
|
+
// A USER ERROR IS A 400 AND THE SHOPPER'S OWN WORDS. Shopify's cart
|
|
7528
|
+
// mutations answer "that variant is sold out" as a userError rather than a
|
|
7529
|
+
// transport failure, and the route it replaced already told the two apart.
|
|
7530
|
+
// runHook carries `status` off a thrown error, so it survives to the
|
|
7531
|
+
// caller; the rest of a thrown error's shape does not.
|
|
7532
|
+
cart: async ({ clientId, clientSecret, context, settings }, { fetcher, shopify = client } = {}) => {
|
|
7533
|
+
const { cartId, lineIds, lines, operation } = context || {};
|
|
7534
|
+
const { domain, storefrontAccessToken } = settings || {};
|
|
7535
|
+
if (operation === "checkout") {
|
|
7536
|
+
const token = await shopify.storefront.createCheckoutToken({ clientId, clientSecret, fetcher });
|
|
7537
|
+
return { message: "Checkout token minted.", result: { token } };
|
|
7538
|
+
}
|
|
7539
|
+
const storefront = { domain, fetcher, storefrontAccessToken };
|
|
7540
|
+
try {
|
|
7541
|
+
const cart = await ({
|
|
7542
|
+
create: () => shopify.storefront.cartCreate({ ...storefront, lines }),
|
|
7543
|
+
get: () => shopify.storefront.cartGet({ ...storefront, cartId }),
|
|
7544
|
+
linesAdd: () => shopify.storefront.cartLinesAdd({ ...storefront, cartId, lines }),
|
|
7545
|
+
linesRemove: () => shopify.storefront.cartLinesRemove({ ...storefront, cartId, lineIds }),
|
|
7546
|
+
linesUpdate: () => shopify.storefront.cartLinesUpdate({ ...storefront, cartId, lines })
|
|
7547
|
+
}[operation] || (() => {
|
|
7548
|
+
throw Object.assign(new Error("Unknown cart operation: " + operation), { status: 400 });
|
|
7549
|
+
}))();
|
|
7550
|
+
return { message: "Cart " + operation + ".", request: { cartId: cartId || null, operation }, result: cart };
|
|
7551
|
+
} catch (error) {
|
|
7552
|
+
if (error == null ? void 0 : error.userError) throw Object.assign(error, { status: 400 });
|
|
7553
|
+
throw error;
|
|
7554
|
+
}
|
|
7555
|
+
},
|
|
5471
7556
|
// MINT A DISCOUNT CODE against the merchant's chosen discount, mapped to
|
|
5472
7557
|
// one lead — which is what lets an order that redeems it be attributed
|
|
5473
7558
|
// back.
|
|
5474
|
-
code: async ({ connection: connection2, context, step }, { adminToken, shopify } = {}) => {
|
|
7559
|
+
code: async ({ connection: connection2, context, step }, { adminToken, shopify = client } = {}) => {
|
|
5475
7560
|
var _a;
|
|
5476
7561
|
const discount = (_a = step.settings) == null ? void 0 : _a.discount;
|
|
5477
7562
|
const request2 = { email: (context == null ? void 0 : context.email) || null, lead: (context == null ? void 0 : context.lead) || null, shop: connection2.shop };
|
|
@@ -5517,7 +7602,7 @@ var shopify_default2 = {
|
|
|
5517
7602
|
// customer at the store is a support ticket: the context may already
|
|
5518
7603
|
// carry the id from an earlier step, the lead may already be linked from
|
|
5519
7604
|
// an earlier run, and Shopify's own get-or-create settles the rest.
|
|
5520
|
-
customer: async ({ connection: connection2, context }, { adminToken, read, shopify } = {}) => {
|
|
7605
|
+
customer: async ({ connection: connection2, context }, { adminToken, read, shopify = client } = {}) => {
|
|
5521
7606
|
const request2 = { email: (context == null ? void 0 : context.email) || null, lead: (context == null ? void 0 : context.lead) || null, shop: connection2.shop };
|
|
5522
7607
|
if (!(context == null ? void 0 : context.email)) return { message: "Lead email is missing \u2014 cannot create Shopify customer.", request: request2, response: { skipped: true }, skipped: true };
|
|
5523
7608
|
if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing \u2014 cannot create Shopify customer.", request: request2, response: { skipped: true }, skipped: true };
|
|
@@ -5567,6 +7652,30 @@ var shopify_default2 = {
|
|
|
5567
7652
|
}]
|
|
5568
7653
|
};
|
|
5569
7654
|
},
|
|
7655
|
+
// HOW MANY ARE LEFT. Read at the moment a campaign's product list is
|
|
7656
|
+
// rendered rather than stored on the product, because stock is the one
|
|
7657
|
+
// product fact that is stale the instant it is written down.
|
|
7658
|
+
//
|
|
7659
|
+
// SUMMED HERE, not by the caller. Shopify answers per variant and every
|
|
7660
|
+
// caller wanted the total; the one that existed summed it inline, and
|
|
7661
|
+
// summing `Object.values()` over the raw map is how a cache wrapper's own
|
|
7662
|
+
// bookkeeping key got counted as a quantity.
|
|
7663
|
+
inventory: async ({ connection: connection2, context }, { adminToken, shopify = client } = {}) => {
|
|
7664
|
+
const productId = context == null ? void 0 : context.product;
|
|
7665
|
+
const request2 = { product: productId || null, shop: connection2.shop };
|
|
7666
|
+
if (!productId) return { message: "No product id \u2014 nothing to count.", request: request2, result: { total: null }, skipped: true };
|
|
7667
|
+
const adminAccessToken = await adminToken();
|
|
7668
|
+
const byVariant = await shopify.admin.getProductInventory({
|
|
7669
|
+
adminAccessToken,
|
|
7670
|
+
domain: connection2.shop,
|
|
7671
|
+
productId
|
|
7672
|
+
});
|
|
7673
|
+
return {
|
|
7674
|
+
message: "Inventory read.",
|
|
7675
|
+
request: request2,
|
|
7676
|
+
result: { total: Object.values(byVariant || {}).reduce((sum, quantity) => sum + quantity, 0) }
|
|
7677
|
+
};
|
|
7678
|
+
},
|
|
5570
7679
|
// AN ORDER ARRIVED AT THE STORE. The largest hook in the family, because
|
|
5571
7680
|
// attribution genuinely is: an order can reach Drawbridge two ways and
|
|
5572
7681
|
// they bill differently.
|
|
@@ -5887,6 +7996,12 @@ var shopify_default2 = {
|
|
|
5887
7996
|
orderId: String(orderId),
|
|
5888
7997
|
rate,
|
|
5889
7998
|
shopId: connection2.source.id,
|
|
7999
|
+
// WHICH VENDOR IS BEING CHARGED, so the sender does not have
|
|
8000
|
+
// to assume. queue/usage.js named the slug itself and
|
|
8001
|
+
// defaults to it for anything already queued when this
|
|
8002
|
+
// shipped — an app-store vendor billing through its own meter
|
|
8003
|
+
// enqueues the same job with its own slug.
|
|
8004
|
+
slug: connection2.slug,
|
|
5890
8005
|
// The App Events API returns no event id, so one is generated
|
|
5891
8006
|
// here — the event handle plus the order id — and sent as the
|
|
5892
8007
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
@@ -5926,10 +8041,28 @@ var shopify_default2 = {
|
|
|
5926
8041
|
//
|
|
5927
8042
|
// The shell has already refused a missing or inactive Shopify connection,
|
|
5928
8043
|
// so what is left is the two things only this hook can know are wrong.
|
|
5929
|
-
|
|
8044
|
+
// A PRODUCT, IN THREE MOVES. Nested by operation rather than by three
|
|
8045
|
+
// slots in the closed vocabulary, the same shape inbound.process uses
|
|
8046
|
+
// for its topic table — they are one subject, and a vendor that syncs
|
|
8047
|
+
// products has to answer all three or none.
|
|
8048
|
+
//
|
|
8049
|
+
// record a webhook said a product changed: upsert the row and
|
|
8050
|
+
// queue the pull
|
|
8051
|
+
// sync pull the product from the storefront and write what it
|
|
8052
|
+
// says, including what it says by NOT answering
|
|
8053
|
+
// variants one page of variants, self-chaining
|
|
8054
|
+
//
|
|
8055
|
+
// `sync` and `variants` were 443 lines of drawbridge-sync's
|
|
8056
|
+
// queue/product.js — a shadow implementation of the step this manifest
|
|
8057
|
+
// already declared, importing three of the vendor's client modules from
|
|
8058
|
+
// a worker file.
|
|
8059
|
+
product: async ({ connection: connection2, context, settings, workflow }, { adminToken, mintId, read, shopify = client } = {}) => {
|
|
8060
|
+
const operation = (context == null ? void 0 : context.operation) || "record";
|
|
8061
|
+
if (operation === "sync") return syncProduct({ adminToken, connection: connection2, context, read, settings, shopify });
|
|
8062
|
+
if (operation === "variants") return syncVariants({ connection: connection2, context, read, settings, shopify });
|
|
5930
8063
|
const request2 = {
|
|
5931
8064
|
numericId: (context == null ? void 0 : context.id) || null,
|
|
5932
|
-
organizationId: workflow.organization,
|
|
8065
|
+
organizationId: (workflow == null ? void 0 : workflow.organization) || null,
|
|
5933
8066
|
title: (context == null ? void 0 : context.title) || null
|
|
5934
8067
|
};
|
|
5935
8068
|
if (!(context == null ? void 0 : context.id)) return { message: "Skipped \u2014 product webhook payload had no id.", request: request2, response: { skipped: true }, skipped: true };
|
|
@@ -5979,9 +8112,612 @@ var shopify_default2 = {
|
|
|
5979
8112
|
}
|
|
5980
8113
|
},
|
|
5981
8114
|
contacts: { remove: false, sync: false },
|
|
5982
|
-
//
|
|
5983
|
-
// own app store has one
|
|
5984
|
-
|
|
8115
|
+
// THE EMBEDDED-APP SURFACE. Only a vendor whose merchants arrive from its
|
|
8116
|
+
// own app store has one, and these thirteen slots are the thirteen routes
|
|
8117
|
+
// that used to live in api/route/shopify-embedded.js.
|
|
8118
|
+
//
|
|
8119
|
+
// A HOOK DESCRIBES ITS WRITES and the shell performs them, so none of these
|
|
8120
|
+
// holds a controller — `read` is the controller's read methods and nothing
|
|
8121
|
+
// else. Where one REFUSES it throws with a status, and the route answers
|
|
8122
|
+
// what it said. The helpers they share are above the manifest.
|
|
8123
|
+
install: {
|
|
8124
|
+
account: {
|
|
8125
|
+
// ESTABLISH THE DURABLE MERCHANT-TO-STORE LINK. The breakout
|
|
8126
|
+
// authenticates the merchant once, top-level, and stashes their user
|
|
8127
|
+
// token in `install.token.save`; App Bridge has already proven shop
|
|
8128
|
+
// control by the time this runs.
|
|
8129
|
+
//
|
|
8130
|
+
// THE STASHED TOKEN IS THE TRUST ANCHOR, not merely any valid user
|
|
8131
|
+
// token: the presented one must BE this shop's one-shot handoff token,
|
|
8132
|
+
// unexpired, and an access or pat record — a refresh or oauth-state
|
|
8133
|
+
// token that happens to resolve to a user must not qualify, which is
|
|
8134
|
+
// the rule a session is held to in drawbridge-api's middleware.
|
|
8135
|
+
link: async ({ context }, { read } = {}) => {
|
|
8136
|
+
var _a;
|
|
8137
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8138
|
+
const token = context == null ? void 0 : context.token;
|
|
8139
|
+
if (!shop || !token) throw refusal("shop and token are required", 400);
|
|
8140
|
+
const now = /* @__PURE__ */ new Date();
|
|
8141
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
8142
|
+
const stashed = (record == null ? void 0 : record.linkToken) ? (_a = decrypt(record.linkToken)) == null ? void 0 : _a.token : null;
|
|
8143
|
+
const expired = !(record == null ? void 0 : record.linkTokenExpiresAt) || new Date(record.linkTokenExpiresAt) <= now;
|
|
8144
|
+
if (!stashed || expired || stashed !== token) throw refusal("Invalid or expired link token", 401);
|
|
8145
|
+
const granted = await read.get({
|
|
8146
|
+
collection: "token",
|
|
8147
|
+
query: {
|
|
8148
|
+
revoked: false,
|
|
8149
|
+
tokenHash: hashToken(token),
|
|
8150
|
+
type: { $in: [tokenTypes.access, tokenTypes.pat] }
|
|
8151
|
+
}
|
|
8152
|
+
});
|
|
8153
|
+
if (!(granted == null ? void 0 : granted.user) || granted.expiresAt && new Date(granted.expiresAt) <= now) {
|
|
8154
|
+
throw refusal("Invalid or expired link token", 401);
|
|
8155
|
+
}
|
|
8156
|
+
const switching = Boolean(record == null ? void 0 : record.user) && record.user !== granted.user;
|
|
8157
|
+
const connections2 = switching ? await read.aggregate({
|
|
8158
|
+
collection: "connection",
|
|
8159
|
+
pipeline: [
|
|
8160
|
+
{ $match: { shop, slug: "shopify" } },
|
|
8161
|
+
{ $project: { _id: 0, id: 1 } }
|
|
8162
|
+
]
|
|
8163
|
+
}) : [];
|
|
8164
|
+
return {
|
|
8165
|
+
message: switching ? "Linked to a different Drawbridge account." : "Linked.",
|
|
8166
|
+
request: { shop },
|
|
8167
|
+
result: { connected: true, switched: switching },
|
|
8168
|
+
// ALL OR NONE. A half-done switch is a store linked to the new
|
|
8169
|
+
// account while the old account's connections still serve from it.
|
|
8170
|
+
transaction: true,
|
|
8171
|
+
writes: [
|
|
8172
|
+
...(connections2 || []).map((connection2) => ({
|
|
8173
|
+
collection: "connection",
|
|
8174
|
+
operation: "delete",
|
|
8175
|
+
query: { id: connection2.id }
|
|
8176
|
+
})),
|
|
8177
|
+
{
|
|
8178
|
+
collection: "shop",
|
|
8179
|
+
data: { $set: { linkToken: null, linkTokenExpiresAt: null, user: granted.user } },
|
|
8180
|
+
operation: "update",
|
|
8181
|
+
query: { shop }
|
|
8182
|
+
}
|
|
8183
|
+
]
|
|
8184
|
+
};
|
|
8185
|
+
},
|
|
8186
|
+
// DISCONNECT THE ACCOUNT FROM THE STORE. Every org connection goes and
|
|
8187
|
+
// the durable link clears; the offline token and the shop record stay,
|
|
8188
|
+
// because the app is still installed and the merchant may link a
|
|
8189
|
+
// different account next.
|
|
8190
|
+
unlink: async ({ context }, { read } = {}) => {
|
|
8191
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8192
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8193
|
+
const connections2 = await read.aggregate({
|
|
8194
|
+
collection: "connection",
|
|
8195
|
+
pipeline: [
|
|
8196
|
+
{ $match: { shop, slug: "shopify" } },
|
|
8197
|
+
{ $project: { _id: 0, id: 1 } }
|
|
8198
|
+
]
|
|
8199
|
+
});
|
|
8200
|
+
return {
|
|
8201
|
+
message: "Unlinked " + (connections2 || []).length + " connection(s).",
|
|
8202
|
+
request: { shop },
|
|
8203
|
+
result: { ok: true },
|
|
8204
|
+
transaction: true,
|
|
8205
|
+
writes: [
|
|
8206
|
+
...(connections2 || []).map((connection2) => ({
|
|
8207
|
+
collection: "connection",
|
|
8208
|
+
operation: "delete",
|
|
8209
|
+
query: { id: connection2.id }
|
|
8210
|
+
})),
|
|
8211
|
+
{
|
|
8212
|
+
collection: "shop",
|
|
8213
|
+
data: { $set: { linkToken: null, linkTokenExpiresAt: null, user: null } },
|
|
8214
|
+
operation: "update",
|
|
8215
|
+
query: { shop }
|
|
8216
|
+
}
|
|
8217
|
+
]
|
|
8218
|
+
};
|
|
8219
|
+
}
|
|
8220
|
+
},
|
|
8221
|
+
organizations: {
|
|
8222
|
+
// BIND ONE ORGANIZATION TO THIS STORE. The longest slot here, and every
|
|
8223
|
+
// gate in it came from a line in the route it replaces.
|
|
8224
|
+
add: async ({ context }, { currencies: currencies2, mintId, read, shopify = client } = {}) => {
|
|
8225
|
+
var _a, _b;
|
|
8226
|
+
const organization2 = context == null ? void 0 : context.organization;
|
|
8227
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8228
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8229
|
+
if (!organization2) throw refusal("organization is required", 400);
|
|
8230
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
8231
|
+
if (!(record == null ? void 0 : record.user)) throw refusal("Account is not connected for this store", 409);
|
|
8232
|
+
const owned = await merchantOrganizations({ read, user: record.user });
|
|
8233
|
+
const org = (owned || []).find((entry) => entry.id === organization2);
|
|
8234
|
+
if (!org) throw refusal("No access to this organization", 403);
|
|
8235
|
+
if (!CONNECTABLE_STATUSES.includes(org.billingStatus)) {
|
|
8236
|
+
throw refusal("This organization can't accept connections right now \u2014 check its billing status in the dashboard", 403);
|
|
8237
|
+
}
|
|
8238
|
+
if (org.connection && org.connection.shop !== shop) {
|
|
8239
|
+
throw refusal("This organization is already connected to another Shopify store", 409);
|
|
8240
|
+
}
|
|
8241
|
+
const settings = (record == null ? void 0 : record.settings) ? decrypt(record.settings) : null;
|
|
8242
|
+
if (!(settings == null ? void 0 : settings.accessToken)) {
|
|
8243
|
+
throw refusal("Store is not installed \u2014 open the app in the Shopify admin first", 409);
|
|
8244
|
+
}
|
|
8245
|
+
const plan = await storePlan({ record, shopify });
|
|
8246
|
+
if (plan.unavailable) throw refusal("Couldn't verify the store's plan \u2014 try again shortly", 502);
|
|
8247
|
+
if (!plan.active) throw refusal("Approve a plan before connecting organizations", 402);
|
|
8248
|
+
let storefront = null;
|
|
8249
|
+
if (!record.storefront) {
|
|
8250
|
+
try {
|
|
8251
|
+
const minted2 = await shopify.oauth.createStorefrontToken({
|
|
8252
|
+
adminAccessToken: settings.accessToken,
|
|
8253
|
+
shop
|
|
8254
|
+
});
|
|
8255
|
+
if (minted2) storefront = encrypt({ token: minted2 });
|
|
8256
|
+
} catch (_) {
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
let currency = (record == null ? void 0 : record.currency) || null;
|
|
8260
|
+
let source = (record == null ? void 0 : record.source) || null;
|
|
8261
|
+
try {
|
|
8262
|
+
const shopData = await shopify.oauth.getShop({ adminAccessToken: settings.accessToken, shop });
|
|
8263
|
+
if (shopData) {
|
|
8264
|
+
currency = String(shopData.currency || "").toLowerCase() || null;
|
|
8265
|
+
source = { domain: shopData.myshopifyDomain, id: String(shopData.id), label: shopData.name };
|
|
8266
|
+
}
|
|
8267
|
+
} catch (_) {
|
|
8268
|
+
}
|
|
8269
|
+
if (currency && currencies2 && !currencies2.includes(currency)) {
|
|
8270
|
+
throw refusal("This store settles in a currency we can't bill yet. Connect a store with a supported settlement currency.", 422);
|
|
8271
|
+
}
|
|
8272
|
+
const minted = ((_a = org.connection) == null ? void 0 : _a.id) ? null : mintId();
|
|
8273
|
+
const connectionId = ((_b = org.connection) == null ? void 0 : _b.id) || minted.id;
|
|
8274
|
+
return {
|
|
8275
|
+
message: "Connected " + org.name + " to " + shop + ".",
|
|
8276
|
+
request: { organization: organization2, shop },
|
|
8277
|
+
result: { ok: true },
|
|
8278
|
+
transaction: true,
|
|
8279
|
+
writes: [
|
|
8280
|
+
...storefront ? [{
|
|
8281
|
+
collection: "shop",
|
|
8282
|
+
data: { $set: { storefront } },
|
|
8283
|
+
operation: "update",
|
|
8284
|
+
query: { shop }
|
|
8285
|
+
}] : [],
|
|
8286
|
+
{
|
|
8287
|
+
collection: "connection",
|
|
8288
|
+
data: {
|
|
8289
|
+
$set: {
|
|
8290
|
+
currency,
|
|
8291
|
+
errors: [],
|
|
8292
|
+
// The token of record lives on the shared `shop` row; this
|
|
8293
|
+
// is the pointer that satisfies the required `settings`
|
|
8294
|
+
// string, and `auth.install.shared` declares how to read it.
|
|
8295
|
+
settings: encrypt({ ref: "shop", shop }),
|
|
8296
|
+
shop,
|
|
8297
|
+
status: "pending",
|
|
8298
|
+
...source && { source }
|
|
8299
|
+
},
|
|
8300
|
+
$setOnInsert: {
|
|
8301
|
+
...minted && { _id: minted._id, id: minted.id },
|
|
8302
|
+
feature: "organization:connection:shopify",
|
|
8303
|
+
organization: organization2,
|
|
8304
|
+
slug: "shopify"
|
|
8305
|
+
}
|
|
8306
|
+
},
|
|
8307
|
+
operation: "update",
|
|
8308
|
+
options: { upsert: true },
|
|
8309
|
+
query: { organization: organization2, slug: "shopify" }
|
|
8310
|
+
},
|
|
8311
|
+
{
|
|
8312
|
+
collection: "organization",
|
|
8313
|
+
data: {
|
|
8314
|
+
// $addToSet UNCONDITIONALLY. The route this replaces only
|
|
8315
|
+
// registered the refs when the upsert INSERTED, which meant an
|
|
8316
|
+
// org whose refs had drifted stayed drifted through every
|
|
8317
|
+
// re-link. Adding a member that is already there is a no-op,
|
|
8318
|
+
// so doing it every time costs nothing and repairs the drift.
|
|
8319
|
+
$addToSet: {
|
|
8320
|
+
"connections.groups": "ecommerce",
|
|
8321
|
+
"connections.ids": connectionId,
|
|
8322
|
+
"connections.keys": "shopify"
|
|
8323
|
+
},
|
|
8324
|
+
$set: { billingProvider: "shopify" }
|
|
8325
|
+
},
|
|
8326
|
+
operation: "update",
|
|
8327
|
+
query: { id: organization2 }
|
|
8328
|
+
}
|
|
8329
|
+
]
|
|
8330
|
+
};
|
|
8331
|
+
},
|
|
8332
|
+
// WHAT THIS STORE CAN SEE: the linked merchant's organizations, the
|
|
8333
|
+
// store's live plan, and the products the merchant has to act on.
|
|
8334
|
+
//
|
|
8335
|
+
// AN ORG CONNECTED TO A DIFFERENT STORE IS OMITTED ENTIRELY, because
|
|
8336
|
+
// `add` would refuse it — a list that offers what the next call rejects
|
|
8337
|
+
// is worse than a shorter one.
|
|
8338
|
+
list: async ({ context }, { conversionRate: conversionRate2, planTitle, read, shopify = client } = {}) => {
|
|
8339
|
+
var _a;
|
|
8340
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8341
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8342
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
8343
|
+
if (!(record == null ? void 0 : record.user)) {
|
|
8344
|
+
return {
|
|
8345
|
+
message: "No account linked to this store.",
|
|
8346
|
+
request: { shop },
|
|
8347
|
+
result: {
|
|
8348
|
+
account: null,
|
|
8349
|
+
connected: false,
|
|
8350
|
+
organizations: [],
|
|
8351
|
+
plan: { active: false, name: null },
|
|
8352
|
+
unavailableProducts: [],
|
|
8353
|
+
unavailableProductsTotal: 0
|
|
8354
|
+
}
|
|
8355
|
+
};
|
|
8356
|
+
}
|
|
8357
|
+
const owned = await merchantOrganizations({ read, user: record.user });
|
|
8358
|
+
const organizations = (owned || []).filter((org) => !org.connection || org.connection.shop === shop).map((org) => {
|
|
8359
|
+
var _a2;
|
|
8360
|
+
return {
|
|
8361
|
+
// The org's OWN billing status decides whether `add` accepts it.
|
|
8362
|
+
// Sent up so the app can disable Connect and name the reason
|
|
8363
|
+
// rather than letting the merchant click into a 403 —
|
|
8364
|
+
// listed-but-blocked beats hidden, because an org vanishing from
|
|
8365
|
+
// the list is indistinguishable from a bug.
|
|
8366
|
+
billingStatus: org.billingStatus || null,
|
|
8367
|
+
connectable: CONNECTABLE_STATUSES.includes(org.billingStatus),
|
|
8368
|
+
connected: Boolean(org.connection),
|
|
8369
|
+
conversion: conversionRate2(org.subscription),
|
|
8370
|
+
id: org.id,
|
|
8371
|
+
name: org.name,
|
|
8372
|
+
plan: planTitle(org.subscription),
|
|
8373
|
+
status: ((_a2 = org.connection) == null ? void 0 : _a2.status) || null
|
|
8374
|
+
};
|
|
8375
|
+
});
|
|
8376
|
+
const connections2 = (owned || []).filter((org) => {
|
|
8377
|
+
var _a2;
|
|
8378
|
+
return ((_a2 = org.connection) == null ? void 0 : _a2.shop) === shop && org.connection.id;
|
|
8379
|
+
}).map((org) => org.connection.id);
|
|
8380
|
+
const [account, plan, unavailable] = await Promise.all([
|
|
8381
|
+
read.get({ collection: "user", query: { id: record.user } }),
|
|
8382
|
+
storePlan({ record, shopify }),
|
|
8383
|
+
connections2.length ? read.aggregate({
|
|
8384
|
+
collection: "product",
|
|
8385
|
+
pipeline: [
|
|
8386
|
+
{ $match: { connections: { $in: connections2 }, "source.domain": shop, status: "inactive" } },
|
|
8387
|
+
// $facet so the app can name the first 25 AND report a
|
|
8388
|
+
// truthful total: listing 25 of 40 under "25 products need
|
|
8389
|
+
// attention" would understate the problem.
|
|
8390
|
+
{
|
|
8391
|
+
$facet: {
|
|
8392
|
+
items: [{ $sort: { title: 1 } }, { $limit: 25 }, { $project: { _id: 0, id: 1, title: 1 } }],
|
|
8393
|
+
total: [{ $count: "count" }]
|
|
8394
|
+
}
|
|
8395
|
+
}
|
|
8396
|
+
]
|
|
8397
|
+
}) : []
|
|
8398
|
+
]);
|
|
8399
|
+
const facet = (unavailable || [])[0] || {};
|
|
8400
|
+
if (plan.active && plan.metered !== true) {
|
|
8401
|
+
const stamped = await read.aggregate({
|
|
8402
|
+
collection: "connection",
|
|
8403
|
+
pipeline: [
|
|
8404
|
+
{ $match: { shop, slug: "shopify" } },
|
|
8405
|
+
{ $project: { _id: 0, "source.metered": 1 } }
|
|
8406
|
+
]
|
|
8407
|
+
});
|
|
8408
|
+
const verdicts = (stamped || []).map((row2) => {
|
|
8409
|
+
var _a2;
|
|
8410
|
+
return (_a2 = row2 == null ? void 0 : row2.source) == null ? void 0 : _a2.metered;
|
|
8411
|
+
});
|
|
8412
|
+
if (verdicts.some((verdict) => typeof verdict === "string")) plan.metered = true;
|
|
8413
|
+
else if (verdicts.some((verdict) => verdict === null)) plan.metered = false;
|
|
8414
|
+
}
|
|
8415
|
+
return {
|
|
8416
|
+
message: organizations.length + " organization(s) for this store.",
|
|
8417
|
+
request: { shop },
|
|
8418
|
+
result: {
|
|
8419
|
+
account: { email: (account == null ? void 0 : account.email) || null },
|
|
8420
|
+
connected: true,
|
|
8421
|
+
organizations,
|
|
8422
|
+
plan,
|
|
8423
|
+
unavailableProducts: (facet.items || []).map((product) => ({
|
|
8424
|
+
id: product.id,
|
|
8425
|
+
title: product.title || "Untitled product"
|
|
8426
|
+
})),
|
|
8427
|
+
// The total can exceed the 25 listed above, and has to.
|
|
8428
|
+
unavailableProductsTotal: ((_a = (facet.total || [])[0]) == null ? void 0 : _a.count) || 0
|
|
8429
|
+
}
|
|
8430
|
+
};
|
|
8431
|
+
},
|
|
8432
|
+
// DISCONNECT ONE ORGANIZATION. The connection is deleted and nothing
|
|
8433
|
+
// else: every dependent — products, ads, steps, workflows, and the org's
|
|
8434
|
+
// own connection refs — is torn down by drawbridge-sync's connection
|
|
8435
|
+
// change stream. The shared token and the other orgs are untouched.
|
|
8436
|
+
remove: async ({ context }, { read } = {}) => {
|
|
8437
|
+
const organization2 = context == null ? void 0 : context.organization;
|
|
8438
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8439
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8440
|
+
if (!organization2) throw refusal("organization is required", 400);
|
|
8441
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
8442
|
+
if (!(record == null ? void 0 : record.user)) throw refusal("Account is not connected for this store", 409);
|
|
8443
|
+
const owned = await merchantOrganizations({ read, user: record.user });
|
|
8444
|
+
if (!(owned || []).some((entry) => entry.id === organization2)) {
|
|
8445
|
+
throw refusal("No access to this organization", 403);
|
|
8446
|
+
}
|
|
8447
|
+
const connection2 = await read.get({
|
|
8448
|
+
collection: "connection",
|
|
8449
|
+
query: { organization: organization2, shop, slug: "shopify" }
|
|
8450
|
+
});
|
|
8451
|
+
if (!connection2) throw refusal("No connection to remove", 404);
|
|
8452
|
+
return {
|
|
8453
|
+
message: "Disconnected.",
|
|
8454
|
+
request: { organization: organization2, shop },
|
|
8455
|
+
result: { ok: true },
|
|
8456
|
+
writes: [{ collection: "connection", operation: "delete", query: { id: connection2.id } }]
|
|
8457
|
+
};
|
|
8458
|
+
}
|
|
8459
|
+
},
|
|
8460
|
+
plan: {
|
|
8461
|
+
// RELEASE EVERY ACTIVE APPROVAL. The App Home then re-checks the plan
|
|
8462
|
+
// and falls back to "Choose a plan".
|
|
8463
|
+
cancel: ({ context }, { read, shopify = client } = {}) => releaseApprovals({ context, read, shopify }),
|
|
8464
|
+
// ONE ATTRIBUTED ORDER'S USAGE CHARGE. Enqueued per conversion by the
|
|
8465
|
+
// order step and run on a retrying queue, so this may be called
|
|
8466
|
+
// several times for one order — which is safe because the idempotency
|
|
8467
|
+
// key is PERMANENT for billing events and Shopify settles the repeat.
|
|
8468
|
+
//
|
|
8469
|
+
// THE HANDLE IS CHECKED AGAINST THE MANIFEST'S OWN DECLARATION, which
|
|
8470
|
+
// is a real strengthening of what it replaced: drawbridge-sync compared
|
|
8471
|
+
// the job's `handle` against the job's `transaction` — two fields from
|
|
8472
|
+
// the same enqueuer, so a drifted enqueuer agreed with itself. The
|
|
8473
|
+
// handle is the one string that decides whether the event bills at all,
|
|
8474
|
+
// and this is the file that publishes it.
|
|
8475
|
+
//
|
|
8476
|
+
// REFUSE BEFORE SENDING, because sending burns the order's PERMANENT
|
|
8477
|
+
// idempotency key on an event that misclassifies or mistraces. The
|
|
8478
|
+
// throw rides the queue's normal retry path and the abandoned-job alert
|
|
8479
|
+
// pages on exhaustion.
|
|
8480
|
+
//
|
|
8481
|
+
// DELIBERATELY NO TIMESTAMP. sendAppEvent defaults to the SEND time,
|
|
8482
|
+
// which is always inside the merchant's current billing cycle at the
|
|
8483
|
+
// moment of sending and so can never hit PERIOD_CLOSED. Pinning the
|
|
8484
|
+
// purchase time reads more correct and is strictly worse: a retry
|
|
8485
|
+
// backoff crossing a cycle boundary would carry a closed-cycle
|
|
8486
|
+
// timestamp, Shopify would 202-accept, silently drop it, and the
|
|
8487
|
+
// commission would be lost. Send-time's worst case is benign — the
|
|
8488
|
+
// charge lands in the next cycle, but it lands.
|
|
8489
|
+
charge: async ({ clientId, clientSecret, context, manifest }, { shopify = client } = {}) => {
|
|
8490
|
+
const { idempotencyKey, order, orderId, rate, revision, shopId, timestamp, transaction, value } = context || {};
|
|
8491
|
+
const handle = (context == null ? void 0 : context.handle) || manifest.auth.install.billing.handle;
|
|
8492
|
+
if (!transaction || !String(transaction).startsWith(handle + ".")) {
|
|
8493
|
+
throw new Error("shopify.usage.billing.handle.mismatch on order " + orderId + ": transaction " + (transaction || "null") + " does not carry handle " + handle + " \u2014 refusing to send");
|
|
8494
|
+
}
|
|
8495
|
+
await shopify.billing.sendAppEvent({
|
|
8496
|
+
clientId,
|
|
8497
|
+
clientSecret,
|
|
8498
|
+
eventHandle: handle,
|
|
8499
|
+
idempotencyKey,
|
|
8500
|
+
// The Shopify-side event record names the order that produced it.
|
|
8501
|
+
reference: transaction,
|
|
8502
|
+
// OPERATOR-ONLY, BOTH OF THEM, and absent on every automatic
|
|
8503
|
+
// send. `revision` suffixes the idempotency key, which is what
|
|
8504
|
+
// makes a second attempt possible once a key has been consumed;
|
|
8505
|
+
// the encoding is owned by sendAppEvent so no caller builds keys
|
|
8506
|
+
// itself. `timestamp` pins the PURCHASE time, which the automatic
|
|
8507
|
+
// path must never do — see the note above — and the recovery
|
|
8508
|
+
// migration must, because it is resending orders from a closed
|
|
8509
|
+
// window and bounds its own run to the merchant's current cycle.
|
|
8510
|
+
...revision && { revision },
|
|
8511
|
+
...timestamp && { timestamp },
|
|
8512
|
+
shopId,
|
|
8513
|
+
value
|
|
8514
|
+
});
|
|
8515
|
+
return {
|
|
8516
|
+
message: "Usage charge sent for order " + orderId + ".",
|
|
8517
|
+
request: { order: order || null, orderId: orderId || null, shopId: shopId || null, value },
|
|
8518
|
+
result: { transaction: String(transaction) },
|
|
8519
|
+
// TRACEABILITY. The App Events API returns no event id, so the
|
|
8520
|
+
// trace key into Shopify's event logs is the transaction id the
|
|
8521
|
+
// commerce.order hook minted and this sent as `reference`.
|
|
8522
|
+
// `amount` is the per-order usage value in cents, mirroring the
|
|
8523
|
+
// Stripe path's marker.
|
|
8524
|
+
...order && { writes: [{
|
|
8525
|
+
collection: "order",
|
|
8526
|
+
data: {
|
|
8527
|
+
$set: {
|
|
8528
|
+
billed: {
|
|
8529
|
+
amount: value,
|
|
8530
|
+
date: /* @__PURE__ */ new Date(),
|
|
8531
|
+
rate: rate || 0,
|
|
8532
|
+
transaction: String(transaction)
|
|
8533
|
+
}
|
|
8534
|
+
}
|
|
8535
|
+
},
|
|
8536
|
+
operation: "update",
|
|
8537
|
+
query: { id: order }
|
|
8538
|
+
}] }
|
|
8539
|
+
};
|
|
8540
|
+
},
|
|
8541
|
+
// RESELECT, NOT CANCEL. The same mechanics — the current approval has to
|
|
8542
|
+
// be released before the pricing page will offer the plan again — but
|
|
8543
|
+
// the intent is a thirty-second round trip rather than a departure.
|
|
8544
|
+
//
|
|
8545
|
+
// `mark` IS A DECLARED CAPABILITY, and this is the only slot here that
|
|
8546
|
+
// needs one. The marker has to be persisted BEFORE the approval is
|
|
8547
|
+
// released: it is what tells sync's CANCELLED handler to hold the pause
|
|
8548
|
+
// — no connection error, no owner email — and start a timed recheck that
|
|
8549
|
+
// pauses honestly if the merchant abandons. A described write lands
|
|
8550
|
+
// after the hook returns, which is after the cancel, and a marker
|
|
8551
|
+
// written then can race the very webhook it exists to soften. Same
|
|
8552
|
+
// precedent as `resolveContact`: the one write a description cannot
|
|
8553
|
+
// carry.
|
|
8554
|
+
reselect: async ({ context }, { mark, read, shopify = client } = {}) => {
|
|
8555
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8556
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8557
|
+
await mark({ shop });
|
|
8558
|
+
return releaseApprovals({ context, read, shopify });
|
|
8559
|
+
},
|
|
8560
|
+
// IS THIS STORE'S ORDER BILLING ACTUALLY METERED? Not a merchant
|
|
8561
|
+
// action — the other two slots here are — but the same subject, and
|
|
8562
|
+
// the reason it is a declared hook rather than a helper is that the
|
|
8563
|
+
// answer was being derived in two places: `lifecycle.health` below,
|
|
8564
|
+
// and drawbridge-sync's app_subscriptions/update handler, whose copy
|
|
8565
|
+
// stated in its own comment that it matched this one and did not.
|
|
8566
|
+
//
|
|
8567
|
+
// THREE ANSWERS, and the caller must keep them apart:
|
|
8568
|
+
//
|
|
8569
|
+
// a string PROVEN. The charge id or usage line id, stamped on
|
|
8570
|
+
// the connection as the evidence.
|
|
8571
|
+
// null PROBED, and nothing is accruing.
|
|
8572
|
+
// undefined NO VERDICT — the probe could not run. `probed` says
|
|
8573
|
+
// which, because undefined does not survive JSON.
|
|
8574
|
+
verify: async ({ context }, { adminToken, read, shopify = client } = {}) => {
|
|
8575
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8576
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8577
|
+
const adminAccessToken = await adminToken();
|
|
8578
|
+
if (!adminAccessToken) {
|
|
8579
|
+
return { message: "No admin token for this store \u2014 no verdict.", request: { shop }, result: { metered: null, probed: false }, skipped: true };
|
|
8580
|
+
}
|
|
8581
|
+
const partnerRow = (read == null ? void 0 : read.get) ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
8582
|
+
const partner = (partnerRow == null ? void 0 : partnerRow.settings) ? decrypt(partnerRow.settings) : {};
|
|
8583
|
+
const { ledger, metered } = await meterVerdict({
|
|
8584
|
+
adminAccessToken,
|
|
8585
|
+
ledgerFloor: new Date(Date.now() - LEDGER_WINDOW_MS),
|
|
8586
|
+
partner,
|
|
8587
|
+
shop,
|
|
8588
|
+
shopId: (context == null ? void 0 : context.shopId) || null,
|
|
8589
|
+
shopify
|
|
8590
|
+
});
|
|
8591
|
+
return {
|
|
8592
|
+
message: typeof metered === "string" ? "Billing is metered." : "No meter evidence for this store.",
|
|
8593
|
+
request: { shop },
|
|
8594
|
+
// `probed` is the LEDGER, not the call: a store whose Partner
|
|
8595
|
+
// credentials are absent was not asked, and reporting that as
|
|
8596
|
+
// "probed, nothing accruing" would stamp an unmetered verdict on
|
|
8597
|
+
// a store nobody looked at.
|
|
8598
|
+
result: { metered: metered ?? null, probed: ledger || typeof metered === "string" }
|
|
8599
|
+
};
|
|
8600
|
+
}
|
|
8601
|
+
},
|
|
8602
|
+
// THE EMBEDDED APP'S SESSION STORE. The @shopify/shopify-app library
|
|
8603
|
+
// persists the offline session on token exchange and refresh; it lives
|
|
8604
|
+
// encrypted on the shared `shop` record, which is the same row every
|
|
8605
|
+
// organization's connection to this store points at.
|
|
8606
|
+
session: {
|
|
8607
|
+
// SOFT-CLEAR, NEVER A DELETE. The library calls this on session
|
|
8608
|
+
// invalidation and not only on uninstall, so hard-deleting the record
|
|
8609
|
+
// would orphan every org connection pointing at it — token resolution
|
|
8610
|
+
// silently nulls — and destroy the durable account link and the
|
|
8611
|
+
// storefront token with it. The next embedded load re-runs token
|
|
8612
|
+
// exchange. The full record delete belongs to the uninstall webhook.
|
|
8613
|
+
clear: async ({ context }) => {
|
|
8614
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8615
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8616
|
+
return {
|
|
8617
|
+
message: "Session cleared.",
|
|
8618
|
+
request: { shop },
|
|
8619
|
+
result: { ok: true },
|
|
8620
|
+
writes: [{
|
|
8621
|
+
collection: "shop",
|
|
8622
|
+
data: { $set: { settings: null } },
|
|
8623
|
+
operation: "update",
|
|
8624
|
+
query: { shop }
|
|
8625
|
+
}]
|
|
8626
|
+
};
|
|
8627
|
+
},
|
|
8628
|
+
read: async ({ context }, { read } = {}) => {
|
|
8629
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8630
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8631
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
8632
|
+
if (!(record == null ? void 0 : record.settings)) throw refusal("No session for this store", 404);
|
|
8633
|
+
return {
|
|
8634
|
+
message: "Session read.",
|
|
8635
|
+
request: { shop },
|
|
8636
|
+
result: { session: decrypt(record.settings) }
|
|
8637
|
+
};
|
|
8638
|
+
},
|
|
8639
|
+
save: async ({ context }) => {
|
|
8640
|
+
const session = context == null ? void 0 : context.session;
|
|
8641
|
+
const shop = sanitizeDomain(session == null ? void 0 : session.shop);
|
|
8642
|
+
if (!shop || !(session == null ? void 0 : session.accessToken)) throw refusal("session.shop and session.accessToken are required", 400);
|
|
8643
|
+
return {
|
|
8644
|
+
message: "Session saved.",
|
|
8645
|
+
request: { shop },
|
|
8646
|
+
result: { ok: true },
|
|
8647
|
+
writes: [{
|
|
8648
|
+
collection: "shop",
|
|
8649
|
+
data: {
|
|
8650
|
+
$set: { settings: encrypt(session), shop },
|
|
8651
|
+
$setOnInsert: { status: "pending" }
|
|
8652
|
+
},
|
|
8653
|
+
operation: "update",
|
|
8654
|
+
options: { upsert: true },
|
|
8655
|
+
query: { shop }
|
|
8656
|
+
}]
|
|
8657
|
+
};
|
|
8658
|
+
}
|
|
8659
|
+
},
|
|
8660
|
+
// THE LINK-TOKEN HANDOFF. The breakout callback runs top-level and can set
|
|
8661
|
+
// a first-party cookie; the embedded App Home cannot read that cookie from
|
|
8662
|
+
// inside the Shopify admin iframe. So the callback stashes the merchant's
|
|
8663
|
+
// user token here keyed by shop, and App Home reads it once and clears it.
|
|
8664
|
+
//
|
|
8665
|
+
// DURABLE RATHER THAN IN REDIS: a restart must not strand a merchant
|
|
8666
|
+
// mid-handoff. Short-lived and encrypted.
|
|
8667
|
+
token: {
|
|
8668
|
+
clear: async ({ context }) => {
|
|
8669
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8670
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8671
|
+
return {
|
|
8672
|
+
message: "Link token cleared.",
|
|
8673
|
+
request: { shop },
|
|
8674
|
+
result: { ok: true },
|
|
8675
|
+
writes: [{
|
|
8676
|
+
collection: "shop",
|
|
8677
|
+
data: { $set: { linkToken: null, linkTokenExpiresAt: null } },
|
|
8678
|
+
operation: "update",
|
|
8679
|
+
query: { shop }
|
|
8680
|
+
}]
|
|
8681
|
+
};
|
|
8682
|
+
},
|
|
8683
|
+
read: async ({ context }, { read } = {}) => {
|
|
8684
|
+
var _a;
|
|
8685
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8686
|
+
if (!shop) throw refusal("shop is required", 400);
|
|
8687
|
+
const record = await read.get({ collection: "shop", query: { shop } });
|
|
8688
|
+
const expiresAt = (record == null ? void 0 : record.linkTokenExpiresAt) ? new Date(record.linkTokenExpiresAt) : null;
|
|
8689
|
+
if (!(record == null ? void 0 : record.linkToken) || !expiresAt || expiresAt <= /* @__PURE__ */ new Date()) {
|
|
8690
|
+
throw refusal("No link token for this store", 404);
|
|
8691
|
+
}
|
|
8692
|
+
return {
|
|
8693
|
+
message: "Link token read.",
|
|
8694
|
+
request: { shop },
|
|
8695
|
+
result: { token: ((_a = decrypt(record.linkToken)) == null ? void 0 : _a.token) ?? null }
|
|
8696
|
+
};
|
|
8697
|
+
},
|
|
8698
|
+
save: async ({ context }) => {
|
|
8699
|
+
const shop = sanitizeDomain(context == null ? void 0 : context.shop);
|
|
8700
|
+
const token = context == null ? void 0 : context.token;
|
|
8701
|
+
if (!shop || !token) throw refusal("shop and token are required", 400);
|
|
8702
|
+
return {
|
|
8703
|
+
message: "Link token saved.",
|
|
8704
|
+
request: { shop },
|
|
8705
|
+
result: { ok: true },
|
|
8706
|
+
writes: [{
|
|
8707
|
+
collection: "shop",
|
|
8708
|
+
data: {
|
|
8709
|
+
$set: {
|
|
8710
|
+
linkToken: encrypt({ token }),
|
|
8711
|
+
linkTokenExpiresAt: new Date(Date.now() + LINK_TOKEN_TTL_MS)
|
|
8712
|
+
}
|
|
8713
|
+
},
|
|
8714
|
+
operation: "update",
|
|
8715
|
+
query: { shop }
|
|
8716
|
+
}]
|
|
8717
|
+
};
|
|
8718
|
+
}
|
|
8719
|
+
}
|
|
8720
|
+
},
|
|
5985
8721
|
// Drawbridge sends its own notification email and SMS — see the private
|
|
5986
8722
|
// `drawbridge` manifest. A vendor answering these would be a second sender,
|
|
5987
8723
|
// which is the arrangement the platform sender replaced.
|
|
@@ -6043,8 +8779,8 @@ var shopify_default2 = {
|
|
|
6043
8779
|
// access token still works, reconciles the scopes the store granted
|
|
6044
8780
|
// against the ones the app now needs, and queues a webhook
|
|
6045
8781
|
// reconciliation.
|
|
6046
|
-
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
6047
|
-
var _a, _b, _c, _d
|
|
8782
|
+
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify = client } = {}) => {
|
|
8783
|
+
var _a, _b, _c, _d;
|
|
6048
8784
|
const request2 = {
|
|
6049
8785
|
connectionId: workflow.connection,
|
|
6050
8786
|
organizationId: workflow.organization,
|
|
@@ -6081,33 +8817,26 @@ var shopify_default2 = {
|
|
|
6081
8817
|
shopCurrency = String(shopData.currency || "").toLowerCase() || null;
|
|
6082
8818
|
sourceRepaired = true;
|
|
6083
8819
|
}
|
|
6084
|
-
const { subscriptions } = await shopify.admin.getActiveAppSubscriptions({
|
|
6085
|
-
adminAccessToken,
|
|
6086
|
-
domain: connection2.shop
|
|
6087
|
-
});
|
|
6088
|
-
const usageLine = (_a = subscriptions.find((subscription) => subscription.usageLineItemId)) == null ? void 0 : _a.usageLineItemId;
|
|
6089
|
-
if (usageLine) metered = usageLine;
|
|
6090
8820
|
const partnerRow = (read == null ? void 0 : read.get) ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
6091
8821
|
const partner = (partnerRow == null ? void 0 : partnerRow.settings) ? decrypt(partnerRow.settings) : {};
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
const stampedAt = (_c = boundaries.find(
|
|
8822
|
+
const ledgerFloor = new Date(Date.now() - LEDGER_WINDOW_MS);
|
|
8823
|
+
const verdict = await meterVerdict({
|
|
8824
|
+
adminAccessToken,
|
|
8825
|
+
ledgerFloor,
|
|
8826
|
+
partner,
|
|
8827
|
+
shop: connection2.shop,
|
|
8828
|
+
shopId: source == null ? void 0 : source.id,
|
|
8829
|
+
shopify
|
|
8830
|
+
});
|
|
8831
|
+
if (verdict.metered !== void 0) metered = verdict.metered;
|
|
8832
|
+
const { boundaries } = verdict;
|
|
8833
|
+
if (boundaries.length) {
|
|
8834
|
+
const stampedAt = (_a = boundaries.find(
|
|
6106
8835
|
(event) => {
|
|
6107
8836
|
var _a2;
|
|
6108
8837
|
return String(event.chargeId || event.id) === String(((_a2 = connection2.source) == null ? void 0 : _a2.metered) || "");
|
|
6109
8838
|
}
|
|
6110
|
-
)) == null ? void 0 :
|
|
8839
|
+
)) == null ? void 0 : _a.occurredAt;
|
|
6111
8840
|
reconciliation = [];
|
|
6112
8841
|
let windowStart = ledgerFloor;
|
|
6113
8842
|
for (const boundary of boundaries) {
|
|
@@ -6157,7 +8886,7 @@ var shopify_default2 = {
|
|
|
6157
8886
|
}
|
|
6158
8887
|
});
|
|
6159
8888
|
} else {
|
|
6160
|
-
(
|
|
8889
|
+
(_b = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _b.call(logger2, new Error(
|
|
6161
8890
|
"shopify.usage.reconciliation.mismatch on shop " + connection2.shop + ": charge " + charge + " charged " + charged + " cents but its window holds " + billed + " cents across " + orders.length + " billed orders \u2014 each cent of difference is commission that never charged"
|
|
6162
8891
|
), {
|
|
6163
8892
|
extra: {
|
|
@@ -6176,13 +8905,13 @@ var shopify_default2 = {
|
|
|
6176
8905
|
}
|
|
6177
8906
|
} catch (probeError) {
|
|
6178
8907
|
billingProbeFailed = true;
|
|
6179
|
-
(
|
|
8908
|
+
(_c = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _c.call(logger2, "shopify.health.billing.probe.failed", {
|
|
6180
8909
|
connectionId: connection2.id,
|
|
6181
8910
|
message: probeError == null ? void 0 : probeError.message,
|
|
6182
8911
|
shop: connection2.shop
|
|
6183
8912
|
});
|
|
6184
8913
|
}
|
|
6185
|
-
if (sourceRepaired || metered !== void 0 && metered !== (((
|
|
8914
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_d = connection2.source) == null ? void 0 : _d.metered) ?? void 0)) {
|
|
6186
8915
|
writes.push({
|
|
6187
8916
|
collection: "connection",
|
|
6188
8917
|
data: {
|
|
@@ -6317,13 +9046,13 @@ var shopify_default2 = {
|
|
|
6317
9046
|
// They are the same two questions every other vendor answers through
|
|
6318
9047
|
// resources.*, so they answer them the same way now.
|
|
6319
9048
|
//
|
|
6320
|
-
// `shopify`
|
|
9049
|
+
// `shopify` DEFAULTS to the client this file imports; nothing injects it,
|
|
6321
9050
|
// which depends on it. What arrives is the SDK's pure HTTP namespaces
|
|
6322
9051
|
// and nothing else — no controller, no collection access. Resolving the
|
|
6323
9052
|
// credential is the caller's job because it is Drawbridge's job: the
|
|
6324
9053
|
// admin token refreshes and writes itself back, which is service work,
|
|
6325
9054
|
// not vendor work.
|
|
6326
|
-
products: async ({ cursor, limit = 100, search, settings, sort }, { shopify } = {}) => {
|
|
9055
|
+
products: async ({ cursor, limit = 100, search, settings, sort }, { shopify = client } = {}) => {
|
|
6327
9056
|
var _a, _b, _c, _d;
|
|
6328
9057
|
const products = await shopify.storefront.getProducts({
|
|
6329
9058
|
cursor,
|
|
@@ -6345,7 +9074,7 @@ var shopify_default2 = {
|
|
|
6345
9074
|
}
|
|
6346
9075
|
};
|
|
6347
9076
|
},
|
|
6348
|
-
promotions: async ({ cursor, limit = 100, search, settings }, { shopify } = {}) => {
|
|
9077
|
+
promotions: async ({ cursor, limit = 100, search, settings }, { shopify = client } = {}) => {
|
|
6349
9078
|
var _a, _b;
|
|
6350
9079
|
const discounts = await shopify.admin.getDiscounts({
|
|
6351
9080
|
adminAccessToken: settings == null ? void 0 : settings.adminAccessToken,
|
|
@@ -6496,6 +9225,13 @@ var shopify_default2 = {
|
|
|
6496
9225
|
key: "Shopify Order Tracking",
|
|
6497
9226
|
queue: "connection",
|
|
6498
9227
|
system: true,
|
|
9228
|
+
// THE TRIGGER THE REGISTER JOB HAND-WROTE. It is byte-identical to
|
|
9229
|
+
// the one on every stored workflow, because these documents exist on
|
|
9230
|
+
// prod with thousands of runs behind them and provisioning is
|
|
9231
|
+
// idempotent on (connection, system, title) — a different trigger
|
|
9232
|
+
// here would not be picked up by the existing rows anyway, and a
|
|
9233
|
+
// different TITLE would provision a second workflow for every store.
|
|
9234
|
+
trigger: { event: "shopify.order.create", type: "webhook" },
|
|
6499
9235
|
type: "step.commerce.order.record"
|
|
6500
9236
|
})
|
|
6501
9237
|
},
|
|
@@ -6506,6 +9242,15 @@ var shopify_default2 = {
|
|
|
6506
9242
|
key: "Shopify Product Sync",
|
|
6507
9243
|
queue: "connection",
|
|
6508
9244
|
system: true,
|
|
9245
|
+
// `shopify.product.update`, NOT `product.insert`. The plan named the
|
|
9246
|
+
// latter, and it is the right long-term answer — a Drawbridge product
|
|
9247
|
+
// row appearing should pull the vendor's copy, rather than
|
|
9248
|
+
// stream/product.js branching on the slug and enqueueing a
|
|
9249
|
+
// vendor-named queue. But a workflow carries ONE trigger, this one is
|
|
9250
|
+
// stored on prod against the webhook, and changing a live workflow's
|
|
9251
|
+
// trigger is a data migration. So the declaration matches what exists
|
|
9252
|
+
// and `product.insert` stays a separate, migration-gated change.
|
|
9253
|
+
trigger: { event: "shopify.product.update", type: "webhook" },
|
|
6509
9254
|
type: "step.commerce.product.sync"
|
|
6510
9255
|
})
|
|
6511
9256
|
}
|
|
@@ -6527,26 +9272,37 @@ var shopify_default2 = {
|
|
|
6527
9272
|
type: "step.connection.health.check"
|
|
6528
9273
|
})
|
|
6529
9274
|
},
|
|
6530
|
-
//
|
|
6531
|
-
//
|
|
6532
|
-
//
|
|
6533
|
-
//
|
|
6534
|
-
//
|
|
6535
|
-
//
|
|
9275
|
+
// THE ONE WORKFLOW THAT CARRIES TWO STEPS, and the shape the plan called
|
|
9276
|
+
// the single genuinely new thing in it.
|
|
9277
|
+
//
|
|
9278
|
+
// Every other system step is its own workflow, titled by its own `key`.
|
|
9279
|
+
// These two are "Shopify Token Activity", one row in the merchant's list
|
|
9280
|
+
// with both steps in it — so they declare `workflow`, which names the
|
|
9281
|
+
// title, the trigger and the ORDER, and the provisioner groups on it.
|
|
9282
|
+
// Without that key the pair could only be provisioned by the hand-written
|
|
9283
|
+
// spec in drawbridge-sync's register job, which is exactly why that spec
|
|
9284
|
+
// outlived every other one.
|
|
9285
|
+
//
|
|
9286
|
+
// AUDIT-ONLY. The step documents are written manually at OAuth time and
|
|
9287
|
+
// the workflow is never dispatched; `queue` is declared defensively so
|
|
9288
|
+
// that if it ever IS, the job lands on a real queue and the handler lookup
|
|
9289
|
+
// misses cleanly rather than throwing "Unknown step type".
|
|
6536
9290
|
token: {
|
|
6537
9291
|
exchange: () => ({
|
|
6538
9292
|
description: "Records the token exchange that completed an install. Audit only \u2014 never dispatched.",
|
|
6539
9293
|
key: "Shopify Token Exchange",
|
|
6540
9294
|
queue: "connection",
|
|
6541
9295
|
system: true,
|
|
6542
|
-
type: "step.connection.token.exchange"
|
|
9296
|
+
type: "step.connection.token.exchange",
|
|
9297
|
+
workflow: { key: "Shopify Token Activity", order: 0, trigger: { event: "shopify.token", type: "event" } }
|
|
6543
9298
|
}),
|
|
6544
9299
|
refresh: () => ({
|
|
6545
9300
|
description: "Records a token rotation. Audit only \u2014 never dispatched.",
|
|
6546
9301
|
key: "Shopify Token Refresh",
|
|
6547
9302
|
queue: "connection",
|
|
6548
9303
|
system: true,
|
|
6549
|
-
type: "step.connection.token.refresh"
|
|
9304
|
+
type: "step.connection.token.refresh",
|
|
9305
|
+
workflow: { key: "Shopify Token Activity", order: 1, trigger: { event: "shopify.token", type: "event" } }
|
|
6550
9306
|
})
|
|
6551
9307
|
}
|
|
6552
9308
|
}
|
|
@@ -6850,14 +9606,15 @@ var webhook_default = {
|
|
|
6850
9606
|
"Drawbridge can POST event payloads to your endpoint as activity happens in your account, so your own systems can react to it.",
|
|
6851
9607
|
"Generate a signing secret and Drawbridge signs every request with it. Your endpoint recomputes the signature to confirm each payload genuinely came from Drawbridge before acting on it."
|
|
6852
9608
|
],
|
|
6853
|
-
// NO
|
|
6854
|
-
//
|
|
6855
|
-
//
|
|
6856
|
-
//
|
|
6857
|
-
//
|
|
9609
|
+
// THERE IS NO CONSENT SCREEN TO FAIL. Connecting mints a signing secret;
|
|
9610
|
+
// nothing is authorized against a third party, so neither `denied` nor
|
|
9611
|
+
// `invalid` has a state it could describe. The failures this connection
|
|
9612
|
+
// really has are at DELIVERY — a merchant's endpoint refusing a POST — and
|
|
9613
|
+
// those reach them through the connection's own errors rather than through
|
|
9614
|
+
// connect copy.
|
|
6858
9615
|
//
|
|
6859
|
-
//
|
|
6860
|
-
// manifests and there was no way to tell "nothing to add" from "nobody
|
|
9616
|
+
// `false` is the recorded decision, not silence: `errors` existed on two of
|
|
9617
|
+
// six manifests and there was no way to tell "nothing to add" from "nobody
|
|
6861
9618
|
// wrote it".
|
|
6862
9619
|
errors: false,
|
|
6863
9620
|
excerpt: "Sign outgoing webhook payloads with an HMAC secret to verify authenticity.",
|
|
@@ -7001,7 +9758,7 @@ var webhook_default = {
|
|
|
7001
9758
|
// The connect prompt only where connecting is not already the card's whole
|
|
7002
9759
|
// story: a disconnected or errored document renders a Connect action itself,
|
|
7003
9760
|
// and a task repeating it talks over the button.
|
|
7004
|
-
tasks: ({ settings, status } = {}) =>
|
|
9761
|
+
tasks: ({ settings, status } = {}) => status === "error" ? [] : (settings == null ? void 0 : settings.secret) ? [
|
|
7005
9762
|
{
|
|
7006
9763
|
message: 'Compute HMAC-SHA256( secret, t + "." + raw body ) and constant-time compare it against a v1 in the X-Drawbridge-Signature header; reject timestamps older than five minutes.',
|
|
7007
9764
|
title: "Requests must be verified",
|
|
@@ -7088,6 +9845,77 @@ var attentive_default3 = {
|
|
|
7088
9845
|
}
|
|
7089
9846
|
};
|
|
7090
9847
|
|
|
9848
|
+
// lib/connections/icons/brightdata.js
|
|
9849
|
+
var brightdata_default = `<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
9850
|
+
<rect width="500" height="500" fill="#0F58FF"/>
|
|
9851
|
+
<circle cx="250" cy="250" r="52" fill="white"/>
|
|
9852
|
+
<rect x="234" y="70" width="32" height="96" rx="16" fill="white"/>
|
|
9853
|
+
<rect x="234" y="334" width="32" height="96" rx="16" fill="white"/>
|
|
9854
|
+
<rect x="70" y="234" width="96" height="32" rx="16" fill="white"/>
|
|
9855
|
+
<rect x="334" y="234" width="96" height="32" rx="16" fill="white"/>
|
|
9856
|
+
<rect x="117" y="140" width="32" height="96" rx="16" transform="rotate(-45 117 140)" fill="white"/>
|
|
9857
|
+
<rect x="304" y="327" width="32" height="96" rx="16" transform="rotate(-45 304 327)" fill="white"/>
|
|
9858
|
+
<rect x="360" y="117" width="32" height="96" rx="16" transform="rotate(45 360 117)" fill="white"/>
|
|
9859
|
+
<rect x="173" y="304" width="32" height="96" rx="16" transform="rotate(45 173 304)" fill="white"/>
|
|
9860
|
+
</svg>`;
|
|
9861
|
+
|
|
9862
|
+
// lib/connections/vendors/brightdata.js
|
|
9863
|
+
var brightdata_default2 = {
|
|
9864
|
+
fields: [
|
|
9865
|
+
{
|
|
9866
|
+
credential: "BRIGHTDATA_BROWSER_URI",
|
|
9867
|
+
input: "password",
|
|
9868
|
+
key: "browserUri",
|
|
9869
|
+
// A wss:// endpoint with the account's user and password in its
|
|
9870
|
+
// authority, which is why it is a password field rather than a url one:
|
|
9871
|
+
// the whole string is the credential.
|
|
9872
|
+
label: "Scraping Browser endpoint",
|
|
9873
|
+
message: "The wss:// Scraping Browser endpoint, carrying its own credentials. Used to render pages that need a real browser.",
|
|
9874
|
+
redact: true,
|
|
9875
|
+
required: true
|
|
9876
|
+
},
|
|
9877
|
+
{
|
|
9878
|
+
credential: "BRIGHTDATA_UNLOCKER_API_KEY",
|
|
9879
|
+
input: "password",
|
|
9880
|
+
key: "unlockerApiKey",
|
|
9881
|
+
label: "Web Unlocker API key",
|
|
9882
|
+
// NO EXPIRY FIELD, and that is a decision rather than an omission. Bright
|
|
9883
|
+
// Data offers an `Unlimited` expiry and recommends against it; Drawbridge
|
|
9884
|
+
// takes the unlimited key deliberately, because there is no way to rotate
|
|
9885
|
+
// this programmatically and a date nothing can act on is a date that only
|
|
9886
|
+
// surfaces after it has already broken scraping. If a fixed expiry is ever
|
|
9887
|
+
// taken instead, the field to add is a stored date and a warning ahead of
|
|
9888
|
+
// it — not a rotation, which the vendor cannot support.
|
|
9889
|
+
message: "Bright Data account settings \u2192 API keys. Taken with an unlimited expiry: their API offers no way to rotate a key, so a dated one can only ever break scraping silently.",
|
|
9890
|
+
redact: true,
|
|
9891
|
+
required: true
|
|
9892
|
+
},
|
|
9893
|
+
{
|
|
9894
|
+
credential: "BRIGHTDATA_UNLOCKER_ZONE",
|
|
9895
|
+
input: "text",
|
|
9896
|
+
key: "unlockerZone",
|
|
9897
|
+
label: "Web Unlocker zone",
|
|
9898
|
+
// Not secret — a zone is a label on the account, and it is shown rather
|
|
9899
|
+
// than redacted so an operator can confirm which one is in use without
|
|
9900
|
+
// clearing the field to read it.
|
|
9901
|
+
message: "The zone name the Unlocker requests run against.",
|
|
9902
|
+
required: true
|
|
9903
|
+
}
|
|
9904
|
+
],
|
|
9905
|
+
icon: brightdata_default,
|
|
9906
|
+
name: "Bright Data",
|
|
9907
|
+
slug: "brightdata",
|
|
9908
|
+
// WHERE THE FACTS CAME FROM, read 2026-09-16.
|
|
9909
|
+
urls: {
|
|
9910
|
+
api: "https://docs.brightdata.com/api-reference/authentication",
|
|
9911
|
+
dashboard: "https://brightdata.com/cp/setting/users",
|
|
9912
|
+
// Bright Data has no scope model — a key carries the account's own access —
|
|
9913
|
+
// so there is nothing to link. `false` is the recorded answer rather than a
|
|
9914
|
+
// missing key.
|
|
9915
|
+
scopes: false
|
|
9916
|
+
}
|
|
9917
|
+
};
|
|
9918
|
+
|
|
7091
9919
|
// lib/connections/vendors/hubspot.js
|
|
7092
9920
|
var hubspot_default2 = {
|
|
7093
9921
|
// WHAT AN ADMIN TYPES on the provider screen. `credential` names the env var a
|
|
@@ -7463,7 +10291,7 @@ var checkIcon = (owner, icon) => {
|
|
|
7463
10291
|
}
|
|
7464
10292
|
};
|
|
7465
10293
|
var vendors = Object.freeze(Object.fromEntries(
|
|
7466
|
-
[attentive_default3, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
|
|
10294
|
+
[attentive_default3, brightdata_default2, hubspot_default2, klaviyo_default3, mailchimp_default3, sendgrid_default2, shopify_default3, twilio_default2].map((vendor) => [vendor.slug, vendor])
|
|
7467
10295
|
));
|
|
7468
10296
|
var buildVendor = (vendor) => {
|
|
7469
10297
|
if (!(vendor == null ? void 0 : vendor.slug)) throw new Error("A vendor needs a slug");
|
|
@@ -7490,7 +10318,7 @@ for (const [slug2, vendor] of Object.entries(vendors)) {
|
|
|
7490
10318
|
buildVendor(vendor);
|
|
7491
10319
|
}
|
|
7492
10320
|
var build = (manifest) => {
|
|
7493
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
10321
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y;
|
|
7494
10322
|
if (!(manifest == null ? void 0 : manifest.slug)) throw new Error("A connection needs a slug");
|
|
7495
10323
|
if (!(manifest == null ? void 0 : manifest.title)) throw new Error(manifest.slug + " needs a title");
|
|
7496
10324
|
if (manifest == null ? void 0 : manifest.private) throw new Error(manifest.slug + " declares private \u2014 that is feature : false now");
|
|
@@ -7756,11 +10584,22 @@ var build = (manifest) => {
|
|
|
7756
10584
|
manifest.slug + " step " + type2 + " may be triggered by " + trigger + ", which is not a trigger \u2014 one of " + Object.keys(TRIGGERS).join(", ")
|
|
7757
10585
|
);
|
|
7758
10586
|
}
|
|
7759
|
-
if (declared2.
|
|
7760
|
-
|
|
10587
|
+
if (declared2.workflow) {
|
|
10588
|
+
if (!declared2.workflow.key) {
|
|
10589
|
+
throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no key \u2014 the title several steps share");
|
|
10590
|
+
}
|
|
10591
|
+
if (typeof declared2.workflow.order !== "number") {
|
|
10592
|
+
throw new Error(manifest.slug + " step " + type2 + " declares a workflow with no order \u2014 two steps in one row need a sequence");
|
|
10593
|
+
}
|
|
10594
|
+
if (declared2.trigger) {
|
|
10595
|
+
throw new Error(manifest.slug + " step " + type2 + " declares BOTH a trigger and a workflow \u2014 the workflow carries the trigger for every step in it");
|
|
10596
|
+
}
|
|
10597
|
+
}
|
|
10598
|
+
for (const candidate of [declared2.trigger, (_y = declared2.workflow) == null ? void 0 : _y.trigger].filter(Boolean)) {
|
|
10599
|
+
const known2 = TRIGGERS[Object.keys(TRIGGERS).find((key) => TRIGGERS[key].event === candidate.event && TRIGGERS[key].type === candidate.type)];
|
|
7761
10600
|
if (!known2) {
|
|
7762
10601
|
throw new Error(
|
|
7763
|
-
manifest.slug + " step " + type2 + " declares trigger { event : " +
|
|
10602
|
+
manifest.slug + " step " + type2 + " declares trigger { event : " + candidate.event + ", type : " + candidate.type + " }, which nothing dispatches"
|
|
7764
10603
|
);
|
|
7765
10604
|
}
|
|
7766
10605
|
}
|