@asteby/storefront 0.6.0 → 0.7.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 +34 -0
- package/dist/index.cjs +72 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +74 -1
- package/dist/index.d.ts +74 -1
- package/dist/index.js +66 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -237,6 +237,38 @@ paymentErrorView(error) // network (re
|
|
|
237
237
|
- `promoProgressView(cart.promotions, locale)` and `rewardLabel(reward, locale)` only format what OPS sends (`remaining`, `percent`, `reached`); never subtract or divide in the shop.
|
|
238
238
|
- The mock serves `POST /checkouts/:id/payment` with outcomes by token (`tok_APRO` paid, `tok_FUND`/`tok_SECU`/`tok_EXPI`/`tok_FORM`/`tok_CALL`/`tok_OTHE` rejected, `tok_CONT` in review, `tok_3DS` challenge, `method: 'cash'` voucher), idempotency, `price_mismatch` 422, attempts cap 429 and `409` while another attempt is paid/pending; plus `PUT /carts/:id/note`, `GET /carts/:id/recommendations` and `GET /promotions`.
|
|
239
239
|
|
|
240
|
+
## Domain and market (single-customer storefront)
|
|
241
|
+
|
|
242
|
+
A storefront that belongs to ONE organization sends its verified domain plus the
|
|
243
|
+
market it is serving. OPS resolves the organization from the domain and honors
|
|
244
|
+
the market when that organization has it active (otherwise it falls back to the
|
|
245
|
+
domain's own market).
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { createStorefrontClient, storefrontDomainHeader } from '@asteby/storefront'
|
|
249
|
+
|
|
250
|
+
const client = createStorefrontClient({
|
|
251
|
+
baseUrl: 'https://ops.example.com/api/gw',
|
|
252
|
+
token: process.env.STOREFRONT_SK!,
|
|
253
|
+
domain: 'godsnutrition.com', // verified domain of the organization
|
|
254
|
+
market: 'mx', // -> X-Storefront-Domain: godsnutrition.com#mx
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
storefrontDomainHeader('godsnutrition.com', 'MX') // 'godsnutrition.com#mx'
|
|
258
|
+
storefrontDomainHeader('godsnutrition.com') // 'godsnutrition.com'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Per-request market (one domain, path prefixes `/mx`, `/us`): create the client
|
|
262
|
+
per request with that request's `market`, or build the header yourself with
|
|
263
|
+
`storefrontDomainHeader` and pass it in `headers`.
|
|
264
|
+
|
|
265
|
+
### Market detection and hreflang
|
|
266
|
+
|
|
267
|
+
Pure helpers, no DOM: `visitorCountry(headers)` (CDN country headers),
|
|
268
|
+
`preferredLanguages` / `negotiateLocale` (Accept-Language), `suggestMarket`
|
|
269
|
+
(suggests, never redirects; respects crawlers and a saved choice; country wins
|
|
270
|
+
over language) and `hreflangLinks` for per-market SEO.
|
|
271
|
+
|
|
240
272
|
## API
|
|
241
273
|
|
|
242
274
|
`createStorefrontClient(options)`:
|
|
@@ -249,6 +281,8 @@ paymentErrorView(error) // network (re
|
|
|
249
281
|
| `clientIp?` | Shopper IP for server-side calls → `X-Storefront-Client-IP` (see below) |
|
|
250
282
|
| `fetch?` | Custom fetch (default `globalThis.fetch`) |
|
|
251
283
|
| `locale?` | `Accept-Language` |
|
|
284
|
+
| `domain?` | Public host of the storefront → `X-Storefront-Domain` (see below) |
|
|
285
|
+
| `market?` | Market code (`mx`, `us`) → `X-Storefront-Domain: <domain>#<market>`; ignored without `domain` |
|
|
252
286
|
| `headers?` | Extra headers on every request |
|
|
253
287
|
| `timeoutMs?` | Abort after N ms (`StorefrontError` code `timeout`) |
|
|
254
288
|
| `onRequest?(ctx)` | Runs before each request; return a `Response` to short-circuit (SSR cache hit) |
|
package/dist/index.cjs
CHANGED
|
@@ -205,6 +205,7 @@ function createStorefrontClient(config) {
|
|
|
205
205
|
headers.set("Accept", "application/json");
|
|
206
206
|
headers.set("Authorization", `Bearer ${config.token}`);
|
|
207
207
|
if (config.locale) headers.set("Accept-Language", config.locale);
|
|
208
|
+
if (config.domain) headers.set("X-Storefront-Domain", storefrontDomainHeader(config.domain, config.market));
|
|
208
209
|
const customerToken = options.customerToken !== void 0 ? options.customerToken : config.customerToken;
|
|
209
210
|
if (customerToken) headers.set("X-Customer-Token", customerToken);
|
|
210
211
|
const clientIp = options.clientIp !== void 0 ? options.clientIp : config.clientIp;
|
|
@@ -407,6 +408,11 @@ function createStorefrontClient(config) {
|
|
|
407
408
|
};
|
|
408
409
|
return client;
|
|
409
410
|
}
|
|
411
|
+
function storefrontDomainHeader(domain, market) {
|
|
412
|
+
const host = domain.trim();
|
|
413
|
+
const code = (market ?? "").trim().toLowerCase();
|
|
414
|
+
return code && /^[a-z0-9_-]{1,32}$/.test(code) ? `${host}#${code}` : host;
|
|
415
|
+
}
|
|
410
416
|
|
|
411
417
|
// src/availability.ts
|
|
412
418
|
var ES = {
|
|
@@ -1045,7 +1051,67 @@ function promoProgressView(promotions, locale, overrides) {
|
|
|
1045
1051
|
return { discount, shipping };
|
|
1046
1052
|
}
|
|
1047
1053
|
|
|
1054
|
+
// src/market.ts
|
|
1055
|
+
var COUNTRY_HEADERS = ["cf-ipcountry", "cloudfront-viewer-country", "x-vercel-ip-country", "x-appengine-country", "x-country-code"];
|
|
1056
|
+
var BOT = /bot|crawl|spider|slurp|bing|facebookexternalhit|embedly|preview|lighthouse|headless|curl|wget/i;
|
|
1057
|
+
function visitorCountry(headers, { trustProxy = true } = {}) {
|
|
1058
|
+
if (!trustProxy) return null;
|
|
1059
|
+
for (const name of COUNTRY_HEADERS) {
|
|
1060
|
+
const value = headers.get(name)?.trim().toUpperCase();
|
|
1061
|
+
if (value && /^[A-Z]{2}$/.test(value) && value !== "XX" && value !== "T1") return value;
|
|
1062
|
+
}
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
function preferredLanguages(acceptLanguage) {
|
|
1066
|
+
if (!acceptLanguage) return [];
|
|
1067
|
+
return acceptLanguage.split(",").map((part, index) => {
|
|
1068
|
+
const [tag = "", ...params] = part.trim().split(";");
|
|
1069
|
+
const q = Number(params.find((p) => p.trim().startsWith("q="))?.split("=")[1] ?? 1);
|
|
1070
|
+
return { tag: tag.trim().toLowerCase(), q: Number.isFinite(q) ? q : 0, index };
|
|
1071
|
+
}).filter((l) => l.tag && l.tag !== "*" && l.q > 0).sort((a, b) => b.q - a.q || a.index - b.index).map((l) => l.tag);
|
|
1072
|
+
}
|
|
1073
|
+
function negotiateLocale(acceptLanguage, available, fallback) {
|
|
1074
|
+
const lowered = new Map(available.map((l) => [l.toLowerCase(), l]));
|
|
1075
|
+
for (const wanted of preferredLanguages(acceptLanguage)) {
|
|
1076
|
+
const exact = lowered.get(wanted);
|
|
1077
|
+
if (exact) return exact;
|
|
1078
|
+
const base = wanted.split("-")[0];
|
|
1079
|
+
const sameBase = available.find((l) => l.toLowerCase().split("-")[0] === base);
|
|
1080
|
+
if (sameBase) return sameBase;
|
|
1081
|
+
}
|
|
1082
|
+
return fallback;
|
|
1083
|
+
}
|
|
1084
|
+
function suggestMarket({
|
|
1085
|
+
headers,
|
|
1086
|
+
markets,
|
|
1087
|
+
current,
|
|
1088
|
+
chosen,
|
|
1089
|
+
trustProxy = true
|
|
1090
|
+
}) {
|
|
1091
|
+
if (chosen || markets.length < 2) return null;
|
|
1092
|
+
if (BOT.test(headers.get("user-agent") ?? "")) return null;
|
|
1093
|
+
const country = visitorCountry(headers, { trustProxy });
|
|
1094
|
+
if (country) {
|
|
1095
|
+
const target = markets.find((m) => m.countries.includes(country));
|
|
1096
|
+
return target && target.code !== current ? { market: target, reason: "country" } : null;
|
|
1097
|
+
}
|
|
1098
|
+
for (const tag of preferredLanguages(headers.get("accept-language"))) {
|
|
1099
|
+
const region = tag.split("-")[1]?.toUpperCase();
|
|
1100
|
+
if (!region) continue;
|
|
1101
|
+
const target = markets.find((m) => m.countries.includes(region));
|
|
1102
|
+
if (target) return target.code !== current ? { market: target, reason: "language" } : null;
|
|
1103
|
+
}
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
function hreflangLinks(markets, path, defaultMarket, protocol = "https") {
|
|
1107
|
+
const links = markets.filter((m) => m.host).map((m) => ({ hrefLang: m.locale, href: `${protocol}://${m.host}${path}` }));
|
|
1108
|
+
const fallback = markets.find((m) => m.code === defaultMarket && m.host);
|
|
1109
|
+
if (fallback) links.push({ hrefLang: "x-default", href: `${protocol}://${fallback.host}${path}` });
|
|
1110
|
+
return links;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1048
1113
|
exports.CLIENT_IP_HEADER = CLIENT_IP_HEADER;
|
|
1114
|
+
exports.COUNTRY_HEADERS = COUNTRY_HEADERS;
|
|
1049
1115
|
exports.NOTE_MAX_LENGTH = NOTE_MAX_LENGTH;
|
|
1050
1116
|
exports.StorefrontError = StorefrontError;
|
|
1051
1117
|
exports.appendParams = appendParams;
|
|
@@ -1061,8 +1127,10 @@ exports.formatAvailabilityMessage = formatAvailabilityMessage;
|
|
|
1061
1127
|
exports.formatMoney = formatMoney;
|
|
1062
1128
|
exports.formatVoucherExpiry = formatVoucherExpiry;
|
|
1063
1129
|
exports.generateIdempotencyKey = generateIdempotencyKey;
|
|
1130
|
+
exports.hreflangLinks = hreflangLinks;
|
|
1064
1131
|
exports.isStorefrontError = isStorefrontError;
|
|
1065
1132
|
exports.isTokenPublishable = isTokenPublishable;
|
|
1133
|
+
exports.negotiateLocale = negotiateLocale;
|
|
1066
1134
|
exports.normalizeClientIp = normalizeClientIp;
|
|
1067
1135
|
exports.normalizeNote = normalizeNote;
|
|
1068
1136
|
exports.parseMoney = parseMoney;
|
|
@@ -1071,6 +1139,7 @@ exports.paymentErrorView = paymentErrorView;
|
|
|
1071
1139
|
exports.paymentIdempotencyKey = paymentIdempotencyKey;
|
|
1072
1140
|
exports.paymentMessages = paymentMessages;
|
|
1073
1141
|
exports.paymentOutcome = paymentOutcome;
|
|
1142
|
+
exports.preferredLanguages = preferredLanguages;
|
|
1074
1143
|
exports.productAvailability = productAvailability;
|
|
1075
1144
|
exports.productListQuery = productListQuery;
|
|
1076
1145
|
exports.promoProgressView = promoProgressView;
|
|
@@ -1079,6 +1148,9 @@ exports.resolveAvailability = resolveAvailability;
|
|
|
1079
1148
|
exports.rewardLabel = rewardLabel;
|
|
1080
1149
|
exports.safeHttpsUrl = safeHttpsUrl;
|
|
1081
1150
|
exports.schemaAvailability = schemaAvailability;
|
|
1151
|
+
exports.storefrontDomainHeader = storefrontDomainHeader;
|
|
1152
|
+
exports.suggestMarket = suggestMarket;
|
|
1082
1153
|
exports.toMoney = toMoney;
|
|
1154
|
+
exports.visitorCountry = visitorCountry;
|
|
1083
1155
|
//# sourceMappingURL=index.cjs.map
|
|
1084
1156
|
//# sourceMappingURL=index.cjs.map
|