@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/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: ORDER_EVENT_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: ORDER_EVENT_HANDLE + "." + orderId,
3877
+ transaction: handle + "." + orderId,
3834
3878
  value: Math.round(fee * 100)
3835
3879
  },
3836
3880
  name: "billing",
@@ -3974,7 +4018,7 @@ var shopify_default2 = {
3974
4018
  // against the ones the app now needs, and queues a webhook
3975
4019
  // reconciliation.
3976
4020
  health: async ({ connection: connection2, workflow }, { adminToken, logger: logger2, read, reconcileScopes, resolveSettings, rotateToken, shopify } = {}) => {
3977
- var _a, _b;
4021
+ var _a, _b, _c;
3978
4022
  const request2 = {
3979
4023
  connectionId: workflow.connection,
3980
4024
  organizationId: workflow.organization,
@@ -3996,7 +4040,7 @@ var shopify_default2 = {
3996
4040
  const writes = [];
3997
4041
  let source = connection2.source || null;
3998
4042
  let sourceRepaired = false;
3999
- let metered = (source == null ? void 0 : source.metered) ?? null;
4043
+ let metered;
4000
4044
  let shopCurrency = null;
4001
4045
  let billingProbeFailed = false;
4002
4046
  try {
@@ -4015,26 +4059,66 @@ var shopify_default2 = {
4015
4059
  domain: connection2.shop
4016
4060
  });
4017
4061
  if (subscriptions.length && subscriptions.every((subscription) => typeof subscription.metered === "boolean")) {
4018
- metered = subscriptions.some((subscription) => subscription.metered);
4062
+ metered = ((_a = subscriptions.find((subscription) => subscription.usageLineItemId)) == null ? void 0 : _a.usageLineItemId) || null;
4019
4063
  }
4020
4064
  } catch (probeError) {
4021
4065
  billingProbeFailed = true;
4022
- (_a = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _a.call(logger2, "shopify.health.billing.probe.failed", {
4066
+ (_b = logger2 == null ? void 0 : logger2.warn) == null ? void 0 : _b.call(logger2, "shopify.health.billing.probe.failed", {
4023
4067
  connectionId: connection2.id,
4024
4068
  message: probeError == null ? void 0 : probeError.message,
4025
4069
  shop: connection2.shop
4026
4070
  });
4027
4071
  }
4028
- if (sourceRepaired || metered !== null && metered !== (((_b = connection2.source) == null ? void 0 : _b.metered) ?? null)) {
4072
+ if (sourceRepaired || metered !== void 0 && metered !== (((_c = connection2.source) == null ? void 0 : _c.metered) ?? void 0)) {
4073
+ writes.push({
4074
+ collection: "connection",
4075
+ data: {
4076
+ $set: sourceRepaired ? { source: { ...source, ...metered !== void 0 && { metered } } } : { "source.metered": metered }
4077
+ },
4078
+ operation: "update",
4079
+ query: { id: connection2.id }
4080
+ });
4081
+ }
4082
+ const hasBillingIssue = (connection2.errors || []).some((entry) => entry.source === BILLING_ERROR_SOURCE);
4083
+ if (metered === null && !hasBillingIssue) {
4029
4084
  writes.push({
4030
4085
  collection: "connection",
4031
4086
  data: {
4032
- $set: sourceRepaired ? { source: { ...source, ...metered !== null && { metered } } } : { "source.metered": metered }
4087
+ $push: {
4088
+ errors: {
4089
+ message: BILLING_ERROR_MESSAGE,
4090
+ source: BILLING_ERROR_SOURCE
4091
+ }
4092
+ }
4033
4093
  },
4034
4094
  operation: "update",
4035
4095
  query: { id: connection2.id }
4036
4096
  });
4037
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: [{
@@ -4049,11 +4133,12 @@ var shopify_default2 = {
4049
4133
  options: { jobId: "connection.update.register." + workflow.connection + "." + randomUUID() },
4050
4134
  queue: "connection"
4051
4135
  }],
4052
- 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." : ""),
4136
+ 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." : ""),
4053
4137
  request: request2,
4054
4138
  response: {
4055
4139
  billingProbeFailed,
4056
- metered,
4140
+ metered: metered ?? null,
4141
+ meterProbed: metered !== void 0,
4057
4142
  pingedAt: /* @__PURE__ */ new Date(),
4058
4143
  refreshTokenExpiresAt: refreshTokenExpiresAt || null,
4059
4144
  refreshTokenRotated,
@@ -4198,7 +4283,15 @@ var shopify_default2 = {
4198
4283
  { input: "text", key: "apiKey", credential: "SHOPIFY_API_KEY", label: "API key", required: true },
4199
4284
  { input: "password", key: "apiSecret", credential: "SHOPIFY_API_SECRET", label: "API secret", redact: true, required: true },
4200
4285
  { input: "text", key: "appHandle", credential: "SHOPIFY_APP_HANDLE", label: "App handle", required: true },
4201
- { 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 }
4202
4295
  ]
4203
4296
  },
4204
4297
  // A pre-launch integration: it only surfaces once the App Store listing
@@ -4340,20 +4433,24 @@ var shopify_default2 = {
4340
4433
  ...(data2 == null ? void 0 : data2.status) === "pending" ? [
4341
4434
  {
4342
4435
  action: "billing",
4343
- message: "Approve the Drawbridge plan to activate this connection. You'll confirm the pricing on Shopify, then everything else happens here.",
4344
- 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"
4345
4439
  }
4346
4440
  ] : [],
4347
4441
  // The store's ACTIVE approval carries no usage component — stamped by
4348
- // the daily health check and the approval webhook (`source.metered`).
4349
- // Approved pricing is never retroactive, so the fix is a fresh approval.
4350
- // Gated on an EXPLICIT false a connection the probe hasn't reached
4351
- // (or couldn't read) shows nothing rather than nagging on silence.
4352
- ...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === false ? [
4442
+ // the daily health check and the approval webhook (`source.metered`:
4443
+ // the usage line's id when present, null when probed-and-absent).
4444
+ // Approved pricing is never retroactive, so the fix is a fresh
4445
+ // approval. Gated on an EXPLICIT null a connection the probe hasn't
4446
+ // reached (or couldn't read) has no key and shows nothing rather than
4447
+ // nagging on silence.
4448
+ ...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
4353
4449
  {
4354
4450
  action: "billing",
4355
- 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.",
4356
- 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"
4357
4454
  }
4358
4455
  ] : []
4359
4456
  ];
@@ -4362,7 +4459,7 @@ var shopify_default2 = {
4362
4459
  };
4363
4460
 
4364
4461
  // lib/connections/providers/webhook.js
4365
- import crypto from "crypto";
4462
+ import crypto3 from "crypto";
4366
4463
 
4367
4464
  // lib/safe-http.js
4368
4465
  import dns2 from "dns";
@@ -4597,7 +4694,7 @@ var webhook_default = {
4597
4694
  request2.body = body;
4598
4695
  const outgoing = { ...headers };
4599
4696
  if (settings == null ? void 0 : settings.secret) {
4600
- outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4697
+ outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto3.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4601
4698
  }
4602
4699
  const response = await send2({ body, headers: outgoing, method, url });
4603
4700
  return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
@@ -4883,45 +4980,6 @@ var publicConnectionKeys = Object.freeze([
4883
4980
  "warnings"
4884
4981
  ]);
4885
4982
 
4886
- // lib/encrypt.js
4887
- import crypto3 from "crypto";
4888
-
4889
- // lib/token.js
4890
- import crypto2 from "crypto";
4891
- var generate = (bytes = 32, encoding = "base64url") => {
4892
- const buf = crypto2.randomBytes(bytes);
4893
- return encoding ? buf.toString(encoding) : buf;
4894
- };
4895
-
4896
- // lib/encrypt.js
4897
- var ALGORITHM = "aes-256-gcm";
4898
- var getKey = () => Buffer.from(process.env.ENCRYPT_CONNECTION_SECRET, "hex");
4899
- var encrypt = (value) => {
4900
- const iv = generate(12, null);
4901
- const cipher = crypto3.createCipheriv(ALGORITHM, getKey(), iv);
4902
- const data2 = Buffer.concat([
4903
- cipher.update(JSON.stringify(value), "utf8"),
4904
- cipher.final()
4905
- ]);
4906
- const tag = cipher.getAuthTag();
4907
- return [iv, tag, data2].map((b) => b.toString("hex")).join(":");
4908
- };
4909
- var decrypt = (value) => {
4910
- if (typeof value !== "string") return value;
4911
- const [ivHex, tagHex, dataHex] = value.split(":");
4912
- const decipher = crypto3.createDecipheriv(
4913
- ALGORITHM,
4914
- getKey(),
4915
- Buffer.from(ivHex, "hex")
4916
- );
4917
- decipher.setAuthTag(Buffer.from(tagHex, "hex"));
4918
- const result = Buffer.concat([
4919
- decipher.update(Buffer.from(dataHex, "hex")),
4920
- decipher.final()
4921
- ]);
4922
- return JSON.parse(result.toString("utf8"));
4923
- };
4924
-
4925
4983
  // lib/providers.js
4926
4984
  var providerFields = (slug2) => {
4927
4985
  var _a;
@@ -4961,9 +5019,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
4961
5019
  });
4962
5020
  const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
4963
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;
4964
5023
  const merged = mergeSettings({
4965
5024
  existing: stored,
4966
- 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]))]))
4967
5026
  });
4968
5027
  for (const key of Array.isArray(clear) ? clear : []) {
4969
5028
  if (fields2.some((field2) => field2.key === key)) delete merged[key];
package/package.json CHANGED
@@ -216,5 +216,5 @@
216
216
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
217
217
  },
218
218
  "types": "dist/index.d.ts",
219
- "version": "0.0.142"
219
+ "version": "0.0.144"
220
220
  }