@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/connections/index.cjs +98 -20
- package/dist/connections/index.d.cts +137 -30
- package/dist/connections/index.d.ts +137 -30
- package/dist/connections/index.js +100 -22
- package/dist/providers.cjs +119 -60
- package/dist/providers.d.cts +14 -3
- package/dist/providers.d.ts +14 -3
- package/dist/providers.js +121 -62
- package/package.json +1 -1
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
|
|
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:
|
|
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
|
|
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.
|
|
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
|
-
(
|
|
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 !==
|
|
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
|
-
$
|
|
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 ===
|
|
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
|
|
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
|
-
//
|
|
4350
|
-
//
|
|
4351
|
-
//
|
|
4352
|
-
|
|
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.
|
|
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
|
|
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=" +
|
|
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