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