@faststore/api 1.12.51 → 1.12.52

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.
@@ -22,7 +22,7 @@ var api = require('@opentelemetry/api');
22
22
  var apiLogs = require('@opentelemetry/api-logs');
23
23
 
24
24
  var name = "@faststore/api";
25
- var version = "1.12.50";
25
+ var version = "1.12.51";
26
26
  var license = "MIT";
27
27
  var main = "dist/index.js";
28
28
  var typings = "dist/index.d.ts";
@@ -91,7 +91,38 @@ var packageJson = {
91
91
  peerDependencies: peerDependencies
92
92
  };
93
93
 
94
- const USER_AGENT = `${packageJson.name}@${packageJson.version}`;
94
+ const USER_AGENT = `${packageJson.name}@${packageJson.version}`; // Headers that may carry credentials: store API keys (`X-VTEX-API-AppKey`,
95
+ // `X-VTEX-API-AppToken`), shopper cookies (`Cookie`, `Set-Cookie`,
96
+ // `VtexIdclientAutCookie_*`), `Authorization`, and generic token, secret or
97
+ // API key headers. They are replaced before a failed request and its response
98
+ // are logged. Matched by substring so new credential headers are covered too.
99
+
100
+ const SENSITIVE_HEADER = /authorization|cookie|token|secret|api-?key|appkey/i;
101
+ const REDACTED = '[redacted]';
102
+ const redactHeaders = headers => {
103
+ if (!headers) {
104
+ return undefined;
105
+ }
106
+
107
+ const redacted = {};
108
+
109
+ const add = (name, value) => {
110
+ redacted[name] = SENSITIVE_HEADER.test(name) ? REDACTED : value;
111
+ };
112
+
113
+ if (Array.isArray(headers)) {
114
+ headers.forEach(([name, value]) => add(name, value));
115
+ } else if (typeof headers.forEach === 'function') {
116
+ // Matches both the global `Headers` and the polyfill returned by
117
+ // `isomorphic-unfetch` on Node, which isn't an instance of the global one.
118
+ const headerList = headers;
119
+ headerList.forEach((value, name) => add(name, value));
120
+ } else {
121
+ Object.entries(headers).forEach(([name, value]) => add(name, value));
122
+ }
123
+
124
+ return redacted;
125
+ };
95
126
  const fetchAPI = async (info, init, options) => {
96
127
  const response = await fetch(info, { ...init,
97
128
  headers: { ...(init == null ? void 0 : init.headers),
@@ -105,9 +136,20 @@ const fetchAPI = async (info, init, options) => {
105
136
  }
106
137
 
107
138
  return response.status !== 204 ? response.json() : undefined;
108
- }
139
+ } // Log a summary instead of the raw Response, whose headers may include
140
+ // `Set-Cookie` with shopper credentials. The request body is omitted because
141
+ // it can carry personal data (e.g. the newsletter email and name).
109
142
 
110
- console.error(info, init, response);
143
+
144
+ console.error( // Only the URL: a `Request` instance could carry unredacted headers.
145
+ typeof info === 'string' ? info : info.url, init && { ...init,
146
+ headers: redactHeaders(init.headers),
147
+ body: init.body ? REDACTED : undefined
148
+ }, {
149
+ status: response.status,
150
+ statusText: response.statusText,
151
+ headers: redactHeaders(response.headers)
152
+ });
111
153
  const text = await response.text();
112
154
  throw new Error(text);
113
155
  };
@@ -239,7 +281,15 @@ const BASE_INIT = {
239
281
  headers: {
240
282
  'content-type': 'application/json'
241
283
  }
242
- };
284
+ }; // Accounts already warned that they rely on public `NL` fields. The commerce
285
+ // client is created per request, so the warning is kept once per account
286
+ // instead of per instance, to avoid flooding the logs. The account comes from
287
+ // the store's API options, not from the request, so the set holds at most one
288
+ // entry per account the process is configured for (usually one). It's still
289
+ // capped, so it can't grow without bound if that ever changes.
290
+
291
+ const MAX_WARNED_NEWSLETTER_ACCOUNTS = 100;
292
+ const accountsWarnedAboutAnonymousNewsletter = /*#__PURE__*/new Set();
243
293
 
244
294
  const getHeader = (headers, name) => {
245
295
  const value = headers == null ? void 0 : headers[name];
@@ -413,11 +463,58 @@ const VtexCommerce = ({
413
463
  body: '{}'
414
464
  });
415
465
  },
