@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/connections/index.cjs +99 -10
- package/dist/connections/index.d.cts +118 -10
- package/dist/connections/index.d.ts +118 -10
- package/dist/connections/index.js +101 -12
- package/dist/providers.cjs +120 -50
- package/dist/providers.d.cts +14 -3
- package/dist/providers.d.ts +14 -3
- package/dist/providers.js +122 -52
- package/package.json +1 -1
|
@@ -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",
|
|
@@ -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
|
-
//
|
|
4285
|
-
//
|
|
4286
|
-
|
|
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
|
|
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.
|
|
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
|
|
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=" +
|
|
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",
|
package/dist/providers.cjs
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
//
|
|
4265
|
-
//
|
|
4266
|
-
|
|
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
|
|
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.
|
|
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];
|
package/dist/providers.d.cts
CHANGED
|
@@ -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
|
package/dist/providers.d.ts
CHANGED
|
@@ -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
|