@drawbridge/drawbridge-utils 0.0.143 → 0.0.144
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connections/index.cjs +82 -7
- package/dist/connections/index.d.cts +100 -7
- package/dist/connections/index.d.ts +100 -7
- package/dist/connections/index.js +84 -9
- package/dist/providers.cjs +103 -47
- package/dist/providers.d.cts +14 -3
- package/dist/providers.d.ts +14 -3
- package/dist/providers.js +105 -49
- package/package.json +1 -1
|
@@ -3366,6 +3366,26 @@ var toCanonicalEmail = (value) => {
|
|
|
3366
3366
|
return local + "@" + domain;
|
|
3367
3367
|
};
|
|
3368
3368
|
|
|
3369
|
+
// lib/encrypt.js
|
|
3370
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
3371
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3372
|
+
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
3373
|
+
var decrypt = (value) => {
|
|
3374
|
+
if (typeof value !== "string") return value;
|
|
3375
|
+
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
3376
|
+
const decipher = import_crypto.default.createDecipheriv(
|
|
3377
|
+
ALGORITHM,
|
|
3378
|
+
getKey(),
|
|
3379
|
+
Buffer.from(ivHex, "hex")
|
|
3380
|
+
);
|
|
3381
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
3382
|
+
const result = Buffer.concat([
|
|
3383
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
3384
|
+
decipher.final()
|
|
3385
|
+
]);
|
|
3386
|
+
return JSON.parse(result.toString("utf8"));
|
|
3387
|
+
};
|
|
3388
|
+
|
|
3369
3389
|
// lib/slugify.js
|
|
3370
3390
|
var import_slugify = __toESM(require("slugify"), 1);
|
|
3371
3391
|
var slugify = (value, nochars = false) => {
|
|
@@ -3420,6 +3440,8 @@ var generateDiscountCode = (0, import_nanoid2.customAlphabet)("0123456789ABCDEFG
|
|
|
3420
3440
|
var ORDER_EVENT_HANDLE = slugify("drawbridge-orders");
|
|
3421
3441
|
var REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
3422
3442
|
var OAUTH_ERROR_SOURCE = "oauth";
|
|
3443
|
+
var BILLING_ERROR_SOURCE = "billing";
|
|
3444
|
+
var BILLING_ERROR_MESSAGE = "Order billing isn't set up for this store \u2014 reselect and approve the plan from the Shopify connection page.";
|
|
3423
3445
|
var OAUTH_GRANT_REVOKED_CODES = ["application_cannot_be_found", "invalid_grant"];
|
|
3424
3446
|
var inbound = {
|
|
3425
3447
|
headers: {
|
|
@@ -3940,6 +3962,9 @@ var shopify_default2 = {
|
|
|
3940
3962
|
}
|
|
3941
3963
|
}
|
|
3942
3964
|
const billable = (org == null ? void 0 : org.billingProvider) === "shopify" && fee > 0 && !backfill;
|
|
3965
|
+
const providerRow = billable ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
3966
|
+
const { orderEventHandle } = (providerRow == null ? void 0 : providerRow.settings) ? decrypt(providerRow.settings) : {};
|
|
3967
|
+
const handle = typeof orderEventHandle === "string" && slugify(orderEventHandle) || ORDER_EVENT_HANDLE;
|
|
3943
3968
|
if (billable && !((_f = connection2 == null ? void 0 : connection2.source) == null ? void 0 : _f.id)) {
|
|
3944
3969
|
(_g = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _g.call(logger2, new Error(
|
|
3945
3970
|
"shopify.usage.billing.skipped on order " + orderId + ": " + Math.round(fee * 100) + " cents not billed \u2014 connection " + (connection2 == null ? void 0 : connection2.id) + " has no source.id"
|
|
@@ -3958,7 +3983,7 @@ var shopify_default2 = {
|
|
|
3958
3983
|
// verify it against the transaction prefix and refuse a
|
|
3959
3984
|
// drifted pair — idempotency keys are permanent, so a
|
|
3960
3985
|
// mistraceable event can never be resent under its own key.
|
|
3961
|
-
handle
|
|
3986
|
+
handle,
|
|
3962
3987
|
idempotencyKey: String(orderId),
|
|
3963
3988
|
orderDocId,
|
|
3964
3989
|
orderId: String(orderId),
|
|
@@ -3968,7 +3993,7 @@ var shopify_default2 = {
|
|
|
3968
3993
|
// here — the event handle plus the order id — and sent as the
|
|
3969
3994
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
3970
3995
|
// the order as billed.transaction.
|
|
3971
|
-
transaction:
|
|
3996
|
+
transaction: handle + "." + orderId,
|
|
3972
3997
|
value: Math.round(fee * 100)
|
|
3973
3998
|
},
|
|
3974
3999
|
name: "billing",
|
|
@@ -4173,6 +4198,46 @@ var shopify_default2 = {
|
|
|
4173
4198
|
query: { id: connection2.id }
|
|
4174
4199
|
});
|
|
4175
4200
|
}
|
|
4201
|
+
const hasBillingIssue = (connection2.errors || []).some((entry) => entry.source === BILLING_ERROR_SOURCE);
|
|
4202
|
+
if (metered === null && !hasBillingIssue) {
|
|
4203
|
+
writes.push({
|
|
4204
|
+
collection: "connection",
|
|
4205
|
+
data: {
|
|
4206
|
+
$push: {
|
|
4207
|
+
errors: {
|
|
4208
|
+
message: BILLING_ERROR_MESSAGE,
|
|
4209
|
+
source: BILLING_ERROR_SOURCE
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
},
|
|
4213
|
+
operation: "update",
|
|
4214
|
+
query: { id: connection2.id }
|
|
4215
|
+
});
|
|
4216
|
+
}
|
|
4217
|
+
if (typeof metered === "string" && hasBillingIssue) {
|
|
4218
|
+
writes.push({
|
|
4219
|
+
collection: "connection",
|
|
4220
|
+
data: {
|
|
4221
|
+
$pull: {
|
|
4222
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4223
|
+
}
|
|
4224
|
+
},
|
|
4225
|
+
operation: "update",
|
|
4226
|
+
query: { id: connection2.id }
|
|
4227
|
+
});
|
|
4228
|
+
writes.push({
|
|
4229
|
+
collection: "connection",
|
|
4230
|
+
data: {
|
|
4231
|
+
$set: { status: "active" }
|
|
4232
|
+
},
|
|
4233
|
+
operation: "update",
|
|
4234
|
+
query: {
|
|
4235
|
+
id: connection2.id,
|
|
4236
|
+
status: "error",
|
|
4237
|
+
"errors.0": { $exists: false }
|
|
4238
|
+
}
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4176
4241
|
return {
|
|
4177
4242
|
writes,
|
|
4178
4243
|
enqueues: [{
|
|
@@ -4337,7 +4402,15 @@ var shopify_default2 = {
|
|
|
4337
4402
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4338
4403
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4339
4404
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4340
|
-
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
4405
|
+
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true },
|
|
4406
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
4407
|
+
// changes: stored value overrides the built-in default
|
|
4408
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
4409
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
4410
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
4411
|
+
// means the default, and the sender still refuses any handle that
|
|
4412
|
+
// disagrees with the transaction it minted.
|
|
4413
|
+
{ format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", required: false, setting: true }
|
|
4341
4414
|
]
|
|
4342
4415
|
},
|
|
4343
4416
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4479,8 +4552,9 @@ var shopify_default2 = {
|
|
|
4479
4552
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4480
4553
|
{
|
|
4481
4554
|
action: "billing",
|
|
4482
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4483
|
-
title: "Approve your plan"
|
|
4555
|
+
message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
|
|
4556
|
+
title: "Approve your plan",
|
|
4557
|
+
type: "warning"
|
|
4484
4558
|
}
|
|
4485
4559
|
] : [],
|
|
4486
4560
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
@@ -4493,8 +4567,9 @@ var shopify_default2 = {
|
|
|
4493
4567
|
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4494
4568
|
{
|
|
4495
4569
|
action: "billing",
|
|
4496
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4497
|
-
title: "Fix order billing"
|
|
4570
|
+
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged. You'll need to reselect your plan on Shopify and approve it to fix this.",
|
|
4571
|
+
title: "Fix order billing",
|
|
4572
|
+
type: "error"
|
|
4498
4573
|
}
|
|
4499
4574
|
] : []
|
|
4500
4575
|
];
|
|
@@ -7,6 +7,7 @@ import { channels } from '../pricing.cjs';
|
|
|
7
7
|
import { customAlphabet } from 'nanoid';
|
|
8
8
|
import { toCanonicalEmail } from '../email.cjs';
|
|
9
9
|
import { conversionRate } from '../plans.cjs';
|
|
10
|
+
import { decrypt } from '../encrypt.cjs';
|
|
10
11
|
import { slugify } from '../slugify.cjs';
|
|
11
12
|
import { safeRequest } from '../safe-http.cjs';
|
|
12
13
|
import 'libphonenumber-js';
|
|
@@ -19,6 +20,8 @@ import '../index.cjs';
|
|
|
19
20
|
import 'currency-codes';
|
|
20
21
|
import '../color.cjs';
|
|
21
22
|
import 'tinycolor2';
|
|
23
|
+
import 'crypto';
|
|
24
|
+
import '../token.cjs';
|
|
22
25
|
import 'slugify';
|
|
23
26
|
import 'dns';
|
|
24
27
|
import 'node:http';
|
|
@@ -3869,6 +3872,13 @@ const REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1000;
|
|
|
3869
3872
|
|
|
3870
3873
|
const OAUTH_ERROR_SOURCE = 'oauth';
|
|
3871
3874
|
|
|
3875
|
+
// The billing Issue a probed-unmetered store carries in its connection
|
|
3876
|
+
// errors, written and cleared beside the source.metered stamp (health check
|
|
3877
|
+
// and drawbridge-sync's approval restamp share the source tag). One string,
|
|
3878
|
+
// used by both writers, so the entries dedupe and $pull cleanly.
|
|
3879
|
+
const BILLING_ERROR_SOURCE = 'billing';
|
|
3880
|
+
const BILLING_ERROR_MESSAGE = 'Order billing isn\'t set up for this store — reselect and approve the plan from the Shopify connection page.';
|
|
3881
|
+
|
|
3872
3882
|
// What Shopify says when the merchant has uninstalled or revoked. Neither is
|
|
3873
3883
|
// retryable and both mean the same thing to a merchant: reconnect.
|
|
3874
3884
|
const OAUTH_GRANT_REVOKED_CODES = [ 'application_cannot_be_found', 'invalid_grant' ];
|
|
@@ -4592,6 +4602,22 @@ var shopify = {
|
|
|
4592
4602
|
// roll back.
|
|
4593
4603
|
const billable = org?.billingProvider === 'shopify' && fee > 0 && ! backfill;
|
|
4594
4604
|
|
|
4605
|
+
// THE EFFECTIVE METER HANDLE. The provider row's orderEventHandle
|
|
4606
|
+
// (admin-editable, for the day the plan config's meter changes)
|
|
4607
|
+
// overrides the built-in default; slugified again on read as belt
|
|
4608
|
+
// and braces with the save-side formatter, since a drifted stored
|
|
4609
|
+
// value would mint transactions no meter classifies. Read directly
|
|
4610
|
+
// (not via providerSettings — that module imports this one) and
|
|
4611
|
+
// only for a billable order, which keeps the cost off every
|
|
4612
|
+
// ordinary redemption.
|
|
4613
|
+
const providerRow = billable
|
|
4614
|
+
? await read.get({ collection : 'provider', query : { slug : 'shopify' } })
|
|
4615
|
+
: null;
|
|
4616
|
+
|
|
4617
|
+
const { orderEventHandle } = providerRow?.settings ? decrypt( providerRow.settings ) : {};
|
|
4618
|
+
|
|
4619
|
+
const handle = ( typeof orderEventHandle === 'string' && slugify( orderEventHandle ) ) || ORDER_EVENT_HANDLE;
|
|
4620
|
+
|
|
4595
4621
|
// A billable order with no shop id is revenue lost SILENTLY — the
|
|
4596
4622
|
// gate below just yields no job, the order records normally, and the
|
|
4597
4623
|
// connection looks healthy. An Error (not a warn) because this is
|
|
@@ -4622,7 +4648,7 @@ var shopify = {
|
|
|
4622
4648
|
// verify it against the transaction prefix and refuse a
|
|
4623
4649
|
// drifted pair — idempotency keys are permanent, so a
|
|
4624
4650
|
// mistraceable event can never be resent under its own key.
|
|
4625
|
-
handle
|
|
4651
|
+
handle,
|
|
4626
4652
|
idempotencyKey : String( orderId ),
|
|
4627
4653
|
orderDocId,
|
|
4628
4654
|
orderId : String( orderId ),
|
|
@@ -4632,7 +4658,7 @@ var shopify = {
|
|
|
4632
4658
|
// here — the event handle plus the order id — and sent as the
|
|
4633
4659
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
4634
4660
|
// the order as billed.transaction.
|
|
4635
|
-
transaction :
|
|
4661
|
+
transaction : handle + '.' + orderId,
|
|
4636
4662
|
value : Math.round( fee * 100 )
|
|
4637
4663
|
},
|
|
4638
4664
|
name : 'billing',
|
|
@@ -4962,6 +4988,63 @@ var shopify = {
|
|
|
4962
4988
|
|
|
4963
4989
|
}
|
|
4964
4990
|
|
|
4991
|
+
// THE ISSUE MARKER rides beside the stamp: a probed-unmetered
|
|
4992
|
+
// store carries a billing entry in the connection's own errors —
|
|
4993
|
+
// the Issues surface a merchant already knows — and a metered
|
|
4994
|
+
// verdict clears it. Keyed on PRESENCE rather than the verdict
|
|
4995
|
+
// changing, so a connection stamped before this writer existed
|
|
4996
|
+
// still gains its entry on the next run. Status is deliberately
|
|
4997
|
+
// untouched on the way in (orders must keep recording), and the
|
|
4998
|
+
// way out mirrors the resume idiom: reactivate only a connection
|
|
4999
|
+
// that is errored with nothing else wrong.
|
|
5000
|
+
const hasBillingIssue = ( connection.errors || [] ).some( ( entry ) => entry.source === BILLING_ERROR_SOURCE );
|
|
5001
|
+
|
|
5002
|
+
if( metered === null && ! hasBillingIssue ){
|
|
5003
|
+
|
|
5004
|
+
writes.push({
|
|
5005
|
+
collection : 'connection',
|
|
5006
|
+
data : {
|
|
5007
|
+
$push : {
|
|
5008
|
+
errors : {
|
|
5009
|
+
message : BILLING_ERROR_MESSAGE,
|
|
5010
|
+
source : BILLING_ERROR_SOURCE
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
},
|
|
5014
|
+
operation : 'update',
|
|
5015
|
+
query : { id : connection.id }
|
|
5016
|
+
});
|
|
5017
|
+
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
if( typeof metered === 'string' && hasBillingIssue ){
|
|
5021
|
+
|
|
5022
|
+
writes.push({
|
|
5023
|
+
collection : 'connection',
|
|
5024
|
+
data : {
|
|
5025
|
+
$pull : {
|
|
5026
|
+
errors : { source : BILLING_ERROR_SOURCE }
|
|
5027
|
+
}
|
|
5028
|
+
},
|
|
5029
|
+
operation : 'update',
|
|
5030
|
+
query : { id : connection.id }
|
|
5031
|
+
});
|
|
5032
|
+
|
|
5033
|
+
writes.push({
|
|
5034
|
+
collection : 'connection',
|
|
5035
|
+
data : {
|
|
5036
|
+
$set : { status : 'active' }
|
|
5037
|
+
},
|
|
5038
|
+
operation : 'update',
|
|
5039
|
+
query : {
|
|
5040
|
+
id : connection.id,
|
|
5041
|
+
status : 'error',
|
|
5042
|
+
'errors.0' : { $exists : false }
|
|
5043
|
+
}
|
|
5044
|
+
});
|
|
5045
|
+
|
|
5046
|
+
}
|
|
5047
|
+
|
|
4965
5048
|
return {
|
|
4966
5049
|
writes,
|
|
4967
5050
|
enqueues : [ {
|
|
@@ -5174,7 +5257,15 @@ var shopify = {
|
|
|
5174
5257
|
{ input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', required : true },
|
|
5175
5258
|
{ input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
|
|
5176
5259
|
{ input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
|
|
5177
|
-
{ input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true }
|
|
5260
|
+
{ input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true },
|
|
5261
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
5262
|
+
// changes: stored value overrides the built-in default
|
|
5263
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
5264
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
5265
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
5266
|
+
// means the default, and the sender still refuses any handle that
|
|
5267
|
+
// disagrees with the transaction it minted.
|
|
5268
|
+
{ format : 'slug', input : 'text', key : 'orderEventHandle', label : 'Order event handle', required : false, setting : true }
|
|
5178
5269
|
]
|
|
5179
5270
|
},
|
|
5180
5271
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -5326,8 +5417,9 @@ var shopify = {
|
|
|
5326
5417
|
? [
|
|
5327
5418
|
{
|
|
5328
5419
|
action : 'billing',
|
|
5329
|
-
message : 'Approve the Drawbridge plan to activate this connection. You\'ll
|
|
5330
|
-
title : 'Approve your plan'
|
|
5420
|
+
message : 'Approve the Drawbridge plan to activate this connection. You\'ll be asked to select a plan on Shopify, then everything else happens here.',
|
|
5421
|
+
title : 'Approve your plan',
|
|
5422
|
+
type : 'warning'
|
|
5331
5423
|
}
|
|
5332
5424
|
]
|
|
5333
5425
|
: []
|
|
@@ -5343,8 +5435,9 @@ var shopify = {
|
|
|
5343
5435
|
? [
|
|
5344
5436
|
{
|
|
5345
5437
|
action : 'billing',
|
|
5346
|
-
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged.
|
|
5347
|
-
title : 'Fix order billing'
|
|
5438
|
+
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged. You\'ll need to reselect your plan on Shopify and approve it to fix this.',
|
|
5439
|
+
title : 'Fix order billing',
|
|
5440
|
+
type : 'error'
|
|
5348
5441
|
}
|
|
5349
5442
|
]
|
|
5350
5443
|
: []
|
|
@@ -7,6 +7,7 @@ import { channels } from '../pricing.js';
|
|
|
7
7
|
import { customAlphabet } from 'nanoid';
|
|
8
8
|
import { toCanonicalEmail } from '../email.js';
|
|
9
9
|
import { conversionRate } from '../plans.js';
|
|
10
|
+
import { decrypt } from '../encrypt.js';
|
|
10
11
|
import { slugify } from '../slugify.js';
|
|
11
12
|
import { safeRequest } from '../safe-http.js';
|
|
12
13
|
import 'libphonenumber-js';
|
|
@@ -19,6 +20,8 @@ import '../index.js';
|
|
|
19
20
|
import 'currency-codes';
|
|
20
21
|
import '../color.js';
|
|
21
22
|
import 'tinycolor2';
|
|
23
|
+
import 'crypto';
|
|
24
|
+
import '../token.js';
|
|
22
25
|
import 'slugify';
|
|
23
26
|
import 'dns';
|
|
24
27
|
import 'node:http';
|
|
@@ -3869,6 +3872,13 @@ const REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1000;
|
|
|
3869
3872
|
|
|
3870
3873
|
const OAUTH_ERROR_SOURCE = 'oauth';
|
|
3871
3874
|
|
|
3875
|
+
// The billing Issue a probed-unmetered store carries in its connection
|
|
3876
|
+
// errors, written and cleared beside the source.metered stamp (health check
|
|
3877
|
+
// and drawbridge-sync's approval restamp share the source tag). One string,
|
|
3878
|
+
// used by both writers, so the entries dedupe and $pull cleanly.
|
|
3879
|
+
const BILLING_ERROR_SOURCE = 'billing';
|
|
3880
|
+
const BILLING_ERROR_MESSAGE = 'Order billing isn\'t set up for this store — reselect and approve the plan from the Shopify connection page.';
|
|
3881
|
+
|
|
3872
3882
|
// What Shopify says when the merchant has uninstalled or revoked. Neither is
|
|
3873
3883
|
// retryable and both mean the same thing to a merchant: reconnect.
|
|
3874
3884
|
const OAUTH_GRANT_REVOKED_CODES = [ 'application_cannot_be_found', 'invalid_grant' ];
|
|
@@ -4592,6 +4602,22 @@ var shopify = {
|
|
|
4592
4602
|
// roll back.
|
|
4593
4603
|
const billable = org?.billingProvider === 'shopify' && fee > 0 && ! backfill;
|
|
4594
4604
|
|
|
4605
|
+
// THE EFFECTIVE METER HANDLE. The provider row's orderEventHandle
|
|
4606
|
+
// (admin-editable, for the day the plan config's meter changes)
|
|
4607
|
+
// overrides the built-in default; slugified again on read as belt
|
|
4608
|
+
// and braces with the save-side formatter, since a drifted stored
|
|
4609
|
+
// value would mint transactions no meter classifies. Read directly
|
|
4610
|
+
// (not via providerSettings — that module imports this one) and
|
|
4611
|
+
// only for a billable order, which keeps the cost off every
|
|
4612
|
+
// ordinary redemption.
|
|
4613
|
+
const providerRow = billable
|
|
4614
|
+
? await read.get({ collection : 'provider', query : { slug : 'shopify' } })
|
|
4615
|
+
: null;
|
|
4616
|
+
|
|
4617
|
+
const { orderEventHandle } = providerRow?.settings ? decrypt( providerRow.settings ) : {};
|
|
4618
|
+
|
|
4619
|
+
const handle = ( typeof orderEventHandle === 'string' && slugify( orderEventHandle ) ) || ORDER_EVENT_HANDLE;
|
|
4620
|
+
|
|
4595
4621
|
// A billable order with no shop id is revenue lost SILENTLY — the
|
|
4596
4622
|
// gate below just yields no job, the order records normally, and the
|
|
4597
4623
|
// connection looks healthy. An Error (not a warn) because this is
|
|
@@ -4622,7 +4648,7 @@ var shopify = {
|
|
|
4622
4648
|
// verify it against the transaction prefix and refuse a
|
|
4623
4649
|
// drifted pair — idempotency keys are permanent, so a
|
|
4624
4650
|
// mistraceable event can never be resent under its own key.
|
|
4625
|
-
handle
|
|
4651
|
+
handle,
|
|
4626
4652
|
idempotencyKey : String( orderId ),
|
|
4627
4653
|
orderDocId,
|
|
4628
4654
|
orderId : String( orderId ),
|
|
@@ -4632,7 +4658,7 @@ var shopify = {
|
|
|
4632
4658
|
// here — the event handle plus the order id — and sent as the
|
|
4633
4659
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
4634
4660
|
// the order as billed.transaction.
|
|
4635
|
-
transaction :
|
|
4661
|
+
transaction : handle + '.' + orderId,
|
|
4636
4662
|
value : Math.round( fee * 100 )
|
|
4637
4663
|
},
|
|
4638
4664
|
name : 'billing',
|
|
@@ -4962,6 +4988,63 @@ var shopify = {
|
|
|
4962
4988
|
|
|
4963
4989
|
}
|
|
4964
4990
|
|
|
4991
|
+
// THE ISSUE MARKER rides beside the stamp: a probed-unmetered
|
|
4992
|
+
// store carries a billing entry in the connection's own errors —
|
|
4993
|
+
// the Issues surface a merchant already knows — and a metered
|
|
4994
|
+
// verdict clears it. Keyed on PRESENCE rather than the verdict
|
|
4995
|
+
// changing, so a connection stamped before this writer existed
|
|
4996
|
+
// still gains its entry on the next run. Status is deliberately
|
|
4997
|
+
// untouched on the way in (orders must keep recording), and the
|
|
4998
|
+
// way out mirrors the resume idiom: reactivate only a connection
|
|
4999
|
+
// that is errored with nothing else wrong.
|
|
5000
|
+
const hasBillingIssue = ( connection.errors || [] ).some( ( entry ) => entry.source === BILLING_ERROR_SOURCE );
|
|
5001
|
+
|
|
5002
|
+
if( metered === null && ! hasBillingIssue ){
|
|
5003
|
+
|
|
5004
|
+
writes.push({
|
|
5005
|
+
collection : 'connection',
|
|
5006
|
+
data : {
|
|
5007
|
+
$push : {
|
|
5008
|
+
errors : {
|
|
5009
|
+
message : BILLING_ERROR_MESSAGE,
|
|
5010
|
+
source : BILLING_ERROR_SOURCE
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
5013
|
+
},
|
|
5014
|
+
operation : 'update',
|
|
5015
|
+
query : { id : connection.id }
|
|
5016
|
+
});
|
|
5017
|
+
|
|
5018
|
+
}
|
|
5019
|
+
|
|
5020
|
+
if( typeof metered === 'string' && hasBillingIssue ){
|
|
5021
|
+
|
|
5022
|
+
writes.push({
|
|
5023
|
+
collection : 'connection',
|
|
5024
|
+
data : {
|
|
5025
|
+
$pull : {
|
|
5026
|
+
errors : { source : BILLING_ERROR_SOURCE }
|
|
5027
|
+
}
|
|
5028
|
+
},
|
|
5029
|
+
operation : 'update',
|
|
5030
|
+
query : { id : connection.id }
|
|
5031
|
+
});
|
|
5032
|
+
|
|
5033
|
+
writes.push({
|
|
5034
|
+
collection : 'connection',
|
|
5035
|
+
data : {
|
|
5036
|
+
$set : { status : 'active' }
|
|
5037
|
+
},
|
|
5038
|
+
operation : 'update',
|
|
5039
|
+
query : {
|
|
5040
|
+
id : connection.id,
|
|
5041
|
+
status : 'error',
|
|
5042
|
+
'errors.0' : { $exists : false }
|
|
5043
|
+
}
|
|
5044
|
+
});
|
|
5045
|
+
|
|
5046
|
+
}
|
|
5047
|
+
|
|
4965
5048
|
return {
|
|
4966
5049
|
writes,
|
|
4967
5050
|
enqueues : [ {
|
|
@@ -5174,7 +5257,15 @@ var shopify = {
|
|
|
5174
5257
|
{ input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', required : true },
|
|
5175
5258
|
{ input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
|
|
5176
5259
|
{ input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
|
|
5177
|
-
{ input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true }
|
|
5260
|
+
{ input : 'text', key : 'listingUrl', credential : 'SHOPIFY_APP_LISTING_URL', label : 'App listing URL', required : true },
|
|
5261
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
5262
|
+
// changes: stored value overrides the built-in default
|
|
5263
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
5264
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
5265
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
5266
|
+
// means the default, and the sender still refuses any handle that
|
|
5267
|
+
// disagrees with the transaction it minted.
|
|
5268
|
+
{ format : 'slug', input : 'text', key : 'orderEventHandle', label : 'Order event handle', required : false, setting : true }
|
|
5178
5269
|
]
|
|
5179
5270
|
},
|
|
5180
5271
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -5326,8 +5417,9 @@ var shopify = {
|
|
|
5326
5417
|
? [
|
|
5327
5418
|
{
|
|
5328
5419
|
action : 'billing',
|
|
5329
|
-
message : 'Approve the Drawbridge plan to activate this connection. You\'ll
|
|
5330
|
-
title : 'Approve your plan'
|
|
5420
|
+
message : 'Approve the Drawbridge plan to activate this connection. You\'ll be asked to select a plan on Shopify, then everything else happens here.',
|
|
5421
|
+
title : 'Approve your plan',
|
|
5422
|
+
type : 'warning'
|
|
5331
5423
|
}
|
|
5332
5424
|
]
|
|
5333
5425
|
: []
|
|
@@ -5343,8 +5435,9 @@ var shopify = {
|
|
|
5343
5435
|
? [
|
|
5344
5436
|
{
|
|
5345
5437
|
action : 'billing',
|
|
5346
|
-
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged.
|
|
5347
|
-
title : 'Fix order billing'
|
|
5438
|
+
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged. You\'ll need to reselect your plan on Shopify and approve it to fix this.',
|
|
5439
|
+
title : 'Fix order billing',
|
|
5440
|
+
type : 'error'
|
|
5348
5441
|
}
|
|
5349
5442
|
]
|
|
5350
5443
|
: []
|
|
@@ -3292,6 +3292,26 @@ var toCanonicalEmail = (value) => {
|
|
|
3292
3292
|
return local + "@" + domain;
|
|
3293
3293
|
};
|
|
3294
3294
|
|
|
3295
|
+
// lib/encrypt.js
|
|
3296
|
+
import crypto from "crypto";
|
|
3297
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3298
|
+
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
3299
|
+
var decrypt = (value) => {
|
|
3300
|
+
if (typeof value !== "string") return value;
|
|
3301
|
+
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
3302
|
+
const decipher = crypto.createDecipheriv(
|
|
3303
|
+
ALGORITHM,
|
|
3304
|
+
getKey(),
|
|
3305
|
+
Buffer.from(ivHex, "hex")
|
|
3306
|
+
);
|
|
3307
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
3308
|
+
const result = Buffer.concat([
|
|
3309
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
3310
|
+
decipher.final()
|
|
3311
|
+
]);
|
|
3312
|
+
return JSON.parse(result.toString("utf8"));
|
|
3313
|
+
};
|
|
3314
|
+
|
|
3295
3315
|
// lib/slugify.js
|
|
3296
3316
|
import slug from "slugify";
|
|
3297
3317
|
var slugify = (value, nochars = false) => {
|
|
@@ -3346,6 +3366,8 @@ var generateDiscountCode = customAlphabet2("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
|
3346
3366
|
var ORDER_EVENT_HANDLE = slugify("drawbridge-orders");
|
|
3347
3367
|
var REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
3348
3368
|
var OAUTH_ERROR_SOURCE = "oauth";
|
|
3369
|
+
var BILLING_ERROR_SOURCE = "billing";
|
|
3370
|
+
var BILLING_ERROR_MESSAGE = "Order billing isn't set up for this store \u2014 reselect and approve the plan from the Shopify connection page.";
|
|
3349
3371
|
var OAUTH_GRANT_REVOKED_CODES = ["application_cannot_be_found", "invalid_grant"];
|
|
3350
3372
|
var inbound = {
|
|
3351
3373
|
headers: {
|
|
@@ -3866,6 +3888,9 @@ var shopify_default2 = {
|
|
|
3866
3888
|
}
|
|
3867
3889
|
}
|
|
3868
3890
|
const billable = (org == null ? void 0 : org.billingProvider) === "shopify" && fee > 0 && !backfill;
|
|
3891
|
+
const providerRow = billable ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
3892
|
+
const { orderEventHandle } = (providerRow == null ? void 0 : providerRow.settings) ? decrypt(providerRow.settings) : {};
|
|
3893
|
+
const handle = typeof orderEventHandle === "string" && slugify(orderEventHandle) || ORDER_EVENT_HANDLE;
|
|
3869
3894
|
if (billable && !((_f = connection2 == null ? void 0 : connection2.source) == null ? void 0 : _f.id)) {
|
|
3870
3895
|
(_g = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _g.call(logger2, new Error(
|
|
3871
3896
|
"shopify.usage.billing.skipped on order " + orderId + ": " + Math.round(fee * 100) + " cents not billed \u2014 connection " + (connection2 == null ? void 0 : connection2.id) + " has no source.id"
|
|
@@ -3884,7 +3909,7 @@ var shopify_default2 = {
|
|
|
3884
3909
|
// verify it against the transaction prefix and refuse a
|
|
3885
3910
|
// drifted pair — idempotency keys are permanent, so a
|
|
3886
3911
|
// mistraceable event can never be resent under its own key.
|
|
3887
|
-
handle
|
|
3912
|
+
handle,
|
|
3888
3913
|
idempotencyKey: String(orderId),
|
|
3889
3914
|
orderDocId,
|
|
3890
3915
|
orderId: String(orderId),
|
|
@@ -3894,7 +3919,7 @@ var shopify_default2 = {
|
|
|
3894
3919
|
// here — the event handle plus the order id — and sent as the
|
|
3895
3920
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
3896
3921
|
// the order as billed.transaction.
|
|
3897
|
-
transaction:
|
|
3922
|
+
transaction: handle + "." + orderId,
|
|
3898
3923
|
value: Math.round(fee * 100)
|
|
3899
3924
|
},
|
|
3900
3925
|
name: "billing",
|
|
@@ -4099,6 +4124,46 @@ var shopify_default2 = {
|
|
|
4099
4124
|
query: { id: connection2.id }
|
|
4100
4125
|
});
|
|
4101
4126
|
}
|
|
4127
|
+
const hasBillingIssue = (connection2.errors || []).some((entry) => entry.source === BILLING_ERROR_SOURCE);
|
|
4128
|
+
if (metered === null && !hasBillingIssue) {
|
|
4129
|
+
writes.push({
|
|
4130
|
+
collection: "connection",
|
|
4131
|
+
data: {
|
|
4132
|
+
$push: {
|
|
4133
|
+
errors: {
|
|
4134
|
+
message: BILLING_ERROR_MESSAGE,
|
|
4135
|
+
source: BILLING_ERROR_SOURCE
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
},
|
|
4139
|
+
operation: "update",
|
|
4140
|
+
query: { id: connection2.id }
|
|
4141
|
+
});
|
|
4142
|
+
}
|
|
4143
|
+
if (typeof metered === "string" && hasBillingIssue) {
|
|
4144
|
+
writes.push({
|
|
4145
|
+
collection: "connection",
|
|
4146
|
+
data: {
|
|
4147
|
+
$pull: {
|
|
4148
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4149
|
+
}
|
|
4150
|
+
},
|
|
4151
|
+
operation: "update",
|
|
4152
|
+
query: { id: connection2.id }
|
|
4153
|
+
});
|
|
4154
|
+
writes.push({
|
|
4155
|
+
collection: "connection",
|
|
4156
|
+
data: {
|
|
4157
|
+
$set: { status: "active" }
|
|
4158
|
+
},
|
|
4159
|
+
operation: "update",
|
|
4160
|
+
query: {
|
|
4161
|
+
id: connection2.id,
|
|
4162
|
+
status: "error",
|
|
4163
|
+
"errors.0": { $exists: false }
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4102
4167
|
return {
|
|
4103
4168
|
writes,
|
|
4104
4169
|
enqueues: [{
|
|
@@ -4263,7 +4328,15 @@ var shopify_default2 = {
|
|
|
4263
4328
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4264
4329
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4265
4330
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4266
|
-
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
4331
|
+
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true },
|
|
4332
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
4333
|
+
// changes: stored value overrides the built-in default
|
|
4334
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
4335
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
4336
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
4337
|
+
// means the default, and the sender still refuses any handle that
|
|
4338
|
+
// disagrees with the transaction it minted.
|
|
4339
|
+
{ format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", required: false, setting: true }
|
|
4267
4340
|
]
|
|
4268
4341
|
},
|
|
4269
4342
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4405,8 +4478,9 @@ var shopify_default2 = {
|
|
|
4405
4478
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4406
4479
|
{
|
|
4407
4480
|
action: "billing",
|
|
4408
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4409
|
-
title: "Approve your plan"
|
|
4481
|
+
message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
|
|
4482
|
+
title: "Approve your plan",
|
|
4483
|
+
type: "warning"
|
|
4410
4484
|
}
|
|
4411
4485
|
] : [],
|
|
4412
4486
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
@@ -4419,8 +4493,9 @@ var shopify_default2 = {
|
|
|
4419
4493
|
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4420
4494
|
{
|
|
4421
4495
|
action: "billing",
|
|
4422
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4423
|
-
title: "Fix order billing"
|
|
4496
|
+
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged. You'll need to reselect your plan on Shopify and approve it to fix this.",
|
|
4497
|
+
title: "Fix order billing",
|
|
4498
|
+
type: "error"
|
|
4424
4499
|
}
|
|
4425
4500
|
] : []
|
|
4426
4501
|
];
|
|
@@ -4429,7 +4504,7 @@ var shopify_default2 = {
|
|
|
4429
4504
|
};
|
|
4430
4505
|
|
|
4431
4506
|
// lib/connections/providers/webhook.js
|
|
4432
|
-
import
|
|
4507
|
+
import crypto2 from "crypto";
|
|
4433
4508
|
|
|
4434
4509
|
// lib/safe-http.js
|
|
4435
4510
|
import dns2 from "dns";
|
|
@@ -4664,7 +4739,7 @@ var webhook_default = {
|
|
|
4664
4739
|
request2.body = body;
|
|
4665
4740
|
const outgoing = { ...headers };
|
|
4666
4741
|
if (settings == null ? void 0 : settings.secret) {
|
|
4667
|
-
outgoing["X-Drawbridge-Signature"] = "sha256=" +
|
|
4742
|
+
outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto2.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
|
|
4668
4743
|
}
|
|
4669
4744
|
const response = await send2({ body, headers: outgoing, method, url });
|
|
4670
4745
|
return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
|
package/dist/providers.cjs
CHANGED
|
@@ -3272,6 +3272,45 @@ var toCanonicalEmail = (value) => {
|
|
|
3272
3272
|
return local + "@" + domain;
|
|
3273
3273
|
};
|
|
3274
3274
|
|
|
3275
|
+
// lib/encrypt.js
|
|
3276
|
+
var import_crypto2 = __toESM(require("crypto"), 1);
|
|
3277
|
+
|
|
3278
|
+
// lib/token.js
|
|
3279
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
3280
|
+
var generate = (bytes = 32, encoding = "base64url") => {
|
|
3281
|
+
const buf = import_crypto.default.randomBytes(bytes);
|
|
3282
|
+
return encoding ? buf.toString(encoding) : buf;
|
|
3283
|
+
};
|
|
3284
|
+
|
|
3285
|
+
// lib/encrypt.js
|
|
3286
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3287
|
+
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
3288
|
+
var encrypt = (value) => {
|
|
3289
|
+
const iv = generate(12, null);
|
|
3290
|
+
const cipher = import_crypto2.default.createCipheriv(ALGORITHM, getKey(), iv);
|
|
3291
|
+
const data2 = Buffer.concat([
|
|
3292
|
+
cipher.update(JSON.stringify(value), "utf8"),
|
|
3293
|
+
cipher.final()
|
|
3294
|
+
]);
|
|
3295
|
+
const tag = cipher.getAuthTag();
|
|
3296
|
+
return [iv, tag, data2].map((b) => b.toString("hex")).join(":");
|
|
3297
|
+
};
|
|
3298
|
+
var decrypt = (value) => {
|
|
3299
|
+
if (typeof value !== "string") return value;
|
|
3300
|
+
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
3301
|
+
const decipher = import_crypto2.default.createDecipheriv(
|
|
3302
|
+
ALGORITHM,
|
|
3303
|
+
getKey(),
|
|
3304
|
+
Buffer.from(ivHex, "hex")
|
|
3305
|
+
);
|
|
3306
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
3307
|
+
const result = Buffer.concat([
|
|
3308
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
3309
|
+
decipher.final()
|
|
3310
|
+
]);
|
|
3311
|
+
return JSON.parse(result.toString("utf8"));
|
|
3312
|
+
};
|
|
3313
|
+
|
|
3275
3314
|
// lib/slugify.js
|
|
3276
3315
|
var import_slugify = __toESM(require("slugify"), 1);
|
|
3277
3316
|
var slugify = (value, nochars = false) => {
|
|
@@ -3326,6 +3365,8 @@ var generateDiscountCode = (0, import_nanoid2.customAlphabet)("0123456789ABCDEFG
|
|
|
3326
3365
|
var ORDER_EVENT_HANDLE = slugify("drawbridge-orders");
|
|
3327
3366
|
var REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
3328
3367
|
var OAUTH_ERROR_SOURCE = "oauth";
|
|
3368
|
+
var BILLING_ERROR_SOURCE = "billing";
|
|
3369
|
+
var BILLING_ERROR_MESSAGE = "Order billing isn't set up for this store \u2014 reselect and approve the plan from the Shopify connection page.";
|
|
3329
3370
|
var OAUTH_GRANT_REVOKED_CODES = ["application_cannot_be_found", "invalid_grant"];
|
|
3330
3371
|
var inbound = {
|
|
3331
3372
|
headers: {
|
|
@@ -3846,6 +3887,9 @@ var shopify_default2 = {
|
|
|
3846
3887
|
}
|
|
3847
3888
|
}
|
|
3848
3889
|
const billable = (org == null ? void 0 : org.billingProvider) === "shopify" && fee > 0 && !backfill;
|
|
3890
|
+
const providerRow = billable ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
3891
|
+
const { orderEventHandle } = (providerRow == null ? void 0 : providerRow.settings) ? decrypt(providerRow.settings) : {};
|
|
3892
|
+
const handle = typeof orderEventHandle === "string" && slugify(orderEventHandle) || ORDER_EVENT_HANDLE;
|
|
3849
3893
|
if (billable && !((_f = connection2 == null ? void 0 : connection2.source) == null ? void 0 : _f.id)) {
|
|
3850
3894
|
(_g = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _g.call(logger2, new Error(
|
|
3851
3895
|
"shopify.usage.billing.skipped on order " + orderId + ": " + Math.round(fee * 100) + " cents not billed \u2014 connection " + (connection2 == null ? void 0 : connection2.id) + " has no source.id"
|
|
@@ -3864,7 +3908,7 @@ var shopify_default2 = {
|
|
|
3864
3908
|
// verify it against the transaction prefix and refuse a
|
|
3865
3909
|
// drifted pair — idempotency keys are permanent, so a
|
|
3866
3910
|
// mistraceable event can never be resent under its own key.
|
|
3867
|
-
handle
|
|
3911
|
+
handle,
|
|
3868
3912
|
idempotencyKey: String(orderId),
|
|
3869
3913
|
orderDocId,
|
|
3870
3914
|
orderId: String(orderId),
|
|
@@ -3874,7 +3918,7 @@ var shopify_default2 = {
|
|
|
3874
3918
|
// here — the event handle plus the order id — and sent as the
|
|
3875
3919
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
3876
3920
|
// the order as billed.transaction.
|
|
3877
|
-
transaction:
|
|
3921
|
+
transaction: handle + "." + orderId,
|
|
3878
3922
|
value: Math.round(fee * 100)
|
|
3879
3923
|
},
|
|
3880
3924
|
name: "billing",
|
|
@@ -4079,6 +4123,46 @@ var shopify_default2 = {
|
|
|
4079
4123
|
query: { id: connection2.id }
|
|
4080
4124
|
});
|
|
4081
4125
|
}
|
|
4126
|
+
const hasBillingIssue = (connection2.errors || []).some((entry) => entry.source === BILLING_ERROR_SOURCE);
|
|
4127
|
+
if (metered === null && !hasBillingIssue) {
|
|
4128
|
+
writes.push({
|
|
4129
|
+
collection: "connection",
|
|
4130
|
+
data: {
|
|
4131
|
+
$push: {
|
|
4132
|
+
errors: {
|
|
4133
|
+
message: BILLING_ERROR_MESSAGE,
|
|
4134
|
+
source: BILLING_ERROR_SOURCE
|
|
4135
|
+
}
|
|
4136
|
+
}
|
|
4137
|
+
},
|
|
4138
|
+
operation: "update",
|
|
4139
|
+
query: { id: connection2.id }
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
if (typeof metered === "string" && hasBillingIssue) {
|
|
4143
|
+
writes.push({
|
|
4144
|
+
collection: "connection",
|
|
4145
|
+
data: {
|
|
4146
|
+
$pull: {
|
|
4147
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4148
|
+
}
|
|
4149
|
+
},
|
|
4150
|
+
operation: "update",
|
|
4151
|
+
query: { id: connection2.id }
|
|
4152
|
+
});
|
|
4153
|
+
writes.push({
|
|
4154
|
+
collection: "connection",
|
|
4155
|
+
data: {
|
|
4156
|
+
$set: { status: "active" }
|
|
4157
|
+
},
|
|
4158
|
+
operation: "update",
|
|
4159
|
+
query: {
|
|
4160
|
+
id: connection2.id,
|
|
4161
|
+
status: "error",
|
|
4162
|
+
"errors.0": { $exists: false }
|
|
4163
|
+
}
|
|
4164
|
+
});
|
|
4165
|
+
}
|
|
4082
4166
|
return {
|
|
4083
4167
|
writes,
|
|
4084
4168
|
enqueues: [{
|
|
@@ -4243,7 +4327,15 @@ var shopify_default2 = {
|
|
|
4243
4327
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4244
4328
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4245
4329
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4246
|
-
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
4330
|
+
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true },
|
|
4331
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
4332
|
+
// changes: stored value overrides the built-in default
|
|
4333
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
4334
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
4335
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
4336
|
+
// means the default, and the sender still refuses any handle that
|
|
4337
|
+
// disagrees with the transaction it minted.
|
|
4338
|
+
{ format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", required: false, setting: true }
|
|
4247
4339
|
]
|
|
4248
4340
|
},
|
|
4249
4341
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4385,8 +4477,9 @@ var shopify_default2 = {
|
|
|
4385
4477
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4386
4478
|
{
|
|
4387
4479
|
action: "billing",
|
|
4388
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4389
|
-
title: "Approve your plan"
|
|
4480
|
+
message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
|
|
4481
|
+
title: "Approve your plan",
|
|
4482
|
+
type: "warning"
|
|
4390
4483
|
}
|
|
4391
4484
|
] : [],
|
|
4392
4485
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
@@ -4399,8 +4492,9 @@ var shopify_default2 = {
|
|
|
4399
4492
|
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4400
4493
|
{
|
|
4401
4494
|
action: "billing",
|
|
4402
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4403
|
-
title: "Fix order billing"
|
|
4495
|
+
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged. You'll need to reselect your plan on Shopify and approve it to fix this.",
|
|
4496
|
+
title: "Fix order billing",
|
|
4497
|
+
type: "error"
|
|
4404
4498
|
}
|
|
4405
4499
|
] : []
|
|
4406
4500
|
];
|
|
@@ -4930,45 +5024,6 @@ var publicConnectionKeys = Object.freeze([
|
|
|
4930
5024
|
"warnings"
|
|
4931
5025
|
]);
|
|
4932
5026
|
|
|
4933
|
-
// lib/encrypt.js
|
|
4934
|
-
var import_crypto2 = __toESM(require("crypto"), 1);
|
|
4935
|
-
|
|
4936
|
-
// lib/token.js
|
|
4937
|
-
var import_crypto = __toESM(require("crypto"), 1);
|
|
4938
|
-
var generate = (bytes = 32, encoding = "base64url") => {
|
|
4939
|
-
const buf = import_crypto.default.randomBytes(bytes);
|
|
4940
|
-
return encoding ? buf.toString(encoding) : buf;
|
|
4941
|
-
};
|
|
4942
|
-
|
|
4943
|
-
// lib/encrypt.js
|
|
4944
|
-
var ALGORITHM = "aes-256-gcm";
|
|
4945
|
-
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
4946
|
-
var encrypt = (value) => {
|
|
4947
|
-
const iv = generate(12, null);
|
|
4948
|
-
const cipher = import_crypto2.default.createCipheriv(ALGORITHM, getKey(), iv);
|
|
4949
|
-
const data2 = Buffer.concat([
|
|
4950
|
-
cipher.update(JSON.stringify(value), "utf8"),
|
|
4951
|
-
cipher.final()
|
|
4952
|
-
]);
|
|
4953
|
-
const tag = cipher.getAuthTag();
|
|
4954
|
-
return [iv, tag, data2].map((b) => b.toString("hex")).join(":");
|
|
4955
|
-
};
|
|
4956
|
-
var decrypt = (value) => {
|
|
4957
|
-
if (typeof value !== "string") return value;
|
|
4958
|
-
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
4959
|
-
const decipher = import_crypto2.default.createDecipheriv(
|
|
4960
|
-
ALGORITHM,
|
|
4961
|
-
getKey(),
|
|
4962
|
-
Buffer.from(ivHex, "hex")
|
|
4963
|
-
);
|
|
4964
|
-
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
4965
|
-
const result = Buffer.concat([
|
|
4966
|
-
decipher.update(Buffer.from(dataHex, "hex")),
|
|
4967
|
-
decipher.final()
|
|
4968
|
-
]);
|
|
4969
|
-
return JSON.parse(result.toString("utf8"));
|
|
4970
|
-
};
|
|
4971
|
-
|
|
4972
5027
|
// lib/providers.js
|
|
4973
5028
|
var providerFields = (slug2) => {
|
|
4974
5029
|
var _a;
|
|
@@ -5008,9 +5063,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
|
|
|
5008
5063
|
});
|
|
5009
5064
|
const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
|
|
5010
5065
|
const pasted = (value) => typeof value === "string" ? value.trim().replace(/^['"]+|['"]+$/g, "") : value;
|
|
5066
|
+
const normalized = (field2, value) => field2.format === "slug" && typeof value === "string" && value ? slugify(value) : value;
|
|
5011
5067
|
const merged = mergeSettings({
|
|
5012
5068
|
existing: stored,
|
|
5013
|
-
incoming: Object.fromEntries(fields2.map((field2) => [field2.key, pasted(settings == null ? void 0 : settings[field2.key])]))
|
|
5069
|
+
incoming: Object.fromEntries(fields2.map((field2) => [field2.key, normalized(field2, pasted(settings == null ? void 0 : settings[field2.key]))]))
|
|
5014
5070
|
});
|
|
5015
5071
|
for (const key of Array.isArray(clear) ? clear : []) {
|
|
5016
5072
|
if (fields2.some((field2) => field2.key === key)) delete merged[key];
|
package/dist/providers.d.cts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connections, mergeSettings } from './connections/index.cjs';
|
|
2
2
|
import { decrypt, encrypt } from './encrypt.cjs';
|
|
3
|
+
import { slugify } from './slugify.cjs';
|
|
3
4
|
import './connections/oauth.cjs';
|
|
4
5
|
import 'node:crypto';
|
|
5
6
|
import './phone.cjs';
|
|
@@ -18,8 +19,6 @@ import 'nanoid';
|
|
|
18
19
|
import './color.cjs';
|
|
19
20
|
import 'tinycolor2';
|
|
20
21
|
import './email.cjs';
|
|
21
|
-
import './slugify.cjs';
|
|
22
|
-
import 'slugify';
|
|
23
22
|
import './safe-http.cjs';
|
|
24
23
|
import 'dns';
|
|
25
24
|
import 'node:http';
|
|
@@ -29,6 +28,7 @@ import 'axios';
|
|
|
29
28
|
import 'net';
|
|
30
29
|
import 'crypto';
|
|
31
30
|
import './token.cjs';
|
|
31
|
+
import 'slugify';
|
|
32
32
|
|
|
33
33
|
// DRAWBRIDGE'S OWN CREDENTIALS for a vendor, as opposed to a merchant's.
|
|
34
34
|
//
|
|
@@ -202,6 +202,17 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
|
|
|
202
202
|
? value.trim().replace( /^['"]+|['"]+$/g, '' )
|
|
203
203
|
: value;
|
|
204
204
|
|
|
205
|
+
// A field declared `format : 'slug'` is normalized on the way IN — a value
|
|
206
|
+
// like Shopify's meter event handle can legally hold no spaces or capitals,
|
|
207
|
+
// and classification against it is case-sensitive, so the stored copy is
|
|
208
|
+
// forced into the only shape the vendor accepts rather than trusting the
|
|
209
|
+
// admin's typing.
|
|
210
|
+
const normalized = ( field, value ) => (
|
|
211
|
+
field.format === 'slug' && typeof value === 'string' && value
|
|
212
|
+
? slugify( value )
|
|
213
|
+
: value
|
|
214
|
+
);
|
|
215
|
+
|
|
205
216
|
// mergeSettings is the blank-keeps rule, already used for merchant
|
|
206
217
|
// connection settings in route/organization-connection.js — same behaviour,
|
|
207
218
|
// one implementation. The declared-field allowlist is the part that is ours:
|
|
@@ -209,7 +220,7 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
|
|
|
209
220
|
// not declare reaching the document.
|
|
210
221
|
const merged = mergeSettings({
|
|
211
222
|
existing : stored,
|
|
212
|
-
incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, pasted( settings?.[ field.key ] ) ] ) )
|
|
223
|
+
incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, normalized( field, pasted( settings?.[ field.key ] ) ) ] ) )
|
|
213
224
|
});
|
|
214
225
|
|
|
215
226
|
// CLEARING IS EXPLICIT, because blank already means keep and the two cannot
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connections, mergeSettings } from './connections/index.js';
|
|
2
2
|
import { decrypt, encrypt } from './encrypt.js';
|
|
3
|
+
import { slugify } from './slugify.js';
|
|
3
4
|
import './connections/oauth.js';
|
|
4
5
|
import 'node:crypto';
|
|
5
6
|
import './phone.js';
|
|
@@ -18,8 +19,6 @@ import 'nanoid';
|
|
|
18
19
|
import './color.js';
|
|
19
20
|
import 'tinycolor2';
|
|
20
21
|
import './email.js';
|
|
21
|
-
import './slugify.js';
|
|
22
|
-
import 'slugify';
|
|
23
22
|
import './safe-http.js';
|
|
24
23
|
import 'dns';
|
|
25
24
|
import 'node:http';
|
|
@@ -29,6 +28,7 @@ import 'axios';
|
|
|
29
28
|
import 'net';
|
|
30
29
|
import 'crypto';
|
|
31
30
|
import './token.js';
|
|
31
|
+
import 'slugify';
|
|
32
32
|
|
|
33
33
|
// DRAWBRIDGE'S OWN CREDENTIALS for a vendor, as opposed to a merchant's.
|
|
34
34
|
//
|
|
@@ -202,6 +202,17 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
|
|
|
202
202
|
? value.trim().replace( /^['"]+|['"]+$/g, '' )
|
|
203
203
|
: value;
|
|
204
204
|
|
|
205
|
+
// A field declared `format : 'slug'` is normalized on the way IN — a value
|
|
206
|
+
// like Shopify's meter event handle can legally hold no spaces or capitals,
|
|
207
|
+
// and classification against it is case-sensitive, so the stored copy is
|
|
208
|
+
// forced into the only shape the vendor accepts rather than trusting the
|
|
209
|
+
// admin's typing.
|
|
210
|
+
const normalized = ( field, value ) => (
|
|
211
|
+
field.format === 'slug' && typeof value === 'string' && value
|
|
212
|
+
? slugify( value )
|
|
213
|
+
: value
|
|
214
|
+
);
|
|
215
|
+
|
|
205
216
|
// mergeSettings is the blank-keeps rule, already used for merchant
|
|
206
217
|
// connection settings in route/organization-connection.js — same behaviour,
|
|
207
218
|
// one implementation. The declared-field allowlist is the part that is ours:
|
|
@@ -209,7 +220,7 @@ const saveProviderSettings = async ({ authenticated, clear, controller, settings
|
|
|
209
220
|
// not declare reaching the document.
|
|
210
221
|
const merged = mergeSettings({
|
|
211
222
|
existing : stored,
|
|
212
|
-
incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, pasted( settings?.[ field.key ] ) ] ) )
|
|
223
|
+
incoming : Object.fromEntries( fields.map( ( field ) => [ field.key, normalized( field, pasted( settings?.[ field.key ] ) ) ] ) )
|
|
213
224
|
});
|
|
214
225
|
|
|
215
226
|
// CLEARING IS EXPLICIT, because blank already means keep and the two cannot
|
package/dist/providers.js
CHANGED
|
@@ -3228,6 +3228,45 @@ var toCanonicalEmail = (value) => {
|
|
|
3228
3228
|
return local + "@" + domain;
|
|
3229
3229
|
};
|
|
3230
3230
|
|
|
3231
|
+
// lib/encrypt.js
|
|
3232
|
+
import crypto2 from "crypto";
|
|
3233
|
+
|
|
3234
|
+
// lib/token.js
|
|
3235
|
+
import crypto from "crypto";
|
|
3236
|
+
var generate = (bytes = 32, encoding = "base64url") => {
|
|
3237
|
+
const buf = crypto.randomBytes(bytes);
|
|
3238
|
+
return encoding ? buf.toString(encoding) : buf;
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// lib/encrypt.js
|
|
3242
|
+
var ALGORITHM = "aes-256-gcm";
|
|
3243
|
+
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
3244
|
+
var encrypt = (value) => {
|
|
3245
|
+
const iv = generate(12, null);
|
|
3246
|
+
const cipher = crypto2.createCipheriv(ALGORITHM, getKey(), iv);
|
|
3247
|
+
const data2 = Buffer.concat([
|
|
3248
|
+
cipher.update(JSON.stringify(value), "utf8"),
|
|
3249
|
+
cipher.final()
|
|
3250
|
+
]);
|
|
3251
|
+
const tag = cipher.getAuthTag();
|
|
3252
|
+
return [iv, tag, data2].map((b) => b.toString("hex")).join(":");
|
|
3253
|
+
};
|
|
3254
|
+
var decrypt = (value) => {
|
|
3255
|
+
if (typeof value !== "string") return value;
|
|
3256
|
+
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
3257
|
+
const decipher = crypto2.createDecipheriv(
|
|
3258
|
+
ALGORITHM,
|
|
3259
|
+
getKey(),
|
|
3260
|
+
Buffer.from(ivHex, "hex")
|
|
3261
|
+
);
|
|
3262
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
3263
|
+
const result = Buffer.concat([
|
|
3264
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
3265
|
+
decipher.final()
|
|
3266
|
+
]);
|
|
3267
|
+
return JSON.parse(result.toString("utf8"));
|
|
3268
|
+
};
|
|
3269
|
+
|
|
3231
3270
|
// lib/slugify.js
|
|
3232
3271
|
import slug from "slugify";
|
|
3233
3272
|
var slugify = (value, nochars = false) => {
|
|
@@ -3282,6 +3321,8 @@ var generateDiscountCode = customAlphabet2("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
|
3282
3321
|
var ORDER_EVENT_HANDLE = slugify("drawbridge-orders");
|
|
3283
3322
|
var REFRESH_TOKEN_FRESHNESS_BUFFER_MS = 5 * 24 * 60 * 60 * 1e3;
|
|
3284
3323
|
var OAUTH_ERROR_SOURCE = "oauth";
|
|
3324
|
+
var BILLING_ERROR_SOURCE = "billing";
|
|
3325
|
+
var BILLING_ERROR_MESSAGE = "Order billing isn't set up for this store \u2014 reselect and approve the plan from the Shopify connection page.";
|
|
3285
3326
|
var OAUTH_GRANT_REVOKED_CODES = ["application_cannot_be_found", "invalid_grant"];
|
|
3286
3327
|
var inbound = {
|
|
3287
3328
|
headers: {
|
|
@@ -3802,6 +3843,9 @@ var shopify_default2 = {
|
|
|
3802
3843
|
}
|
|
3803
3844
|
}
|
|
3804
3845
|
const billable = (org == null ? void 0 : org.billingProvider) === "shopify" && fee > 0 && !backfill;
|
|
3846
|
+
const providerRow = billable ? await read.get({ collection: "provider", query: { slug: "shopify" } }) : null;
|
|
3847
|
+
const { orderEventHandle } = (providerRow == null ? void 0 : providerRow.settings) ? decrypt(providerRow.settings) : {};
|
|
3848
|
+
const handle = typeof orderEventHandle === "string" && slugify(orderEventHandle) || ORDER_EVENT_HANDLE;
|
|
3805
3849
|
if (billable && !((_f = connection2 == null ? void 0 : connection2.source) == null ? void 0 : _f.id)) {
|
|
3806
3850
|
(_g = logger2 == null ? void 0 : logger2.error) == null ? void 0 : _g.call(logger2, new Error(
|
|
3807
3851
|
"shopify.usage.billing.skipped on order " + orderId + ": " + Math.round(fee * 100) + " cents not billed \u2014 connection " + (connection2 == null ? void 0 : connection2.id) + " has no source.id"
|
|
@@ -3820,7 +3864,7 @@ var shopify_default2 = {
|
|
|
3820
3864
|
// verify it against the transaction prefix and refuse a
|
|
3821
3865
|
// drifted pair — idempotency keys are permanent, so a
|
|
3822
3866
|
// mistraceable event can never be resent under its own key.
|
|
3823
|
-
handle
|
|
3867
|
+
handle,
|
|
3824
3868
|
idempotencyKey: String(orderId),
|
|
3825
3869
|
orderDocId,
|
|
3826
3870
|
orderId: String(orderId),
|
|
@@ -3830,7 +3874,7 @@ var shopify_default2 = {
|
|
|
3830
3874
|
// here — the event handle plus the order id — and sent as the
|
|
3831
3875
|
// event's `reference`. queue/usage.js stamps the same id onto
|
|
3832
3876
|
// the order as billed.transaction.
|
|
3833
|
-
transaction:
|
|
3877
|
+
transaction: handle + "." + orderId,
|
|
3834
3878
|
value: Math.round(fee * 100)
|
|
3835
3879
|
},
|
|
3836
3880
|
name: "billing",
|
|
@@ -4035,6 +4079,46 @@ var shopify_default2 = {
|
|
|
4035
4079
|
query: { id: connection2.id }
|
|
4036
4080
|
});
|
|
4037
4081
|
}
|
|
4082
|
+
const hasBillingIssue = (connection2.errors || []).some((entry) => entry.source === BILLING_ERROR_SOURCE);
|
|
4083
|
+
if (metered === null && !hasBillingIssue) {
|
|
4084
|
+
writes.push({
|
|
4085
|
+
collection: "connection",
|
|
4086
|
+
data: {
|
|
4087
|
+
$push: {
|
|
4088
|
+
errors: {
|
|
4089
|
+
message: BILLING_ERROR_MESSAGE,
|
|
4090
|
+
source: BILLING_ERROR_SOURCE
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
},
|
|
4094
|
+
operation: "update",
|
|
4095
|
+
query: { id: connection2.id }
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
if (typeof metered === "string" && hasBillingIssue) {
|
|
4099
|
+
writes.push({
|
|
4100
|
+
collection: "connection",
|
|
4101
|
+
data: {
|
|
4102
|
+
$pull: {
|
|
4103
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4104
|
+
}
|
|
4105
|
+
},
|
|
4106
|
+
operation: "update",
|
|
4107
|
+
query: { id: connection2.id }
|
|
4108
|
+
});
|
|
4109
|
+
writes.push({
|
|
4110
|
+
collection: "connection",
|
|
4111
|
+
data: {
|
|
4112
|
+
$set: { status: "active" }
|
|
4113
|
+
},
|
|
4114
|
+
operation: "update",
|
|
4115
|
+
query: {
|
|
4116
|
+
id: connection2.id,
|
|
4117
|
+
status: "error",
|
|
4118
|
+
"errors.0": { $exists: false }
|
|
4119
|
+
}
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4038
4122
|
return {
|
|
4039
4123
|
writes,
|
|
4040
4124
|
enqueues: [{
|
|
@@ -4199,7 +4283,15 @@ var shopify_default2 = {
|
|
|
4199
4283
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4200
4284
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4201
4285
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4202
|
-
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true }
|
|
4286
|
+
{ input: "text", key: "listingUrl", credential: "SHOPIFY_APP_LISTING_URL", label: "App listing URL", required: true },
|
|
4287
|
+
// The usage meter's event handle, when the plan config's meter ever
|
|
4288
|
+
// changes: stored value overrides the built-in default
|
|
4289
|
+
// (events.order.handle) at the mint point, and `format : 'slug'`
|
|
4290
|
+
// normalizes it on save — a handle can carry no spaces or capitals,
|
|
4291
|
+
// and classification against it is case-sensitive. Optional: empty
|
|
4292
|
+
// means the default, and the sender still refuses any handle that
|
|
4293
|
+
// disagrees with the transaction it minted.
|
|
4294
|
+
{ format: "slug", input: "text", key: "orderEventHandle", label: "Order event handle", required: false, setting: true }
|
|
4203
4295
|
]
|
|
4204
4296
|
},
|
|
4205
4297
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4341,8 +4433,9 @@ var shopify_default2 = {
|
|
|
4341
4433
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4342
4434
|
{
|
|
4343
4435
|
action: "billing",
|
|
4344
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4345
|
-
title: "Approve your plan"
|
|
4436
|
+
message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
|
|
4437
|
+
title: "Approve your plan",
|
|
4438
|
+
type: "warning"
|
|
4346
4439
|
}
|
|
4347
4440
|
] : [],
|
|
4348
4441
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
@@ -4355,8 +4448,9 @@ var shopify_default2 = {
|
|
|
4355
4448
|
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4356
4449
|
{
|
|
4357
4450
|
action: "billing",
|
|
4358
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4359
|
-
title: "Fix order billing"
|
|
4451
|
+
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged. You'll need to reselect your plan on Shopify and approve it to fix this.",
|
|
4452
|
+
title: "Fix order billing",
|
|
4453
|
+
type: "error"
|
|
4360
4454
|
}
|
|
4361
4455
|
] : []
|
|
4362
4456
|
];
|
|
@@ -4365,7 +4459,7 @@ var shopify_default2 = {
|
|
|
4365
4459
|
};
|
|
4366
4460
|
|
|
4367
4461
|
// lib/connections/providers/webhook.js
|
|
4368
|
-
import
|
|
4462
|
+
import crypto3 from "crypto";
|
|
4369
4463
|
|
|
4370
4464
|
// lib/safe-http.js
|
|
4371
4465
|
import dns2 from "dns";
|
|
@@ -4600,7 +4694,7 @@ var webhook_default = {
|
|
|
4600
4694
|
request2.body = body;
|
|
4601
4695
|
const outgoing = { ...headers };
|
|
4602
4696
|
if (settings == null ? void 0 : settings.secret) {
|
|
4603
|
-
outgoing["X-Drawbridge-Signature"] = "sha256=" +
|
|
4697
|
+
outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto3.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
|
|
4604
4698
|
}
|
|
4605
4699
|
const response = await send2({ body, headers: outgoing, method, url });
|
|
4606
4700
|
return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
|
|
@@ -4886,45 +4980,6 @@ var publicConnectionKeys = Object.freeze([
|
|
|
4886
4980
|
"warnings"
|
|
4887
4981
|
]);
|
|
4888
4982
|
|
|
4889
|
-
// lib/encrypt.js
|
|
4890
|
-
import crypto3 from "crypto";
|
|
4891
|
-
|
|
4892
|
-
// lib/token.js
|
|
4893
|
-
import crypto2 from "crypto";
|
|
4894
|
-
var generate = (bytes = 32, encoding = "base64url") => {
|
|
4895
|
-
const buf = crypto2.randomBytes(bytes);
|
|
4896
|
-
return encoding ? buf.toString(encoding) : buf;
|
|
4897
|
-
};
|
|
4898
|
-
|
|
4899
|
-
// lib/encrypt.js
|
|
4900
|
-
var ALGORITHM = "aes-256-gcm";
|
|
4901
|
-
var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
|
|
4902
|
-
var encrypt = (value) => {
|
|
4903
|
-
const iv = generate(12, null);
|
|
4904
|
-
const cipher = crypto3.createCipheriv(ALGORITHM, getKey(), iv);
|
|
4905
|
-
const data2 = Buffer.concat([
|
|
4906
|
-
cipher.update(JSON.stringify(value), "utf8"),
|
|
4907
|
-
cipher.final()
|
|
4908
|
-
]);
|
|
4909
|
-
const tag = cipher.getAuthTag();
|
|
4910
|
-
return [iv, tag, data2].map((b) => b.toString("hex")).join(":");
|
|
4911
|
-
};
|
|
4912
|
-
var decrypt = (value) => {
|
|
4913
|
-
if (typeof value !== "string") return value;
|
|
4914
|
-
const [ivHex, tagHex, dataHex] = value.split(":");
|
|
4915
|
-
const decipher = crypto3.createDecipheriv(
|
|
4916
|
-
ALGORITHM,
|
|
4917
|
-
getKey(),
|
|
4918
|
-
Buffer.from(ivHex, "hex")
|
|
4919
|
-
);
|
|
4920
|
-
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
4921
|
-
const result = Buffer.concat([
|
|
4922
|
-
decipher.update(Buffer.from(dataHex, "hex")),
|
|
4923
|
-
decipher.final()
|
|
4924
|
-
]);
|
|
4925
|
-
return JSON.parse(result.toString("utf8"));
|
|
4926
|
-
};
|
|
4927
|
-
|
|
4928
4983
|
// lib/providers.js
|
|
4929
4984
|
var providerFields = (slug2) => {
|
|
4930
4985
|
var _a;
|
|
@@ -4964,9 +5019,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
|
|
|
4964
5019
|
});
|
|
4965
5020
|
const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
|
|
4966
5021
|
const pasted = (value) => typeof value === "string" ? value.trim().replace(/^['"]+|['"]+$/g, "") : value;
|
|
5022
|
+
const normalized = (field2, value) => field2.format === "slug" && typeof value === "string" && value ? slugify(value) : value;
|
|
4967
5023
|
const merged = mergeSettings({
|
|
4968
5024
|
existing: stored,
|
|
4969
|
-
incoming: Object.fromEntries(fields2.map((field2) => [field2.key, pasted(settings == null ? void 0 : settings[field2.key])]))
|
|
5025
|
+
incoming: Object.fromEntries(fields2.map((field2) => [field2.key, normalized(field2, pasted(settings == null ? void 0 : settings[field2.key]))]))
|
|
4970
5026
|
});
|
|
4971
5027
|
for (const key of Array.isArray(clear) ? clear : []) {
|
|
4972
5028
|
if (fields2.some((field2) => field2.key === key)) delete merged[key];
|
package/package.json
CHANGED