416
- subscribeToNewsletter: data => {
466
+
467
+ /**
468
+ * Creates a newsletter subscription in the `NL` Master Data entity.
469
+ *
470
+ * With `FS_NEWSLETTER_APP_KEY`/`FS_NEWSLETTER_APP_TOKEN` set, the document is
471
+ * created with a `POST` authenticated by that dedicated key, so `NL` fields
472
+ * can stay private. The request is create-only: the body is built here from
473
+ * the validated fields and never carries a document id, so the caller can't
474
+ * reach an existing record.
475
+ *
476
+ * Without the credentials, it falls back to the legacy anonymous `PATCH`,
477
+ * which only works while `NL` fields are public.
478
+ */
479
+ subscribeToNewsletter: ({
480
+ name,
481
+ email
482
+ }) => {
483
+ const body = JSON.stringify({ ...(name ? {
484
+ name
485
+ } : {}),
486
+ email,
487
+ isNewsletterOptIn: true
488
+ });
489
+ const appKey = process.env.FS_NEWSLETTER_APP_KEY;
490
+ const appToken = process.env.FS_NEWSLETTER_APP_TOKEN;
491
+
492
+ if (appKey && appToken) {
493
+ return fetchAPI(`${base}/api/dataentities/NL/documents`, {
494
+ method: 'POST',
495
+ headers: {
496
+ 'content-type': 'application/json',
497
+ accept: 'application/json',
498
+ 'X-VTEX-API-AppKey': appKey,
499
+ 'X-VTEX-API-AppToken': appToken
500
+ },
501
+ body
502
+ });
503
+ }
504
+
505
+ if (!accountsWarnedAboutAnonymousNewsletter.has(account)) {
506
+ if (accountsWarnedAboutAnonymousNewsletter.size >= MAX_WARNED_NEWSLETTER_ACCOUNTS) {
507
+ accountsWarnedAboutAnonymousNewsletter.clear();
508
+ }
509
+
510
+ accountsWarnedAboutAnonymousNewsletter.add(account); // Name only the missing variables, so a partial setup is easy to spot.
511
+
512
+ const missing = [!appKey && 'FS_NEWSLETTER_APP_KEY', !appToken && 'FS_NEWSLETTER_APP_TOKEN'].filter(Boolean);
513
+ console.warn(`[subscribeToNewsletter] ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not set. Falling back to an anonymous Master Data request, which requires public fields in the NL entity. Configure both credentials and make the NL fields private.`);
514
+ }
515
+
417
516
  return fetchAPI(`${base}/api/dataentities/NL/documents/`, { ...BASE_INIT,
418
- body: JSON.stringify({ ...data,
419
- isNewsletterOptIn: true
420
- }),
517
+ body,
421
518
  method: 'PATCH'
422
519
  });
423
520
  }
@@ -1209,6 +1306,11 @@ const StoreFacetValueBoolean = {
1209
1306
  }) => quantity
1210
1307
  };
1211
1308
 
1309
+ const EMAIL_MAX_LENGTH = 254;
1310
+ const NAME_MAX_LENGTH = 100; // Character classes exclude `.`, so each domain label is matched once and the
1311
+ // pattern stays linear on long inputs.
1312
+
1313
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@.]+(\.[^\s@.]+)+$/;
1212
1314
  const subscribeToNewsletter = async (_, {
1213
1315
  data
1214
1316
  }, {
@@ -1216,7 +1318,25 @@ const subscribeToNewsletter = async (_, {
1216
1318
  commerce
1217
1319
  }
1218
1320
  }) => {
1219
- const response = await commerce.subscribeToNewsletter(data);
1321
+ var _data$email$trim$toLo, _data$email, _data$name$trim, _data$name;
1322
+
1323
+ // The subscription is written with a store credential, so only validated
1324
+ // values reach Master Data.
1325
+ const email = (_data$email$trim$toLo = (_data$email = data.email) == null ? void 0 : _data$email.trim().toLowerCase()) != null ? _data$email$trim$toLo : '';
1326
+ const name = (_data$name$trim = (_data$name = data.name) == null ? void 0 : _data$name.trim()) != null ? _data$name$trim : '';
1327
+
1328
+ if (email.length > EMAIL_MAX_LENGTH || !EMAIL_PATTERN.test(email)) {
1329
+ throw new BadRequestError('Invalid email');
1330
+ }
1331
+
1332
+ if (name.length > NAME_MAX_LENGTH) {
1333
+ throw new BadRequestError(`Invalid name: must have at most ${NAME_MAX_LENGTH} characters`);
1334
+ }
1335
+
1336
+ const response = await commerce.subscribeToNewsletter({
1337
+ email,
1338
+ name: name || undefined
1339
+ });
1220
1340
  return {
1221
1341
  id: response == null ? void 0 : response.Id
1222
1342
  };