@drawbridge/drawbridge-utils 0.0.142 → 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 +98 -20
- package/dist/connections/index.d.cts +137 -30
- package/dist/connections/index.d.ts +137 -30
- package/dist/connections/index.js +100 -22
- package/dist/providers.cjs +119 -60
- package/dist/providers.d.cts +14 -3
- package/dist/providers.d.ts +14 -3
- package/dist/providers.js +121 -62
- package/package.json +1 -1
|
@@ -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',
|
|
@@ -4866,14 +4892,18 @@ var shopify = {
|
|
|
4866
4892
|
// holds a live admin token, so a missing source is one lookup away
|
|
4867
4893
|
// from fixed.
|
|
4868
4894
|
//
|
|
4869
|
-
// `metered` rides on the same pass:
|
|
4870
|
-
//
|
|
4871
|
-
//
|
|
4872
|
-
//
|
|
4873
|
-
//
|
|
4874
|
-
//
|
|
4875
|
-
//
|
|
4876
|
-
//
|
|
4895
|
+
// `metered` rides on the same pass: the usage LINE ITEM ID from the
|
|
4896
|
+
// merchant's ACTIVE approval — an id is proof of WHICH approval
|
|
4897
|
+
// carries the meter, where a boolean was only an assertion. Three
|
|
4898
|
+
// states: a gid string (probed, usage line present), null (probed
|
|
4899
|
+
// and absent — the unbillable state tasks() nags on), or no key at
|
|
4900
|
+
// all (never probed — silence, never treated as evidence). A meter
|
|
4901
|
+
// added to the app's pricing config is never retroactive — it
|
|
4902
|
+
// bills only from the moment the merchant approves the plan
|
|
4903
|
+
// version that carries it — so a null here means every order's
|
|
4904
|
+
// usage event is 202-accepted and silently never billed. Probed
|
|
4905
|
+
// every run, so a re-approval both clears the card within a day
|
|
4906
|
+
// and refreshes the id it re-minted.
|
|
4877
4907
|
//
|
|
4878
4908
|
// Best-effort by design: the ping above already proved access, so a
|
|
4879
4909
|
// blip in either lookup must not error a healthy connection — the
|
|
@@ -4886,7 +4916,9 @@ var shopify = {
|
|
|
4886
4916
|
|
|
4887
4917
|
let source = connection.source || null;
|
|
4888
4918
|
let sourceRepaired = false;
|
|
4889
|
-
|
|
4919
|
+
// undefined = no verdict this run; a string/null verdict comes
|
|
4920
|
+
// only from a successful probe of an ACTIVE approval.
|
|
4921
|
+
let metered;
|
|
4890
4922
|
let shopCurrency = null;
|
|
4891
4923
|
let billingProbeFailed = false;
|
|
4892
4924
|
|
|
@@ -4913,14 +4945,16 @@ var shopify = {
|
|
|
4913
4945
|
|
|
4914
4946
|
// Only an ACTIVE approval says anything about metering — no
|
|
4915
4947
|
// subscriptions (mid-install, plan pending) leaves the stamp
|
|
4916
|
-
// untouched rather than asserting
|
|
4948
|
+
// untouched rather than asserting null from silence. The
|
|
4917
4949
|
// every-boolean guard is version skew insurance: an older
|
|
4918
|
-
// @drawbridge/shopify returns subscriptions without the flag
|
|
4919
|
-
// and reading that silence as
|
|
4920
|
-
// merchant to re-approve a
|
|
4950
|
+
// @drawbridge/shopify returns subscriptions without the flag
|
|
4951
|
+
// (and without usageLineItemId), and reading that silence as
|
|
4952
|
+
// unmetered would nag every healthy merchant to re-approve a
|
|
4953
|
+
// plan that is fine — the flag being boolean proves an SDK
|
|
4954
|
+
// that also reports the line id.
|
|
4921
4955
|
if( subscriptions.length && subscriptions.every( ( subscription ) => typeof subscription.metered === 'boolean' ) ){
|
|
4922
4956
|
|
|
4923
|
-
metered = subscriptions.
|
|
4957
|
+
metered = subscriptions.find( ( subscription ) => subscription.usageLineItemId )?.usageLineItemId || null;
|
|
4924
4958
|
|
|
4925
4959
|
}
|
|
4926
4960
|
|
|
@@ -4936,13 +4970,16 @@ var shopify = {
|
|
|
4936
4970
|
|
|
4937
4971
|
}
|
|
4938
4972
|
|
|
4939
|
-
|
|
4973
|
+
// A verdict writes only when it CHANGES the stored one — the daily
|
|
4974
|
+
// run stays idempotent, and undefined (no verdict) never touches
|
|
4975
|
+
// the stamp.
|
|
4976
|
+
if( sourceRepaired || ( metered !== undefined && metered !== ( connection.source?.metered ?? undefined ) ) ){
|
|
4940
4977
|
|
|
4941
4978
|
writes.push({
|
|
4942
4979
|
collection : 'connection',
|
|
4943
4980
|
data : {
|
|
4944
4981
|
$set : sourceRepaired
|
|
4945
|
-
? { source : { ...source, ...( metered !==
|
|
4982
|
+
? { source : { ...source, ...( metered !== undefined && { metered }) } }
|
|
4946
4983
|
: { 'source.metered' : metered }
|
|
4947
4984
|
},
|
|
4948
4985
|
operation : 'update',
|
|
@@ -4951,6 +4988,63 @@ var shopify = {
|
|
|
4951
4988
|
|
|
4952
4989
|
}
|
|
4953
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
|
+
|
|
4954
5048
|
return {
|
|
4955
5049
|
writes,
|
|
4956
5050
|
enqueues : [ {
|
|
@@ -4972,11 +5066,12 @@ var shopify = {
|
|
|
4972
5066
|
: 'Health check passed — ping ok, webhooks reconciled.'
|
|
4973
5067
|
)
|
|
4974
5068
|
+ ( sourceRepaired ? ' Billing source repaired from the live shop.' : '' )
|
|
4975
|
-
+ ( metered ===
|
|
5069
|
+
+ ( metered === null ? ' No usage line on the store\'s approved plan — order billing needs the merchant to approve the updated plan.' : '' ),
|
|
4976
5070
|
request,
|
|
4977
5071
|
response : {
|
|
4978
5072
|
billingProbeFailed,
|
|
4979
|
-
metered,
|
|
5073
|
+
metered : metered ?? null,
|
|
5074
|
+
meterProbed : metered !== undefined,
|
|
4980
5075
|
pingedAt : new Date(),
|
|
4981
5076
|
refreshTokenExpiresAt : refreshTokenExpiresAt || null,
|
|
4982
5077
|
refreshTokenRotated,
|
|
@@ -5162,7 +5257,15 @@ var shopify = {
|
|
|
5162
5257
|
{ input : 'text', key : 'apiKey', credential : 'SHOPIFY_API_KEY', label : 'API key', required : true },
|
|
5163
5258
|
{ input : 'password', key : 'apiSecret', credential : 'SHOPIFY_API_SECRET', label : 'API secret', redact : true, required : true },
|
|
5164
5259
|
{ input : 'text', key : 'appHandle', credential : 'SHOPIFY_APP_HANDLE', label : 'App handle', required : true },
|
|
5165
|
-
{ 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 }
|
|
5166
5269
|
]
|
|
5167
5270
|
},
|
|
5168
5271
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -5314,23 +5417,27 @@ var shopify = {
|
|
|
5314
5417
|
? [
|
|
5315
5418
|
{
|
|
5316
5419
|
action : 'billing',
|
|
5317
|
-
message : 'Approve the Drawbridge plan to activate this connection. You\'ll
|
|
5318
|
-
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'
|
|
5319
5423
|
}
|
|
5320
5424
|
]
|
|
5321
5425
|
: []
|
|
5322
5426
|
),
|
|
5323
5427
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
5324
|
-
// the daily health check and the approval webhook (`source.metered
|
|
5325
|
-
//
|
|
5326
|
-
//
|
|
5327
|
-
//
|
|
5328
|
-
|
|
5428
|
+
// the daily health check and the approval webhook (`source.metered`:
|
|
5429
|
+
// the usage line's id when present, null when probed-and-absent).
|
|
5430
|
+
// Approved pricing is never retroactive, so the fix is a fresh
|
|
5431
|
+
// approval. Gated on an EXPLICIT null — a connection the probe hasn't
|
|
5432
|
+
// reached (or couldn't read) has no key and shows nothing rather than
|
|
5433
|
+
// nagging on silence.
|
|
5434
|
+
...( data?.status === 'active' && data?.source?.metered === null
|
|
5329
5435
|
? [
|
|
5330
5436
|
{
|
|
5331
5437
|
action : 'billing',
|
|
5332
|
-
message : 'Order billing isn\'t set up for this store, so campaign-attributed orders aren\'t being charged.
|
|
5333
|
-
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'
|
|
5334
5441
|
}
|
|
5335
5442
|
]
|
|
5336
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",
|
|
@@ -4038,7 +4063,7 @@ var shopify_default2 = {
|
|
|
4038
4063
|
// against the ones the app now needs, and queues a webhook
|
|
4039
4064
|
// reconciliation.
|
|
4040
4065
|
health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
|
|
4041
|
-
var _a, _b;
|
|
4066
|
+
var _a, _b, _c;
|
|
4042
4067
|
const request2 = {
|
|
4043
4068
|
connectionId: workflow.connection,
|
|
4044
4069
|
organizationId: workflow.organization,
|
|
@@ -4060,7 +4085,7 @@ var shopify_default2 = {
|
|
|
4060
4085
|
const writes = [];
|
|
4061
4086
|
let source = connection2.source || null;
|
|
4062
4087
|
let sourceRepaired = false;
|
|
4063
|
-
let metered
|
|
4088
|
+
let metered;
|
|
4064
4089
|
let shopCurrency = null;
|
|
4065
4090
|
let billingProbeFailed = false;
|
|
4066
4091
|
try {
|
|
@@ -4079,25 +4104,65 @@ var shopify_default2 = {
|
|
|
4079
4104
|
domain: connection2.shop
|
|
4080
4105
|
});
|
|
4081
4106
|
if (subscriptions.length && subscriptions.every((subscription) => typeof subscription.metered === "boolean")) {
|
|
4082
|
-
metered = subscriptions.
|
|
4107
|
+
metered = ((_a = subscriptions.find((subscription) => subscription.usageLineItemId)) == null ? void 0 : _a.usageLineItemId) || null;
|
|
4083
4108
|
}
|
|
4084
4109
|
} catch (probeError) {
|
|
4085
4110
|
billingProbeFailed = true;
|
|
4086
|
-
(
|
|
4111
|
+
(_b = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _b.call(logger2, "shopify.health.billing.probe.failed", {
|
|
4087
4112
|
connectionId: connection2.id,
|
|
4088
4113
|
message: probeError == null ? void 0 : probeError.message,
|
|
4089
4114
|
shop: connection2.shop
|
|
4090
4115
|
});
|
|
4091
4116
|
}
|
|
4092
|
-
if (sourceRepaired || metered !==
|
|
4117
|
+
if (sourceRepaired || metered !== void 0 && metered !== (((_c = connection2.source) == null ? void 0 : _c.metered) ?? void 0)) {
|
|
4118
|
+
writes.push({
|
|
4119
|
+
collection: "connection",
|
|
4120
|
+
data: {
|
|
4121
|
+
$set: sourceRepaired ? { source: { ...source, ...metered !== void 0 && { metered } } } : { "source.metered": metered }
|
|
4122
|
+
},
|
|
4123
|
+
operation: "update",
|
|
4124
|
+
query: { id: connection2.id }
|
|
4125
|
+
});
|
|
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) {
|
|
4093
4144
|
writes.push({
|
|
4094
4145
|
collection: "connection",
|
|
4095
4146
|
data: {
|
|
4096
|
-
$
|
|
4147
|
+
$pull: {
|
|
4148
|
+
errors: { source: BILLING_ERROR_SOURCE }
|
|
4149
|
+
}
|
|
4097
4150
|
},
|
|
4098
4151
|
operation: "update",
|
|
4099
4152
|
query: { id: connection2.id }
|
|
4100
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
|
+
});
|
|
4101
4166
|
}
|
|
4102
4167
|
return {
|
|
4103
4168
|
writes,
|
|
@@ -4113,11 +4178,12 @@ var shopify_default2 = {
|
|
|
4113
4178
|
options: { jobId: "connection.update.register." + workflow.connection + "." + randomUUID() },
|
|
4114
4179
|
queue: "connection"
|
|
4115
4180
|
}],
|
|
4116
|
-
message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered ===
|
|
4181
|
+
message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
|
|
4117
4182
|
request: request2,
|
|
4118
4183
|
response: {
|
|
4119
4184
|
billingProbeFailed,
|
|
4120
|
-
metered,
|
|
4185
|
+
metered: metered ?? null,
|
|
4186
|
+
meterProbed: metered !== void 0,
|
|
4121
4187
|
pingedAt: /* @__PURE__ */ new Date(),
|
|
4122
4188
|
refreshTokenExpiresAt: refreshTokenExpiresAt || null,
|
|
4123
4189
|
refreshTokenRotated,
|
|
@@ -4262,7 +4328,15 @@ var shopify_default2 = {
|
|
|
4262
4328
|
{ input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
|
|
4263
4329
|
{ input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
|
|
4264
4330
|
{ input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
|
|
4265
|
-
{ 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 }
|
|
4266
4340
|
]
|
|
4267
4341
|
},
|
|
4268
4342
|
// A pre-launch integration: it only surfaces once the App Store listing
|
|
@@ -4404,20 +4478,24 @@ var shopify_default2 = {
|
|
|
4404
4478
|
...(data2 == null ? void 0 : data2.status) === "pending" ? [
|
|
4405
4479
|
{
|
|
4406
4480
|
action: "billing",
|
|
4407
|
-
message: "Approve the Drawbridge plan to activate this connection. You'll
|
|
4408
|
-
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"
|
|
4409
4484
|
}
|
|
4410
4485
|
] : [],
|
|
4411
4486
|
// The store's ACTIVE approval carries no usage component — stamped by
|
|
4412
|
-
// the daily health check and the approval webhook (`source.metered
|
|
4413
|
-
//
|
|
4414
|
-
//
|
|
4415
|
-
//
|
|
4416
|
-
|
|
4487
|
+
// the daily health check and the approval webhook (`source.metered`:
|
|
4488
|
+
// the usage line's id when present, null when probed-and-absent).
|
|
4489
|
+
// Approved pricing is never retroactive, so the fix is a fresh
|
|
4490
|
+
// approval. Gated on an EXPLICIT null — a connection the probe hasn't
|
|
4491
|
+
// reached (or couldn't read) has no key and shows nothing rather than
|
|
4492
|
+
// nagging on silence.
|
|
4493
|
+
...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
|
|
4417
4494
|
{
|
|
4418
4495
|
action: "billing",
|
|
4419
|
-
message: "Order billing isn't set up for this store, so campaign-attributed orders aren't being charged.
|
|
4420
|
-
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"
|
|
4421
4499
|
}
|
|
4422
4500
|
] : []
|
|
4423
4501
|
];
|
|
@@ -4426,7 +4504,7 @@ var shopify_default2 = {
|
|
|
4426
4504
|
};
|
|
4427
4505
|
|
|
4428
4506
|
// lib/connections/providers/webhook.js
|
|
4429
|
-
import
|
|
4507
|
+
import crypto2 from "crypto";
|
|
4430
4508
|
|
|
4431
4509
|
// lib/safe-http.js
|
|
4432
4510
|
import dns2 from "dns";
|
|
@@ -4661,7 +4739,7 @@ var webhook_default = {
|
|
|
4661
4739
|
request2.body = body;
|
|
4662
4740
|
const outgoing = { ...headers };
|
|
4663
4741
|
if (settings == null ? void 0 : settings.secret) {
|
|
4664
|
-
outgoing["X-Drawbridge-Signature"] = "sha256=" +
|
|
4742
|
+
outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto2.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
|
|
4665
4743
|
}
|
|
4666
4744
|
const response = await send2({ body, headers: outgoing, method, url });
|
|
4667
4745
|
return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
|