@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.
@@ -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 : ORDER_EVENT_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 : ORDER_EVENT_HANDLE + '.' + orderId,
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: whether the merchant's ACTIVE
4870
- // approval carries a usage line item. A meter added to the app's
4871
- // pricing config is never retroactive it bills only from the
4872
- // moment the merchant approves the plan version that carries it —
4873
- // so an approval with no usage line means every order's usage event
4874
- // is 202-accepted and silently never billed. Stamped on the
4875
- // connection so tasks() can tell the merchant to re-approve, and
4876
- // probed every run so a re-approval clears the card within a day.
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
- let metered = source?.metered ?? null;
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 false from silence. The
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 false would nag every healthy
4920
- // merchant to re-approve a plan that is fine.
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.some( ( subscription ) => subscription.metered );
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
- if( sourceRepaired || ( metered !== null && metered !== ( connection.source?.metered ?? null ) ) ){
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 !== null && { 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 === false ? ' No usage line on the store\'s approved plan — order billing needs the merchant to approve the updated plan.' : '' ),
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 confirm the pricing on Shopify, then everything else happens here.',
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
- // Approved pricing is never retroactive, so the fix is a fresh approval.
5326
- // Gated on an EXPLICIT false a connection the probe hasn't reached
5327
- // (or couldn't read) shows nothing rather than nagging on silence.
5328
- ...( data?.status === 'active' && data?.source?.metered === false
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. Approve the updated plan to fix it one click here, one confirmation on Shopify.',
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: ORDER_EVENT_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: ORDER_EVENT_HANDLE + "." + orderId,
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 = (source == null ? void 0 : source.metered) ?? null;
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.some((subscription) => subscription.metered);
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
- (_a = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _a.call(logger2, "shopify.health.billing.probe.failed", {
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 !== null && metered !== (((_b = connection2.source) == null ? void 0 : _b.metered) ?? null)) {
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
- $set: sourceRepaired ? { source: { ...source, ...metered !== null && { metered } } } : { "source.metered": metered }
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 === false ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
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 confirm the pricing on Shopify, then everything else happens here.",
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
- // Approved pricing is never retroactive, so the fix is a fresh approval.
4414
- // Gated on an EXPLICIT false a connection the probe hasn't reached
4415
- // (or couldn't read) shows nothing rather than nagging on silence.
4416
- ...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === false ? [
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. Approve the updated plan to fix it \u2014 one click here, one confirmation on Shopify.",
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 crypto from "crypto";
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=" + crypto.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
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 } };