@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.
@@ -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",
@@ -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
@@ -4281,9 +4354,18 @@ var shopify_default2 = {
4281
4354
  // there is nothing further to choose. `shop` absent means the install did not
4282
4355
  // finish, which is a credential problem rather than a setup one, so the
4283
4356
  // stored status already says so.
4284
- // Nothing to add — no setting can make this connection unusable, so the
4285
- // credential's own verdict stands.
4286
- status: (data2) => data2 == null ? void 0 : data2.status,
4357
+ //
4358
+ // ONE presentation downgrade: an ACTIVE store whose approval carries no
4359
+ // usage meter (source.metered explicitly null) PRESENTS as error, so the
4360
+ // connections list shows the red state and the merchant knows to click in
4361
+ // — where the Fix-order-billing card and the billing Issue explain the
4362
+ // click. The STORED status stays active on purpose: serving and tracking
4363
+ // gate on it, and the orders recorded while the merchant fixes the plan
4364
+ // are the ones billed afterward.
4365
+ status: (data2) => {
4366
+ var _a;
4367
+ 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;
4368
+ },
4287
4369
  // Step types name the CAPABILITY, not this vendor. A second store platform
4288
4370
  // implements the same four commerce steps, and the connection on the step
4289
4371
  // says which store it runs against — so a merchant sees one "Create
@@ -4405,8 +4487,9 @@ var shopify_default2 = {
4405
4487
  ...(data2 == null ? void 0 : data2.status) === "pending" ? [
4406
4488
  {
4407
4489
  action: "billing",
4408
- message: "Approve the Drawbridge plan to activate this connection. You'll confirm the pricing on Shopify, then everything else happens here.",
4409
- title: "Approve your plan"
4490
+ message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
4491
+ title: "Approve your plan",
4492
+ type: "warning"
4410
4493
  }
4411
4494
  ] : [],
4412
4495
  // The store's ACTIVE approval carries no usage component — stamped by
@@ -4419,8 +4502,9 @@ var shopify_default2 = {
4419
4502
  ...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
4420
4503
  {
4421
4504
  action: "billing",
4422
- 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.",
4423
- title: "Fix order billing"
4505
+ 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.",
4506
+ title: "Fix order billing",
4507
+ type: "error"
4424
4508
  }
4425
4509
  ] : []
4426
4510
  ];
@@ -4429,7 +4513,7 @@ var shopify_default2 = {
4429
4513
  };
4430
4514
 
4431
4515
  // lib/connections/providers/webhook.js
4432
- import crypto from "crypto";
4516
+ import crypto2 from "crypto";
4433
4517
 
4434
4518
  // lib/safe-http.js
4435
4519
  import dns2 from "dns";
@@ -4664,7 +4748,7 @@ var webhook_default = {
4664
4748
  request2.body = body;
4665
4749
  const outgoing = { ...headers };
4666
4750
  if (settings == null ? void 0 : settings.secret) {
4667
- outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4751
+ outgoing["X-Drawbridge-Signature"] = "sha256=" + crypto2.createHmac("sha256", settings.secret).update(JSON.stringify(body)).digest("hex");
4668
4752
  }
4669
4753
  const response = await send2({ body, headers: outgoing, method, url });
4670
4754
  return { message: "Webhook POSTed to " + url + ".", request: request2, response: response || { delivered: true } };
@@ -5026,6 +5110,11 @@ var publicConnectionKeys = Object.freeze([
5026
5110
  "settings",
5027
5111
  "shop",
5028
5112
  "slug",
5113
+ // Vendor identity plus the metered billing stamp — domain, id, label,
5114
+ // metered — no credentials live here (those are the encrypted settings
5115
+ // blob). The status and tasks hooks pivot on source.metered, so stripping
5116
+ // it here left the presented status blind to the one state it must show.
5117
+ "source",
5029
5118
  "status",
5030
5119
  "tasks",
5031
5120
  "title",
@@ -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: ORDER_EVENT_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: ORDER_EVENT_HANDLE + "." + orderId,
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
@@ -4261,9 +4353,18 @@ var shopify_default2 = {
4261
4353
  // there is nothing further to choose. `shop` absent means the install did not
4262
4354
  // finish, which is a credential problem rather than a setup one, so the
4263
4355
  // stored status already says so.
4264
- // Nothing to add — no setting can make this connection unusable, so the
4265
- // credential's own verdict stands.
4266
- status: (data2) => data2 == null ? void 0 : data2.status,
4356
+ //
4357
+ // ONE presentation downgrade: an ACTIVE store whose approval carries no
4358
+ // usage meter (source.metered explicitly null) PRESENTS as error, so the
4359
+ // connections list shows the red state and the merchant knows to click in
4360
+ // — where the Fix-order-billing card and the billing Issue explain the
4361
+ // click. The STORED status stays active on purpose: serving and tracking
4362
+ // gate on it, and the orders recorded while the merchant fixes the plan
4363
+ // are the ones billed afterward.
4364
+ status: (data2) => {
4365
+ var _a;
4366
+ 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;
4367
+ },
4267
4368
  // Step types name the CAPABILITY, not this vendor. A second store platform
4268
4369
  // implements the same four commerce steps, and the connection on the step
4269
4370
  // says which store it runs against — so a merchant sees one "Create
@@ -4385,8 +4486,9 @@ var shopify_default2 = {
4385
4486
  ...(data2 == null ? void 0 : data2.status) === "pending" ? [
4386
4487
  {
4387
4488
  action: "billing",
4388
- message: "Approve the Drawbridge plan to activate this connection. You'll confirm the pricing on Shopify, then everything else happens here.",
4389
- title: "Approve your plan"
4489
+ message: "Approve the Drawbridge plan to activate this connection. You'll be asked to select a plan on Shopify, then everything else happens here.",
4490
+ title: "Approve your plan",
4491
+ type: "warning"
4390
4492
  }
4391
4493
  ] : [],
4392
4494
  // The store's ACTIVE approval carries no usage component — stamped by
@@ -4399,8 +4501,9 @@ var shopify_default2 = {
4399
4501
  ...(data2 == null ? void 0 : data2.status) === "active" && ((_a = data2 == null ? void 0 : data2.source) == null ? void 0 : _a.metered) === null ? [
4400
4502
  {
4401
4503
  action: "billing",
4402
- 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.",
4403
- title: "Fix order billing"
4504
+ 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.",
4505
+ title: "Fix order billing",
4506
+ type: "error"
4404
4507
  }
4405
4508
  ] : []
4406
4509
  ];
@@ -4923,6 +5026,11 @@ var publicConnectionKeys = Object.freeze([
4923
5026
  "settings",
4924
5027
  "shop",
4925
5028
  "slug",
5029
+ // Vendor identity plus the metered billing stamp — domain, id, label,
5030
+ // metered — no credentials live here (those are the encrypted settings
5031
+ // blob). The status and tasks hooks pivot on source.metered, so stripping
5032
+ // it here left the presented status blind to the one state it must show.
5033
+ "source",
4926
5034
  "status",
4927
5035
  "tasks",
4928
5036
  "title",
@@ -4930,45 +5038,6 @@ var publicConnectionKeys = Object.freeze([
4930
5038
  "warnings"
4931
5039
  ]);
4932
5040
 
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
5041
  // lib/providers.js
4973
5042
  var providerFields = (slug2) => {
4974
5043
  var _a;
@@ -5008,9 +5077,10 @@ var saveProviderSettings = async ({ authenticated, clear, controller, settings,
5008
5077
  });
5009
5078
  const stored = (existing == null ? void 0 : existing.settings) ? decrypt(existing.settings) : {};
5010
5079
  const pasted = (value) => typeof value === "string" ? value.trim().replace(/^['"]+|['"]+$/g, "") : value;
5080
+ const normalized = (field2, value) => field2.format === "slug" && typeof value === "string" && value ? slugify(value) : value;
5011
5081
  const merged = mergeSettings({
5012
5082
  existing: stored,
5013
- incoming: Object.fromEntries(fields2.map((field2) => [field2.key, pasted(settings == null ? void 0 : settings[field2.key])]))
5083
+ incoming: Object.fromEntries(fields2.map((field2) => [field2.key, normalized(field2, pasted(settings == null ? void 0 : settings[field2.key]))]))
5014
5084
  });
5015
5085
  for (const key of Array.isArray(clear) ? clear : []) {
5016
5086
  if (fields2.some((field2) => field2.key === key)) delete merged[key];
@@ -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
@@ -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