@misiki/gocommerce-connector 0.28.0 → 0.30.0
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/README.md +6 -0
- package/dist/{index-BaFtBtaA.d.cts → index-BdXhbm20.d.cts} +145 -24
- package/dist/{index-BaFtBtaA.d.ts → index-BdXhbm20.d.ts} +145 -24
- package/dist/index.cjs +204 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +204 -27
- package/dist/index.js.map +1 -1
- package/dist/services/index.cjs +202 -26
- package/dist/services/index.cjs.map +1 -1
- package/dist/services/index.d.cts +1 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +202 -26
- package/dist/services/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -134,8 +134,17 @@ function remember(key, load, ttl = TTL_MS) {
|
|
|
134
134
|
});
|
|
135
135
|
return value;
|
|
136
136
|
}
|
|
137
|
+
var known = /* @__PURE__ */ new Map();
|
|
138
|
+
function keep(key, value) {
|
|
139
|
+
known.set(key, value);
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
142
|
+
function recall(key) {
|
|
143
|
+
return known.get(key);
|
|
144
|
+
}
|
|
137
145
|
function forgetAll() {
|
|
138
146
|
entries.clear();
|
|
147
|
+
known.clear();
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
// src/browser.ts
|
|
@@ -1120,7 +1129,22 @@ function toCartOffer(o, currency) {
|
|
|
1120
1129
|
applies: o.applies,
|
|
1121
1130
|
amount: o.amount ? toMajor(o.amount) : null,
|
|
1122
1131
|
remaining: o.remaining ? toMajor(o.remaining) : null,
|
|
1123
|
-
reason: o.reason ?? ""
|
|
1132
|
+
reason: o.reason ?? "",
|
|
1133
|
+
autoApply: !!o.auto_apply
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function toCheckoutQuote(q) {
|
|
1137
|
+
const d = q.discount;
|
|
1138
|
+
return {
|
|
1139
|
+
subtotal: toMajor(q.subtotal),
|
|
1140
|
+
shippingCharges: toMajor(q.shipping),
|
|
1141
|
+
shippingMethod: q.shipping_method ?? null,
|
|
1142
|
+
discount: toMajor(d?.amount),
|
|
1143
|
+
coupon: d ? { code: d.code, title: d.title, autoApplied: d.auto_applied, freeShipping: !!d.free_shipping } : null,
|
|
1144
|
+
tax: toMajor(q.tax),
|
|
1145
|
+
taxIncluded: q.tax_inclusive,
|
|
1146
|
+
codCharges: toMajor(q.payment_fee),
|
|
1147
|
+
total: toMajor(q.total)
|
|
1124
1148
|
};
|
|
1125
1149
|
}
|
|
1126
1150
|
function toFreeShipping(f) {
|
|
@@ -1593,7 +1617,23 @@ function attributesFrom(p) {
|
|
|
1593
1617
|
return Object.keys(out).length ? out : void 0;
|
|
1594
1618
|
}
|
|
1595
1619
|
var counts = (values) => Object.fromEntries((values ?? []).map((v) => [v.value, v.count]));
|
|
1596
|
-
function
|
|
1620
|
+
function categoryFacets(counted, tree) {
|
|
1621
|
+
const byId = new Map(tree.map((c) => [c.id, c]));
|
|
1622
|
+
const totals = /* @__PURE__ */ new Map();
|
|
1623
|
+
for (const { id: id2, count } of counted) {
|
|
1624
|
+
for (let c = byId.get(id2); c; c = c.parent_id === null ? void 0 : byId.get(c.parent_id)) {
|
|
1625
|
+
totals.set(c.id, (totals.get(c.id) ?? 0) + count);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const kept = tree.filter((c) => totals.has(c.id)).sort((a, b) => a.position - b.position || a.id - b.id);
|
|
1629
|
+
const parentOf = (c) => c.parent_id !== null && byId.has(c.parent_id) ? c.parent_id : null;
|
|
1630
|
+
const under = (parent) => kept.filter((c) => parentOf(c) === parent).flatMap((c) => [{ name: c.title, slug: c.slug, count: totals.get(c.id), depth: c.depth }, ...under(c.id)]);
|
|
1631
|
+
return [
|
|
1632
|
+
...under(null),
|
|
1633
|
+
...counted.filter((c) => !byId.has(c.id)).map((c) => ({ name: c.title, slug: c.slug, count: c.count, depth: 0 }))
|
|
1634
|
+
];
|
|
1635
|
+
}
|
|
1636
|
+
function toStorefrontFacets(f, tree = []) {
|
|
1597
1637
|
if (!f) return { priceStat: {}, categories: [], tags: [], allFilters: {} };
|
|
1598
1638
|
const allFilters = {};
|
|
1599
1639
|
const group = (key, values) => {
|
|
@@ -1604,7 +1644,7 @@ function toStorefrontFacets(f) {
|
|
|
1604
1644
|
for (const [handle, values] of Object.entries(f.attributes ?? {})) group(`attributes.${handle}`, values);
|
|
1605
1645
|
return {
|
|
1606
1646
|
priceStat: f.price ? { min: toMajor(f.price.min), max: toMajor(f.price.max) } : {},
|
|
1607
|
-
categories: (f.categories ?? []
|
|
1647
|
+
categories: categoryFacets(f.categories ?? [], tree),
|
|
1608
1648
|
tags: (f.tags ?? []).map((t) => ({ name: t.value, count: t.count })),
|
|
1609
1649
|
allFilters
|
|
1610
1650
|
};
|
|
@@ -1637,13 +1677,14 @@ var SearchService = class extends CatalogService {
|
|
|
1637
1677
|
productTypes: listParam(p, "product_type"),
|
|
1638
1678
|
facets: true
|
|
1639
1679
|
});
|
|
1680
|
+
const tree = res.facets?.categories?.length ? await this.facts.categories().catch(() => []) : [];
|
|
1640
1681
|
return {
|
|
1641
1682
|
data: res.data,
|
|
1642
1683
|
count: res.count,
|
|
1643
1684
|
totalPages: res.noOfPage,
|
|
1644
1685
|
page: res.page,
|
|
1645
1686
|
categoryHierarchy: res.categoryHierarchy,
|
|
1646
|
-
facets: toStorefrontFacets(res.facets)
|
|
1687
|
+
facets: toStorefrontFacets(res.facets, tree)
|
|
1647
1688
|
};
|
|
1648
1689
|
} catch (err) {
|
|
1649
1690
|
if (err?.status === 404) return this.emptyResult();
|
|
@@ -2111,6 +2152,7 @@ var customizerService = new CustomizerService();
|
|
|
2111
2152
|
|
|
2112
2153
|
// src/services/checkout-service.ts
|
|
2113
2154
|
var CheckoutService = class extends BaseService {
|
|
2155
|
+
expected = /* @__PURE__ */ new Map();
|
|
2114
2156
|
/** Delivery options for the address on the cart: `{ data: [{ id, name, base_rate }] }`. */
|
|
2115
2157
|
async getShippingRates({ cartId } = {}) {
|
|
2116
2158
|
const id2 = cartId || storedCartId();
|
|
@@ -2282,6 +2324,64 @@ var CheckoutService = class extends BaseService {
|
|
|
2282
2324
|
);
|
|
2283
2325
|
return { methods: options.methods ?? [], currency: options.currency };
|
|
2284
2326
|
}
|
|
2327
|
+
/**
|
|
2328
|
+
* What checkout would charge for the cart now, by the storefront's method
|
|
2329
|
+
* code (`COD`, `RAZORPAY`, `PAYPAL`) or none yet (D185). Null until the
|
|
2330
|
+
* address step has stored an address, and null from an engine before D185.
|
|
2331
|
+
*
|
|
2332
|
+
* A refusal is checkout's own: the same GoCommerceError, with the same
|
|
2333
|
+
* message, that placing the order would raise now.
|
|
2334
|
+
*/
|
|
2335
|
+
async getQuote({ cartId, method = "" } = {}) {
|
|
2336
|
+
const id2 = cartId || storedCartId();
|
|
2337
|
+
if (!id2 || !cartExtras(id2).shippingAddress) return null;
|
|
2338
|
+
const { extras, email, address } = await this.storedCheckout(id2);
|
|
2339
|
+
const code = method.toLowerCase() === "paypal" ? await this.redirectGateway() : method.toLowerCase();
|
|
2340
|
+
try {
|
|
2341
|
+
const quote2 = await this.data("/api/checkout/quote", {
|
|
2342
|
+
query: {
|
|
2343
|
+
cart: id2,
|
|
2344
|
+
country: address.country,
|
|
2345
|
+
state: address.state,
|
|
2346
|
+
shipping_rate_id: extras.shippingRateId || void 0,
|
|
2347
|
+
method: code || void 0,
|
|
2348
|
+
email: email || void 0
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
return toCheckoutQuote(quote2);
|
|
2352
|
+
} catch (err) {
|
|
2353
|
+
if (err instanceof GoCommerceError && (err.status === 405 || err.status === 404 && err.message.startsWith("no route for "))) {
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
throw err;
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* The total on screen as the shopper confirms, from getQuote, or null when nothing was quoted
|
|
2361
|
+
* (D185). Every order placed for the cart then carries it, and checkout refuses one whose own
|
|
2362
|
+
* total differs instead of charging it: `onChange` gets checkout's figures now and the sentence
|
|
2363
|
+
* the placing call fails with.
|
|
2364
|
+
*/
|
|
2365
|
+
expectTotal({
|
|
2366
|
+
cartId,
|
|
2367
|
+
total,
|
|
2368
|
+
onChange
|
|
2369
|
+
}) {
|
|
2370
|
+
const id2 = cartId || storedCartId();
|
|
2371
|
+
if (!id2) return;
|
|
2372
|
+
if (typeof total === "number") this.expected.set(id2, { total, onChange });
|
|
2373
|
+
else this.expected.delete(id2);
|
|
2374
|
+
}
|
|
2375
|
+
async storedCheckout(cartId) {
|
|
2376
|
+
const id2 = cartId || storedCartId();
|
|
2377
|
+
if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
|
|
2378
|
+
const extras = cartExtras(id2);
|
|
2379
|
+
const ship = extras.shippingAddress;
|
|
2380
|
+
if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
2381
|
+
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
2382
|
+
const email = cart.email || (ship.email ? String(ship.email) : "");
|
|
2383
|
+
return { id: id2, extras, email, address: toEngineAddress(ship), currency: cart.currency };
|
|
2384
|
+
}
|
|
2285
2385
|
async redirectGateway() {
|
|
2286
2386
|
const { methods } = await this.listMethods();
|
|
2287
2387
|
return methods.find((m) => REDIRECT_GATEWAYS.has(m.code))?.code;
|
|
@@ -2297,15 +2397,8 @@ var CheckoutService = class extends BaseService {
|
|
|
2297
2397
|
* the engine refuses a key replayed with a different body.
|
|
2298
2398
|
*/
|
|
2299
2399
|
async place(code, cartId, extra = {}) {
|
|
2300
|
-
const id2 =
|
|
2301
|
-
if (!id2) throw new GoCommerceError("Your bag is empty.", 400);
|
|
2302
|
-
const extras = cartExtras(id2);
|
|
2303
|
-
const ship = extras.shippingAddress;
|
|
2304
|
-
if (!ship) throw new GoCommerceError("Add a delivery address first.", 400);
|
|
2305
|
-
const cart = await this.data(`/api/carts/${encodeURIComponent(id2)}`);
|
|
2306
|
-
const email = cart.email || (ship.email ? String(ship.email) : "");
|
|
2400
|
+
const { id: id2, extras, email, address, currency } = await this.storedCheckout(cartId);
|
|
2307
2401
|
if (!email) throw new GoCommerceError("Add an email address so we can send your receipt.", 400);
|
|
2308
|
-
const address = toEngineAddress(ship);
|
|
2309
2402
|
const body = {
|
|
2310
2403
|
cart_id: id2,
|
|
2311
2404
|
email,
|
|
@@ -2328,10 +2421,23 @@ var CheckoutService = class extends BaseService {
|
|
|
2328
2421
|
...meta ? { meta_capi: meta } : {},
|
|
2329
2422
|
...attribution ? { attribution } : {}
|
|
2330
2423
|
};
|
|
2424
|
+
const expected = this.expected.get(id2);
|
|
2331
2425
|
const result = await this.data(`/api/checkout/${encodeURIComponent(code)}`, {
|
|
2332
2426
|
method: "POST",
|
|
2333
|
-
body:
|
|
2427
|
+
body: {
|
|
2428
|
+
...Object.keys(metadata).length ? { ...body, metadata } : body,
|
|
2429
|
+
...expected ? { quoted_total_minor: toMinor(expected.total, currency) } : {}
|
|
2430
|
+
},
|
|
2334
2431
|
headers: { "Idempotency-Key": key }
|
|
2432
|
+
}).catch((err) => {
|
|
2433
|
+
if (keyAlreadyUsed(err)) {
|
|
2434
|
+
throw new GoCommerceError("This order was already placed in another tab. Check that tab for its confirmation.", 400, err.code);
|
|
2435
|
+
}
|
|
2436
|
+
const now = movedTotal(err);
|
|
2437
|
+
if (!expected || !now) throw err;
|
|
2438
|
+
const message = `Your total changed to ${shopperAmount(now.total, currency)}. Check the new total and confirm again.`;
|
|
2439
|
+
expected.onChange?.(now, message);
|
|
2440
|
+
throw new GoCommerceError(message, 409, "total_changed", now);
|
|
2335
2441
|
});
|
|
2336
2442
|
if (result.order.access_token) {
|
|
2337
2443
|
rememberOrder({
|
|
@@ -2354,6 +2460,21 @@ var CheckoutService = class extends BaseService {
|
|
|
2354
2460
|
};
|
|
2355
2461
|
var REDIRECT_GATEWAYS = /* @__PURE__ */ new Set(["adyen", "paddle", "lemonsqueezy", "creem", "hyperswitch", "revenuecat"]);
|
|
2356
2462
|
var unsupported = (method, why) => new NotSupportedError("CheckoutService", method, why);
|
|
2463
|
+
function keyAlreadyUsed(err) {
|
|
2464
|
+
return err instanceof GoCommerceError && err.status === 400 && err.message.includes("Idempotency-Key was already used");
|
|
2465
|
+
}
|
|
2466
|
+
function movedTotal(err) {
|
|
2467
|
+
if (!(err instanceof GoCommerceError) || err.code !== "total_changed") return null;
|
|
2468
|
+
const now = err.details;
|
|
2469
|
+
return now?.total ? toCheckoutQuote(now) : null;
|
|
2470
|
+
}
|
|
2471
|
+
function shopperAmount(amount, currency) {
|
|
2472
|
+
return new Intl.NumberFormat(currency === "INR" ? "en-IN" : "en", {
|
|
2473
|
+
style: "currency",
|
|
2474
|
+
currency,
|
|
2475
|
+
...Number.isInteger(amount) ? { minimumFractionDigits: 0 } : {}
|
|
2476
|
+
}).format(amount);
|
|
2477
|
+
}
|
|
2357
2478
|
function newKey() {
|
|
2358
2479
|
const c = globalThis.crypto;
|
|
2359
2480
|
if (c?.randomUUID) return c.randomUUID();
|
|
@@ -2415,7 +2536,8 @@ var CouponService = class extends BaseService {
|
|
|
2415
2536
|
minAmount: o.minAmount,
|
|
2416
2537
|
description: o.applies ? o.title : o.remaining !== null ? `${o.title}. Add ${o.remaining} more to use this.` : o.reason || o.title,
|
|
2417
2538
|
applies: o.applies,
|
|
2418
|
-
remaining: o.remaining
|
|
2539
|
+
remaining: o.remaining,
|
|
2540
|
+
autoApply: o.autoApply
|
|
2419
2541
|
}));
|
|
2420
2542
|
return { data, count: data.length, pageSize: data.length, noOfPage: 1, page: 1 };
|
|
2421
2543
|
}
|
|
@@ -3168,15 +3290,18 @@ var StoreService = class extends BaseService {
|
|
|
3168
3290
|
async getStoreByIdOrDomain(_args = {}) {
|
|
3169
3291
|
const base = await readStaticStore();
|
|
3170
3292
|
const store = this.store;
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
if (isStoreMissing(err)) {
|
|
3293
|
+
const [plugins, live] = await Promise.all([
|
|
3294
|
+
this.facts.plugins().catch((err) => {
|
|
3295
|
+
if (store && isStoreMissing(err)) {
|
|
3174
3296
|
throw new GoCommerceError(`store not found: no store "${store}" on this engine`, 404, "store_not_found");
|
|
3175
3297
|
}
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3298
|
+
return void 0;
|
|
3299
|
+
}),
|
|
3300
|
+
this.live().catch(() => ({}))
|
|
3301
|
+
]);
|
|
3302
|
+
const known2 = `${this.creds.apiUrl ?? ""}|${store ?? ""}|closed`;
|
|
3303
|
+
const closed = plugins ? keep(known2, closedOf(plugins)) : recall(known2);
|
|
3304
|
+
return { ...base, ...live, closed, id: base.id || "gocommerce" };
|
|
3180
3305
|
}
|
|
3181
3306
|
async getStore(storeId) {
|
|
3182
3307
|
return this.getStoreByIdOrDomain({ storeId });
|
|
@@ -3211,6 +3336,46 @@ var StoreService = class extends BaseService {
|
|
|
3211
3336
|
}
|
|
3212
3337
|
return super.post(path, data);
|
|
3213
3338
|
}
|
|
3339
|
+
/**
|
|
3340
|
+
* A closed store's password (D170), traded for a pass. Never throws. The
|
|
3341
|
+
* engine allows 20 tries an hour per client, and sees this server as the
|
|
3342
|
+
* client unless `clientAddress`, the shopper's, travels as `X-Forwarded-For`.
|
|
3343
|
+
*/
|
|
3344
|
+
async unlockStore(password, { clientAddress } = {}) {
|
|
3345
|
+
const typed = password.trim();
|
|
3346
|
+
if (!typed) return { outcome: "wrong" };
|
|
3347
|
+
try {
|
|
3348
|
+
return { outcome: "unlocked", pass: await this.storePass({ password: typed }, clientAddress) };
|
|
3349
|
+
} catch (err) {
|
|
3350
|
+
return { outcome: err instanceof GoCommerceError && UNLOCK_REFUSED[err.status] || "unavailable" };
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* A pass checked without spending a try: the pass, or null when it is wrong,
|
|
3355
|
+
* expired, ended by a new password, or the store takes none now. Throws when
|
|
3356
|
+
* the engine cannot say.
|
|
3357
|
+
*/
|
|
3358
|
+
async checkStorePass(token) {
|
|
3359
|
+
if (!token) return null;
|
|
3360
|
+
try {
|
|
3361
|
+
return await this.storePass({ token });
|
|
3362
|
+
} catch (err) {
|
|
3363
|
+
if (err instanceof GoCommerceError && (err.status === 422 || err.status === 404)) return null;
|
|
3364
|
+
throw err;
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
async storePass(body, clientAddress) {
|
|
3368
|
+
const answer = await this.data("/api/store/password", {
|
|
3369
|
+
method: "POST",
|
|
3370
|
+
body,
|
|
3371
|
+
headers: clientAddress && ONE_IP_ADDRESS.test(clientAddress) ? { "X-Forwarded-For": clientAddress } : void 0
|
|
3372
|
+
});
|
|
3373
|
+
const expiresAt = new Date(answer?.expires_at ?? NaN);
|
|
3374
|
+
if (!answer?.token || Number.isNaN(expiresAt.getTime())) {
|
|
3375
|
+
throw new GoCommerceError("The commerce engine answered a store pass without its token or expiry.", 502);
|
|
3376
|
+
}
|
|
3377
|
+
return { token: answer.token, expiresAt };
|
|
3378
|
+
}
|
|
3214
3379
|
async live() {
|
|
3215
3380
|
const [ready, plugins, menus, base, profile, ships] = await Promise.all([
|
|
3216
3381
|
this.facts.ready(),
|
|
@@ -3328,6 +3493,17 @@ var StoreService = class extends BaseService {
|
|
|
3328
3493
|
}
|
|
3329
3494
|
};
|
|
3330
3495
|
var storeService = new StoreService();
|
|
3496
|
+
var ONE_IP_ADDRESS = /^[\da-f.:]{2,45}$/i;
|
|
3497
|
+
var UNLOCK_REFUSED = {
|
|
3498
|
+
422: "wrong",
|
|
3499
|
+
429: "throttled",
|
|
3500
|
+
404: "open"
|
|
3501
|
+
};
|
|
3502
|
+
function closedOf(plugins) {
|
|
3503
|
+
const plugin = plugins?.get("store-close");
|
|
3504
|
+
if (!plugin) return void 0;
|
|
3505
|
+
return { message: String(plugin.settings?.message ?? ""), password: plugin.settings?.password === true };
|
|
3506
|
+
}
|
|
3331
3507
|
var AD_TAGS = [
|
|
3332
3508
|
["meta-pixel", "pixel_id", "facebookPixel"],
|
|
3333
3509
|
["microsoft-clarity", "project_id", "microsoftClarity"],
|
|
@@ -3356,10 +3532,10 @@ function googleTags(plugins) {
|
|
|
3356
3532
|
}
|
|
3357
3533
|
function deliverable(base, ships) {
|
|
3358
3534
|
if (ships.everywhere || !ships.countries.length) return {};
|
|
3359
|
-
const
|
|
3360
|
-
const countries = ships.countries.map((code) =>
|
|
3361
|
-
const
|
|
3362
|
-
return { countries, ...
|
|
3535
|
+
const known2 = new Map((base.countries ?? []).map((c) => [String(c.code).toUpperCase(), c]));
|
|
3536
|
+
const countries = ships.countries.map((code) => known2.get(code) ?? { code, name: regionName(code) });
|
|
3537
|
+
const keep2 = base.country && ships.countries.includes(String(base.country.code).toUpperCase());
|
|
3538
|
+
return { countries, ...keep2 ? {} : { country: countries[0] } };
|
|
3363
3539
|
}
|
|
3364
3540
|
function regionName(code) {
|
|
3365
3541
|
try {
|
|
@@ -3514,7 +3690,8 @@ function toSection(s) {
|
|
|
3514
3690
|
title: c.title,
|
|
3515
3691
|
slug: c.slug,
|
|
3516
3692
|
link: `/collections/${c.slug}`,
|
|
3517
|
-
image: mediaUrl(c.image) ?? null
|
|
3693
|
+
image: mediaUrl(c.image) ?? null,
|
|
3694
|
+
description: c.description || null
|
|
3518
3695
|
}));
|
|
3519
3696
|
}
|
|
3520
3697
|
return out;
|
|
@@ -4233,6 +4410,7 @@ exports.toCart = toCart;
|
|
|
4233
4410
|
exports.toCartLine = toCartLine;
|
|
4234
4411
|
exports.toCartOffer = toCartOffer;
|
|
4235
4412
|
exports.toCategory = toCategory;
|
|
4413
|
+
exports.toCheckoutQuote = toCheckoutQuote;
|
|
4236
4414
|
exports.toCmsPage = toCmsPage;
|
|
4237
4415
|
exports.toCollection = toCollection;
|
|
4238
4416
|
exports.toEngineAddress = toEngineAddress;
|