@drawbridge/drawbridge-utils 0.0.143 → 0.0.145

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",
@@ -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
@@ -4217,9 +4309,18 @@ var shopify_default2 = {
4217
4309
  // there is nothing further to choose. `shop` absent means the install did not
4218
4310
  // finish, which is a credential problem rather than a setup one, so the
4219
4311
  // stored status already says so.
4220
- // Nothing to add — no setting can make this connection unusable, so the
4221
- // credential's own verdict stands.
4222
- status: (data2) => data2 == null ? void 0 : data2.status,
4312
+ //
4313
+ // ONE presentation downgrade: an ACTIVE store whose approval carries no
4314
+ // usage meter (source.metered explicitly null) PRESENTS as error, so the
4315
+ // connections list shows the red state and the merchant knows to click in
4316
+ // — where the Fix-order-billing card and the billing Issue explain the
4317
+ // click. The STORED status stays active on purpose: serving and tracking
4318
+ // gate on it, and the orders recorded while the merchant fixes the plan
4319
+ // are the ones billed afterward.
4320
+ status: (data2) => {
4321
+ var _a;
4322
+ return (data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? "error" : data2 == null ? void 0 : data2.status;
4323
+ },
4223
4324
  // Step types name the CAPABILITY, not this vendor. A second store platform
4224
4325
  // implements the same four commerce steps, and the connection on the step
4225
4326
  // says which store it runs against — so a merchant sees one "Create
@@ -4341,8 +4442,9 @@ var shopify_default2 = {
4341
4442
  ...(data2 == null ? void 0 : data2.status) === "pending" ? [
4342
4443
  {
4343
4444
  action: "billing",
4344
- message: "Approve the Drawbridge plan to activate this connection. You'll confirm the pricing on Shopify, then everything else happens here.",
4345
- title: "Approve your plan"
4445
+ message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
4446
+ title: "Approve your plan",
4447
+ type: "warning"
4346
4448
  }
4347
4449
  ] : [],
4348
4450
  // The store's ACTIVE approval carries no usage component — stamped by
@@ -4355,8 +4457,9 @@ var shopify_default2 = {
4355
4457
  ...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
4356
4458
  {
4357
4459
  action: "billing",
4358
- 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.",
4359
- title: "Fix order billing"
4460
+ 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.",
4461
+ title: "Fix order billing",
4462
+ type: "error"
4360
4463
  }
4361
4464
  ] : []
4362
4465
  ];
@@ -4365,7 +4468,7 @@ var shopify_default2 = {
4365
4468
  };
4366
4469
 
4367
4470
  // lib/connections/providers/webhook.js
4368
- import crypto from "crypto";
4471
+ import crypto3 from "crypto";
4369
4472
 
4370
4473
  // lib/safe-http.js
4371
4474
  import dns2 from "dns";
@@ -4600,7 +4703,7 @@ var webhook_default = {
4600
4703
  request2.body = body;
4601
4704
  const outgoing = { ...headers };
4602
4705
  if (settings == null ? void 0 : settings.secret) {
4603
- outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4706
+ outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto3.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4604
4707
  }
4605
4708
  const response = await send2({ body, headers: outgoing, method, url });
4606
4709
  return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
@@ -4879,6 +4982,11 @@ var publicConnectionKeys = Object.freeze([
4879
4982
  "settings",
4880
4983
  "shop",
4881
4984
  "slug",
4985
+ // Vendor identity plus the metered billing stamp — domain, id, label,
4986
+ // metered — no credentials live here (those are the encrypted settings
4987
+ // blob). The status and tasks hooks pivot on source.metered, so stripping
4988
+ // it here left the presented status blind to the one state it must show.
4989
+ "source",
4882
4990
  "status",
4883
4991
  "tasks",
4884
4992
  "title",
@@ -4886,45 +4994,6 @@ var publicConnectionKeys = Object.freeze([
4886
4994
  "warnings"
4887
4995
  ]);
4888
4996
 
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
4997
  // lib/providers.js
4929
4998
  var providerFields = (slug2) => {
4930
4999
  var _a;
@@ -4964,9 +5033,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
4964
5033
  });
4965
5034
  const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
4966
5035
  const pasted = (value) => typeof value === "string" ? value.trim().replace(/^['"]+|['"]+$/g, "") : value;
5036
+ const normalized = (field2, value) => field2.format === "slug" && typeof value === "string" && value ? slugify(value) : value;
4967
5037
  const merged = mergeSettings({
4968
5038
  existing: stored,
4969
- incoming: Object.fromEntries(fields2.map((field2) => [field2.key, pasted(settings == null ? void 0 : settings[field2.key])]))
5039
+ incoming: Object.fromEntries(fields2.map((field2) => [field2.key, normalized(field2, pasted(settings == null ? void 0 : settings[field2.key]))]))
4970
5040
  });
4971
5041
  for (const key of Array.isArray(clear) ? clear : []) {
4972
5042
  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.143"
219
+ "version": "0.0.145"
220
220
  }