oneentry 1.0.167 → 1.0.169

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/changelog.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # SDK Change Log
2
2
 
3
+ ## v.1.0.169
4
+
5
+ ### What's Fixed
6
+
7
+ - **A response that is not JSON came back as a parser message, and on the error branch as nothing at all.** The platform has been seen answering `200 OK` with `content-type: text/html` and its own admin page as the body — every content endpoint of a project at once, for minutes, a single request enough to reproduce it (CMS-957). A successful status carrying someone else's body cannot be told apart from a real answer by status alone, and all the caller got was `SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON`: a sentence about the SDK's own parsing rather than about what arrived. Read as "this record does not exist", it made a static build of a demo shop bake `404` into 8 of 54 product pages while every one of those products was live. The same body behind a failing status was worse: `_parseJson` swallowed it and returned `{}`, which carries no `statusCode` and therefore does not read as an error at all, so a `502` came back looking like an empty success. A body that is not JSON is now described — `Expected JSON from the platform, received text/html (HTTP 200): <!DOCTYPE html>…` — with `statusCode: 0` on a successful status, the same "no usable answer" contract as a dropped connection, and the status the server really sent when it failed. Token refresh reads its body through the same helper instead of `response.json()`, so an HTML answer there fails the refresh rather than throwing a raw parse error out of the request that triggered it.
8
+
9
+ ### Tests
10
+
11
+ - **`src/base/tests/nonjson.spec.ts` pins the five outcomes** with a stubbed `fetch`, no stand required: HTML behind a `200`, HTML behind a `502`, a normal JSON answer, an empty body, and an error the platform itself sent.
12
+
13
+ ## v.1.0.168
14
+
15
+ ### What's Fixed
16
+
17
+ - **`Search.globalSearch` could not ask for a language, so a second-locale word found nothing.** The search index is per language, and the method sent no `langCode` — not even the one given to `defineOneEntry`. On a bilingual project the Spanish word "agua" came back with an empty group while the same request with `langCode=es_ES` returned a full page. The method now takes `langCode` as its last parameter, defaulting to the client's own language, so an existing call needs no edit and starts asking in the right language by itself. Measured on a bilingual stand: "agua" 0 → 30+, the English control unchanged (CMS-912).
18
+
19
+ - **Methods that post a body no longer modify the object they are given.** `_normalizePostBody` wrapped `formData` and dropped an empty `phoneSMS` in place, and `signUp` and `updateUser` wrote `langCode` onto the caller's object. A body that is reused — a retry wrapper, two parallel calls, a shared default — came back already wrapped, was wrapped a second time, and the platform answered `400 "formData.en_US[0].marker" is required`: a validation error about a field the caller never sent. The normalizer now works on a copy, `notificationData` included, and the caller's object is identical after the call (CMS-909).
20
+
21
+ - **A dropped connection returned `{}`, which reads exactly like an empty answer.** With `isShell` on — the default — a network failure was handed back as the thrown `TypeError`, whose `message` and `stack` are not enumerable, so it serialized to an empty object. There was no way to tell "no connection" from "no data": on one site such a value entered the page cache and was served for minutes after the link came back, and a static build baked a site of 404s while reporting success. A failure that never reached the platform now returns `{ statusCode: 0, message, pageData: null, timestamp }` — `statusCode: 0` is a value no HTTP answer uses. An error the platform itself sent is passed through untouched (CMS-780).
22
+
23
+ - **`Discounts.getBonusHistory` was declared to return an array; it returns a container.** The endpoint answers `{ items, total }` — an account with no transactions gives `{"items":[],"total":0}` — while the method and the `IDiscounts` interface both promised `IBonusTransactionEntity[]`. Code written from the type read `.length` off an object and found `undefined`. The return type is now `IBonusHistoryResponse`, exported alongside the rest. Found while covering the module with tests (CMS-531); types only, the wire format did not change.
24
+
25
+ - **The declared value shapes of a form submission were the ones the platform refuses.** A body written from the types got three `400`s in a row: a `list` field typed as `[{ title, value }]` (a page attribute's shape) answers `value[0] must be a string`, a `text` field typed as an object answers `value must be an array`, and `params` inside it answers `is not allowed`; a `spam` field had no type at all and a bare token is refused with the same sentence as a wrong one. `IBodyPostFormData.formData` now uses submission-specific types — `IBodyPostTypeList` (a list of strings), `IBodyPostTypeText` (a one-element array holding exactly one of `plainValue`, `htmlValue` or `mdValue`, no `params`) and `IBodyPostTypeSpam` (`{ event: { token, siteKey } }`) — with the read types left as they were. Types and documentation only; the wire format did not change (CMS-888).
26
+
27
+ ### Tests
28
+
29
+ - **`discounts` and `oauth` now have tests that run.** The discounts suite covered four of six methods; `getBonusBalance` and `getBonusHistory` were untested, and it was the new coverage that found the wrong return type above. The suite went from 5 cases to 8, and the bonus ones authenticate first — the balance belongs to the signed-in user and answers `401` under the app token alone. The `oauth` case was an empty shell: its body was commented out and the expectation rule disabled, so it passed while testing nothing. The happy path still cannot be covered from the test stand — it needs a provider of type `oauth` and a one-time code from that provider's consent screen, and the stand has neither — so what is pinned instead is the SDK's own half of the contract: a refusal comes back as an error value rather than an exception, and the caller's body is not modified. The limitation is written into the test rather than left as a silent pass (CMS-531, CMS-532).
30
+
3
31
  ## v.1.0.167
4
32
 
5
33
  ### What's New
@@ -124,8 +124,9 @@ class AuthProviderApi extends asyncModules_1.default {
124
124
  * @see {@link https://js-sdk.oneentry.cloud/docs/auth-provider/signUp signUp} documentation.
125
125
  */
126
126
  async signUp(marker, body, langCode = this.state.lang) {
127
- body['langCode'] = langCode;
128
- const result = await this._fetchPost(`/marker/${marker}/users/sign-up`, this._normalizePostBody(body, langCode));
127
+ // `langCode` goes onto our own copy — the caller's body stays as it was
128
+ // given, so a retry or a second parallel call sends the same thing (CMS-909).
129
+ const result = await this._fetchPost(`/marker/${marker}/users/sign-up`, this._normalizePostBody({ ...body, langCode }, langCode));
129
130
  // Validate response if validation is enabled
130
131
  const validated = await this._validateResponse(result, schema('SignUpResponseSchema'));
131
132
  return this._normalizeData(validated);
@@ -103,13 +103,19 @@ export default abstract class AsyncModules extends SyncModules {
103
103
  */
104
104
  private makeOptions;
105
105
  /**
106
- * Reads a response body as JSON, tolerating an empty body.
106
+ * Reads an error response body as JSON, tolerating an empty body.
107
107
  *
108
108
  * `Response.json()` throws on an empty body (e.g. 204 or an empty error
109
109
  * payload); this helper mirrors the OK-path guard (`text ? JSON.parse : {}`)
110
110
  * so both the success and the error branches handle empty bodies the same way.
111
+ *
112
+ * A body that is present but is not JSON — a gateway's HTML page in front of a
113
+ * 502, the platform's own admin page (CMS-957) — used to collapse into `{}`,
114
+ * which carries no `statusCode` and therefore does not read as an error at all:
115
+ * a failed request came back looking like an empty success. It is described
116
+ * instead, keeping the status the server actually sent.
111
117
  * @param {Response} response - The fetch Response to read.
112
- * @returns {Promise<unknown>} Parsed JSON, or an empty object when the body is empty/unparsable.
118
+ * @returns {Promise<unknown>} Parsed JSON, `{}` for an empty body, or a described error for a body that is not JSON.
113
119
  */
114
120
  private _parseJson;
115
121
  /**
@@ -120,4 +126,42 @@ export default abstract class AsyncModules extends SyncModules {
120
126
  * @description Define an asynchronous method 'browserResponse' that takes a path and options as parameters
121
127
  */
122
128
  private browserResponse;
129
+ /**
130
+ * Describes a response whose body is not the JSON the API is supposed to send.
131
+ *
132
+ * The platform has been seen answering `200 OK` with `content-type: text/html`
133
+ * and the admin page as the body — every content endpoint at once, for minutes
134
+ * (CMS-957). A successful status carrying someone else's body is indistinguishable
135
+ * from a real answer by status alone, and the only symptom used to be
136
+ * `SyntaxError: Unexpected token '<'` from the JSON parser: a message about the
137
+ * SDK's own parsing rather than about what the platform did. Callers read that as
138
+ * "this record does not exist" and a static build baked 404s over live products.
139
+ *
140
+ * The returned error names the received content type, the HTTP status and the
141
+ * first characters of the body, so a log line says what arrived instead of JSON.
142
+ * `statusCode` stays `0` — the request produced no usable answer, the same
143
+ * contract as a dropped connection, and a value no HTTP answer uses.
144
+ * @param {Response} response - The response whose body could not be parsed.
145
+ * @param {string} text - The raw body as read.
146
+ * @returns {IError} The error in the shape the SDK returns for platform errors.
147
+ */
148
+ private _nonJsonError;
149
+ /**
150
+ * Turns whatever was thrown into the error shape the SDK already returns.
151
+ *
152
+ * With `isShell` on, a failure is returned rather than thrown — and a dropped
153
+ * connection throws a `TypeError`, whose `message` and `stack` are not
154
+ * enumerable. Returned as it was, it serializes to `{}` and reads exactly
155
+ * like an empty answer: on the sanbenitocog pilot such a value entered the
156
+ * page cache and was served for minutes after the link came back, and
157
+ * `next build` baked a site of 404s while reporting success (CMS-780).
158
+ *
159
+ * An object that already carries a `statusCode` is the platform's own error
160
+ * and is passed through untouched. Everything else becomes
161
+ * `{ statusCode: 0, message, pageData: null, timestamp }` — `statusCode: 0`
162
+ * says "the request never reached the platform", which no HTTP answer uses.
163
+ * @param {unknown} e - Whatever the try block threw.
164
+ * @returns {IError} The error in the shape the SDK returns for platform errors.
165
+ */
166
+ private _toShellError;
123
167
  }
@@ -239,8 +239,14 @@ class AsyncModules extends syncModules_1.default {
239
239
  });
240
240
  // Check if the response status is OK (status code 200-299)
241
241
  if (response.ok) {
242
- // Parse the response body as JSON and store the result
243
- const result = await response.json();
242
+ // Parsed through the shared helper rather than `response.json()`: a `200` whose body is
243
+ // not JSON (CMS-957) threw a raw SyntaxError out of the refresh and took the request that
244
+ // triggered it with it. A body without tokens is a failed refresh, reported as such.
245
+ const result = await this._parseJson(response);
246
+ if (typeof (result === null || result === void 0 ? void 0 : result.refreshToken) !== 'string' ||
247
+ typeof (result === null || result === void 0 ? void 0 : result.accessToken) !== 'string') {
248
+ return false;
249
+ }
244
250
  // Update the state's refreshToken and accessToken with the new tokens from the response
245
251
  this.state.refreshToken = result.refreshToken;
246
252
  this.state.accessToken = result.accessToken;
@@ -313,22 +319,37 @@ class AsyncModules extends syncModules_1.default {
313
319
  return options;
314
320
  }
315
321
  /**
316
- * Reads a response body as JSON, tolerating an empty body.
322
+ * Reads an error response body as JSON, tolerating an empty body.
317
323
  *
318
324
  * `Response.json()` throws on an empty body (e.g. 204 or an empty error
319
325
  * payload); this helper mirrors the OK-path guard (`text ? JSON.parse : {}`)
320
326
  * so both the success and the error branches handle empty bodies the same way.
327
+ *
328
+ * A body that is present but is not JSON — a gateway's HTML page in front of a
329
+ * 502, the platform's own admin page (CMS-957) — used to collapse into `{}`,
330
+ * which carries no `statusCode` and therefore does not read as an error at all:
331
+ * a failed request came back looking like an empty success. It is described
332
+ * instead, keeping the status the server actually sent.
321
333
  * @param {Response} response - The fetch Response to read.
322
- * @returns {Promise<unknown>} Parsed JSON, or an empty object when the body is empty/unparsable.
334
+ * @returns {Promise<unknown>} Parsed JSON, `{}` for an empty body, or a described error for a body that is not JSON.
323
335
  */
324
336
  async _parseJson(response) {
337
+ let text;
325
338
  try {
326
- const text = await response.text();
327
- return text ? JSON.parse(text) : {};
339
+ text = await response.text();
328
340
  }
329
341
  catch {
330
342
  return {};
331
343
  }
344
+ if (!text) {
345
+ return {};
346
+ }
347
+ try {
348
+ return JSON.parse(text);
349
+ }
350
+ catch {
351
+ return this._nonJsonError(response, text);
352
+ }
332
353
  }
333
354
  /**
334
355
  * Handles responses from the browser's fetch API.
@@ -386,10 +407,18 @@ class AsyncModules extends syncModules_1.default {
386
407
  // Check if the response status is OK (status code 200-299)
387
408
  if (response.ok) {
388
409
  // Read the body as text; if it is empty return an empty object,
389
- // otherwise parse it as JSON. A parse failure propagates to the
390
- // catch below and is handled according to isShell.
410
+ // otherwise parse it as JSON. A body that is not JSON is turned into a
411
+ // described error by `_nonJsonError` and handled according to isShell.
391
412
  const text = await response.text();
392
- return (text ? JSON.parse(text) : {});
413
+ if (!text) {
414
+ return {};
415
+ }
416
+ try {
417
+ return JSON.parse(text);
418
+ }
419
+ catch {
420
+ throw this._nonJsonError(response, text);
421
+ }
393
422
  }
394
423
  // Handle non-OK responses (applies to the first response and to a failed retry).
395
424
  const status = response.status;
@@ -410,12 +439,79 @@ class AsyncModules extends syncModules_1.default {
410
439
  // Handle any errors that occur during the fetch process
411
440
  // Determine whether to return or throw the error based on 'isShell' state
412
441
  if (this.state.isShell) {
413
- return e;
442
+ return this._toShellError(e);
414
443
  }
415
444
  else {
416
445
  throw e;
417
446
  }
418
447
  }
419
448
  }
449
+ /**
450
+ * Describes a response whose body is not the JSON the API is supposed to send.
451
+ *
452
+ * The platform has been seen answering `200 OK` with `content-type: text/html`
453
+ * and the admin page as the body — every content endpoint at once, for minutes
454
+ * (CMS-957). A successful status carrying someone else's body is indistinguishable
455
+ * from a real answer by status alone, and the only symptom used to be
456
+ * `SyntaxError: Unexpected token '<'` from the JSON parser: a message about the
457
+ * SDK's own parsing rather than about what the platform did. Callers read that as
458
+ * "this record does not exist" and a static build baked 404s over live products.
459
+ *
460
+ * The returned error names the received content type, the HTTP status and the
461
+ * first characters of the body, so a log line says what arrived instead of JSON.
462
+ * `statusCode` stays `0` — the request produced no usable answer, the same
463
+ * contract as a dropped connection, and a value no HTTP answer uses.
464
+ * @param {Response} response - The response whose body could not be parsed.
465
+ * @param {string} text - The raw body as read.
466
+ * @returns {IError} The error in the shape the SDK returns for platform errors.
467
+ */
468
+ _nonJsonError(response, text) {
469
+ var _a, _b, _c;
470
+ const contentType = (_c = (_b = (_a = response.headers) === null || _a === void 0 ? void 0 : _a.get) === null || _b === void 0 ? void 0 : _b.call(_a, 'content-type')) !== null && _c !== void 0 ? _c : 'unknown';
471
+ const excerpt = text.trim().slice(0, 120).replace(/\s+/g, ' ');
472
+ return {
473
+ // A successful status carrying a non-JSON body produced no usable answer, the same
474
+ // contract as a dropped connection. A failing one already says something true about
475
+ // the request, so that status is kept.
476
+ statusCode: response.ok ? 0 : response.status,
477
+ message: `Expected JSON from the platform, received ${contentType} ` +
478
+ `(HTTP ${response.status}): ${excerpt}`,
479
+ pageData: null,
480
+ timestamp: new Date().toISOString(),
481
+ };
482
+ }
483
+ /**
484
+ * Turns whatever was thrown into the error shape the SDK already returns.
485
+ *
486
+ * With `isShell` on, a failure is returned rather than thrown — and a dropped
487
+ * connection throws a `TypeError`, whose `message` and `stack` are not
488
+ * enumerable. Returned as it was, it serializes to `{}` and reads exactly
489
+ * like an empty answer: on the sanbenitocog pilot such a value entered the
490
+ * page cache and was served for minutes after the link came back, and
491
+ * `next build` baked a site of 404s while reporting success (CMS-780).
492
+ *
493
+ * An object that already carries a `statusCode` is the platform's own error
494
+ * and is passed through untouched. Everything else becomes
495
+ * `{ statusCode: 0, message, pageData: null, timestamp }` — `statusCode: 0`
496
+ * says "the request never reached the platform", which no HTTP answer uses.
497
+ * @param {unknown} e - Whatever the try block threw.
498
+ * @returns {IError} The error in the shape the SDK returns for platform errors.
499
+ */
500
+ _toShellError(e) {
501
+ if (e && typeof e === 'object' && 'statusCode' in e) {
502
+ return e;
503
+ }
504
+ const message = e instanceof Error
505
+ ? `${e.name}: ${e.message}`
506
+ : typeof e === 'string'
507
+ ? e
508
+ : 'Request failed before a response was received';
509
+ return {
510
+ statusCode: 0,
511
+ message,
512
+ pageData: null,
513
+ timestamp: new Date().toISOString(),
514
+ };
515
+ }
420
516
  }
421
517
  exports.default = AsyncModules;
@@ -81,10 +81,17 @@ export default abstract class SyncModules {
81
81
  * 2. **formData localization**: the API expects formData as
82
82
  * `{ "<langCode>": [...fields] }`, but callers pass a flat array or a single object.
83
83
  * The method wraps the data into the required structure.
84
- * If `formData` is absent from the body — return the body as-is (only phoneSMS cleanup applied).
85
- * @param {any} body - The body to normalize.
84
+ * If `formData` is absent from the body — return a copy with only the phoneSMS cleanup applied.
85
+ *
86
+ * **The caller's object is never touched.** Methods that post a body are
87
+ * routinely called with one that is reused — a retry wrapper, two parallel
88
+ * calls, a shared default. Normalizing in place sends the already-wrapped
89
+ * value the second time, it is wrapped again, and the platform answers
90
+ * `400 "formData.en_US[0].marker" is required` about a field the caller
91
+ * never sent (CMS-909).
92
+ * @param {any} body - The body to normalize. Left unchanged.
86
93
  * @param {string} [langCode] - The language code for normalization.
87
- * @returns {any} Normalized body.
94
+ * @returns {any} A normalized copy of the body.
88
95
  */
89
96
  protected _normalizePostBody(body: any, langCode?: string): any;
90
97
  /**
@@ -220,27 +220,38 @@ class SyncModules {
220
220
  * 2. **formData localization**: the API expects formData as
221
221
  * `{ "<langCode>": [...fields] }`, but callers pass a flat array or a single object.
222
222
  * The method wraps the data into the required structure.
223
- * If `formData` is absent from the body — return the body as-is (only phoneSMS cleanup applied).
224
- * @param {any} body - The body to normalize.
223
+ * If `formData` is absent from the body — return a copy with only the phoneSMS cleanup applied.
224
+ *
225
+ * **The caller's object is never touched.** Methods that post a body are
226
+ * routinely called with one that is reused — a retry wrapper, two parallel
227
+ * calls, a shared default. Normalizing in place sends the already-wrapped
228
+ * value the second time, it is wrapped again, and the platform answers
229
+ * `400 "formData.en_US[0].marker" is required` about a field the caller
230
+ * never sent (CMS-909).
231
+ * @param {any} body - The body to normalize. Left unchanged.
225
232
  * @param {string} [langCode] - The language code for normalization.
226
- * @returns {any} Normalized body.
233
+ * @returns {any} A normalized copy of the body.
227
234
  */
228
235
  _normalizePostBody(body, langCode = this.state.lang) {
229
- // API does not accept phoneSMS = '' — delete the field if empty.
230
- if (body.notificationData && body.notificationData.phoneSMS === '') {
231
- delete body.notificationData.phoneSMS;
236
+ const next = { ...body };
237
+ // API does not accept phoneSMS = '' — drop the field if empty. `notificationData`
238
+ // is copied too: deleting the key on the original would reach the caller.
239
+ if (next.notificationData && next.notificationData.phoneSMS === '') {
240
+ next.notificationData = { ...next.notificationData };
241
+ delete next.notificationData.phoneSMS;
232
242
  }
233
243
  // If formData is not provided — no further normalization needed.
234
- if (!body.formData)
235
- return body;
244
+ if (!next.formData)
245
+ return next;
236
246
  // Wrap form fields in an object keyed by locale:
237
247
  // [{ marker, value }] → { "en_US": [{ marker, value }] }
238
- const formData = {};
239
- formData[langCode] = Array.isArray(body.formData)
240
- ? body.formData
241
- : [body.formData];
242
- body.formData = formData;
243
- return body;
248
+ // The fields themselves are reused by reference — they are not modified.
249
+ next.formData = {
250
+ [langCode]: Array.isArray(next.formData)
251
+ ? next.formData
252
+ : [next.formData],
253
+ };
254
+ return next;
244
255
  }
245
256
  /**
246
257
  * Normalizes the value of a single attribute in place.
@@ -1,7 +1,7 @@
1
1
  import AsyncModules from '../base/asyncModules';
2
2
  import type StateModule from '../base/stateModule';
3
3
  import type { IError } from '../base/utils';
4
- import type { IBonusBalanceEntity, IBonusTransactionEntity, IBonusTransactionType, ICouponValidationResult, IDiscountsApi, IDiscountsEntity, IDiscountsResponse, IDiscountsVectorSearch, IDiscountType } from './discountsInterfaces';
4
+ import type { IBonusBalanceEntity, IBonusHistoryResponse, IBonusTransactionType, ICouponValidationResult, IDiscountsApi, IDiscountsEntity, IDiscountsResponse, IDiscountsVectorSearch, IDiscountType } from './discountsInterfaces';
5
5
  /**
6
6
  * Controllers for working with events
7
7
  * @handle /api/content/events
@@ -63,11 +63,11 @@ export default class DiscountsApi extends AsyncModules implements IDiscountsApi
63
63
  * @param {number} [discountId] - Discount id filter.
64
64
  * @param {number} [moduleId] - Module id filter.
65
65
  * @param {boolean} [isAdmin] - Admin filter.
66
- * @returns {Promise<IBonusTransactionEntity[] | IError>} Returns array of bonus transactions or an error object.
66
+ * @returns {Promise<IBonusHistoryResponse | IError>} Returns the container with the transactions and their total, or an error object.
67
67
  * @throws {IError} When isShell=false and an error occurs during the fetch
68
68
  * @see {@link https://js-sdk.oneentry.cloud/docs/discounts/getBonusHistory getBonusHistory} documentation.
69
69
  */
70
- getBonusHistory(type?: IBonusTransactionType, dateFrom?: string, dateTo?: string, discountId?: number, moduleId?: number, isAdmin?: boolean): Promise<IBonusTransactionEntity[] | IError>;
70
+ getBonusHistory(type?: IBonusTransactionType, dateFrom?: string, dateTo?: string, discountId?: number, moduleId?: number, isAdmin?: boolean): Promise<IBonusHistoryResponse | IError>;
71
71
  /**
72
72
  * Validate discounts coupon
73
73
  * @handleName validateDiscountsCoupon
@@ -85,7 +85,7 @@ class DiscountsApi extends asyncModules_1.default {
85
85
  * @param {number} [discountId] - Discount id filter.
86
86
  * @param {number} [moduleId] - Module id filter.
87
87
  * @param {boolean} [isAdmin] - Admin filter.
88
- * @returns {Promise<IBonusTransactionEntity[] | IError>} Returns array of bonus transactions or an error object.
88
+ * @returns {Promise<IBonusHistoryResponse | IError>} Returns the container with the transactions and their total, or an error object.
89
89
  * @throws {IError} When isShell=false and an error occurs during the fetch
90
90
  * @see {@link https://js-sdk.oneentry.cloud/docs/discounts/getBonusHistory getBonusHistory} documentation.
91
91
  */
@@ -55,10 +55,10 @@ interface IDiscountsApi {
55
55
  * @param {number} [discountId] - Discount id filter.
56
56
  * @param {number} [moduleId] - Module id filter.
57
57
  * @param {boolean} [isAdmin] - Admin filter.
58
- * @returns {Promise<IBonusTransactionEntity[] | IError>} Returns array of bonus transactions or an error object.
58
+ * @returns {Promise<IBonusHistoryResponse | IError>} Returns the container with the transactions and their total, or an error object.
59
59
  * @throws {IError} When isShell=false and an error occurs during the fetch
60
60
  */
61
- getBonusHistory(type?: IBonusTransactionType, dateFrom?: string, dateTo?: string, discountId?: number, moduleId?: number, isAdmin?: boolean): Promise<IBonusTransactionEntity[] | IError>;
61
+ getBonusHistory(type?: IBonusTransactionType, dateFrom?: string, dateTo?: string, discountId?: number, moduleId?: number, isAdmin?: boolean): Promise<IBonusHistoryResponse | IError>;
62
62
  /**
63
63
  * Semantic (vector) search for discounts.
64
64
  * @handleName getDiscountsByVectorSearch
@@ -284,6 +284,18 @@ interface IBonusTransactionEntity {
284
284
  expiresAt?: string;
285
285
  createdAt?: string;
286
286
  }
287
+ /**
288
+ * @interface IBonusHistoryResponse
289
+ * @property {IBonusTransactionEntity[]} items - The transactions of the current page.
290
+ * @property {number} total - How many transactions the filter matches in total.
291
+ * @description Container the bonus history arrives in. The endpoint answers
292
+ * `{ items, total }`, not a bare array — measured on the SDK test stand,
293
+ * 2026-09-28: an account with no transactions answers `{"items":[],"total":0}`.
294
+ */
295
+ interface IBonusHistoryResponse {
296
+ items: IBonusTransactionEntity[];
297
+ total: number;
298
+ }
287
299
  /**
288
300
  * @interface IBonusBalanceEntity
289
301
  * @description Represents the balance of bonus points.
@@ -292,4 +304,4 @@ interface IBonusTransactionEntity {
292
304
  interface IBonusBalanceEntity {
293
305
  balance: number;
294
306
  }
295
- export type { IBonusBalanceEntity, IBonusHistoryQuery, IBonusTransactionEntity, IBonusTransactionType, ICouponEntity, ICouponValidationResult, IDiscountByMarkerQuery, IDiscountCondition, IDiscountsApi, IDiscountsEntity, IDiscountsQuery, IDiscountsResponse, IDiscountsValidateCoupon, IDiscountsVectorSearch, IDiscountType, IDiscountValue, };
307
+ export type { IBonusBalanceEntity, IBonusHistoryQuery, IBonusHistoryResponse, IBonusTransactionEntity, IBonusTransactionType, ICouponEntity, ICouponValidationResult, IDiscountByMarkerQuery, IDiscountCondition, IDiscountsApi, IDiscountsEntity, IDiscountsQuery, IDiscountsResponse, IDiscountsValidateCoupon, IDiscountsVectorSearch, IDiscountType, IDiscountValue, };
@@ -420,6 +420,133 @@ interface IUpdateFormsData {
420
420
  * Contains an array of data form objects with the following values
421
421
  */
422
422
  type FormDataType = IBodyTypeStringNumberFloat | IBodyTypeTimeDate | IBodyTypeText | IBodyTypeTextWithHeader | IBodyTypeImageGroupOfImages | IBodyTypeFile | IBodyTypeRadioButtonList | Record<string, unknown>;
423
+ /**
424
+ * Value shapes accepted by a SUBMISSION (`POST /form-data`).
425
+ *
426
+ * For three field kinds they are not the shapes a page attribute uses, and a
427
+ * body written from the read types is refused. Measured on
428
+ * rwah2o.oneentry.cloud, 2026-09-12 and again 2026-09-28 (CMS-888).
429
+ *
430
+ * `list` takes a list of strings; the `[{ title, value }]` form of a page
431
+ * attribute answers `400 formData's marker '<marker>' value[0] must be a string`.
432
+ *
433
+ * `text` takes a one-element array holding exactly ONE of `plainValue`,
434
+ * `htmlValue` or `mdValue`, and no `params`. The object form answers
435
+ * `400 … value must be an array`, `params` answers `… is not allowed`, and two
436
+ * representations answer `Only one of htmlValue, plainValue or mdValue can be provided`.
437
+ *
438
+ * `spam` takes the captcha event, not the bare token.
439
+ */
440
+ type FormDataPostType = IBodyTypeStringNumberFloat | IBodyTypeTimeDate | IBodyPostTypeText | IBodyPostTypeList | IBodyPostTypeSpam | IBodyTypeTextWithHeader | IBodyTypeImageGroupOfImages | IBodyTypeFile | Record<string, unknown>;
441
+ /**
442
+ * Represents a `list` field of a form SUBMISSION.
443
+ * @interface IBodyPostTypeList
444
+ * @property {string} marker - marker name. Example: "recipient".
445
+ * @property {string} type - Type value. Example: "list".
446
+ * @property {string[]} value - Chosen options as plain strings. Example: ["Tom Hoffart"].
447
+ * @example
448
+ {
449
+ "marker": "recipient",
450
+ "type": "list",
451
+ "value": ["Tom Hoffart"]
452
+ }
453
+ * @description A submission sends the chosen options as strings. The `[{ title, value }]`
454
+ * shape a page attribute stores is refused here with `value[0] must be a string`.
455
+ */
456
+ interface IBodyPostTypeList {
457
+ marker: string;
458
+ type: 'list';
459
+ value: string[];
460
+ }
461
+ /**
462
+ * Represents a `text` field of a form SUBMISSION.
463
+ * @interface IBodyPostTypeText
464
+ * @property {string} marker - marker name. Example: "your_message".
465
+ * @property {string} type - Type value. Example: "text".
466
+ * @property {ITextSubmissionValue[]} value - One-element array holding exactly one representation.
467
+ * @example
468
+ {
469
+ "marker": "your_message",
470
+ "type": "text",
471
+ "value": [{ "plainValue": "Hello" }]
472
+ }
473
+ * @description The array holds one object carrying exactly one of `plainValue`,
474
+ * `htmlValue` or `mdValue`. Editor `params` are refused, and so is sending two
475
+ * representations at once.
476
+ */
477
+ interface IBodyPostTypeText {
478
+ marker: string;
479
+ type: 'text';
480
+ value: ITextSubmissionValue[];
481
+ }
482
+ /**
483
+ * @interface ITextSubmissionValuePlain
484
+ * @property {string} plainValue - Plain-text representation. Example: "Hello".
485
+ * @description Plain-text branch of a submitted text value.
486
+ */
487
+ interface ITextSubmissionValuePlain {
488
+ plainValue: string;
489
+ }
490
+ /**
491
+ * @interface ITextSubmissionValueHtml
492
+ * @property {string} htmlValue - HTML representation. Example: "<p>Hello</p>".
493
+ * @description HTML branch of a submitted text value.
494
+ */
495
+ interface ITextSubmissionValueHtml {
496
+ htmlValue: string;
497
+ }
498
+ /**
499
+ * @interface ITextSubmissionValueMd
500
+ * @property {string} mdValue - Markdown representation. Must not be empty. Example: "**Hello**".
501
+ * @description Markdown branch of a submitted text value.
502
+ */
503
+ interface ITextSubmissionValueMd {
504
+ mdValue: string;
505
+ }
506
+ /**
507
+ * Exactly one representation of a submitted text value.
508
+ */
509
+ type ITextSubmissionValue = ITextSubmissionValuePlain | ITextSubmissionValueHtml | ITextSubmissionValueMd;
510
+ /**
511
+ * Represents a `spam` (captcha) field of a form SUBMISSION.
512
+ * @interface IBodyPostTypeSpam
513
+ * @property {string} marker - marker name. Example: "spam".
514
+ * @property {string} type - Type value. Example: "spam".
515
+ * @property {ICaptchaValue} value - The captcha event, not the bare token.
516
+ * @example
517
+ {
518
+ "marker": "spam",
519
+ "type": "spam",
520
+ "value": { "event": { "token": "<reCAPTCHA v3 token>", "siteKey": "6Ldjk7Yt…" } }
521
+ }
522
+ * @description The field carries a reCAPTCHA v3 token obtained in the browser.
523
+ * A bare string is refused with the same sentence as a wrong token —
524
+ * `Captcha Validation Failed, captcha type is: google` — so a malformed value
525
+ * is indistinguishable from a bot.
526
+ */
527
+ interface IBodyPostTypeSpam {
528
+ marker: string;
529
+ type: 'spam';
530
+ value: ICaptchaValue;
531
+ }
532
+ /**
533
+ * @interface ICaptchaEvent
534
+ * @property {string} token - The token issued by the captcha provider in the browser.
535
+ * @property {string} siteKey - The site key the token was issued for. Example: "6Ldjk7Yt…".
536
+ * @description The captcha payload of a `spam` field.
537
+ */
538
+ interface ICaptchaEvent {
539
+ token: string;
540
+ siteKey: string;
541
+ }
542
+ /**
543
+ * @interface ICaptchaValue
544
+ * @property {ICaptchaEvent} event - The captcha payload.
545
+ * @description Wrapper the platform expects around the captcha payload.
546
+ */
547
+ interface ICaptchaValue {
548
+ event: ICaptchaEvent;
549
+ }
423
550
  /**
424
551
  * Represents the structure of the body for posting form data.
425
552
  * @interface IBodyPostFormData
@@ -428,7 +555,7 @@ type FormDataType = IBodyTypeStringNumberFloat | IBodyTypeTimeDate | IBodyTypeTe
428
555
  * @property {string} moduleEntityIdentifier - Text identifier (marker) of the module entity. Example: "blog".
429
556
  * @property {string | null} replayTo - Email to reply to. Example: "some@email.com".
430
557
  * @property {string} status - Status of the form. Example: "sent".
431
- * @property {FormDataType[]} formData - Array of form data objects.
558
+ * @property {FormDataPostType[]} formData - Array of form data objects. A submission's value shapes differ from the read shapes — see FormDataPostType.
432
559
  * @example
433
560
  [
434
561
  {
@@ -455,7 +582,7 @@ interface IBodyPostFormData {
455
582
  moduleEntityIdentifier: string;
456
583
  replayTo: string | null;
457
584
  status: string;
458
- formData: FormDataType[];
585
+ formData: FormDataPostType[];
459
586
  fileQuery?: IUploadingQuery;
460
587
  }
461
588
  /**
@@ -711,4 +838,4 @@ interface IListOptionValue {
711
838
  value: string;
712
839
  extended: IListOptionExtended;
713
840
  }
714
- export type { FormDataStatus, FormDataType, IBodyPostFormData, IBodyTypeFile, IBodyTypeImageGroupOfImages, IBodyTypeRadioButtonList, IBodyTypeStringNumberFloat, IBodyTypeText, IBodyTypeTextWithHeader, IBodyTypeTimeDate, IFileValue, IFormByMarkerDataEntity, IFormDataEntity, IFormDataSearchEntity, IFormsByMarkerDataEntity, IFormsData, IFormsDataEntity, IFormsDataFilter, IFormsDataSearchResponse, IFormsDataVectorSearch, IImageValue, IImageValueParams, IListOptionExtended, IListOptionValue, IPostFormResponse, IPostFormResponseData, ITextEditorParams, ITextValue, ITextWithHeaderValue, ITimeDateValue, IUpdateFormsData, };
841
+ export type { FormDataPostType, FormDataStatus, FormDataType, IBodyPostFormData, IBodyPostTypeList, IBodyPostTypeSpam, IBodyPostTypeText, IBodyTypeFile, IBodyTypeImageGroupOfImages, IBodyTypeRadioButtonList, IBodyTypeStringNumberFloat, IBodyTypeText, IBodyTypeTextWithHeader, IBodyTypeTimeDate, ICaptchaEvent, ICaptchaValue, IFileValue, IFormByMarkerDataEntity, IFormDataEntity, IFormDataSearchEntity, IFormsByMarkerDataEntity, IFormsData, IFormsDataEntity, IFormsDataFilter, IFormsDataSearchResponse, IFormsDataVectorSearch, IImageValue, IImageValueParams, IListOptionExtended, IListOptionValue, IPostFormResponse, IPostFormResponseData, ITextEditorParams, ITextSubmissionValue, ITextValue, ITextWithHeaderValue, ITimeDateValue, IUpdateFormsData, };
@@ -26,10 +26,11 @@ export default class SearchApi extends AsyncModules implements ISearchApi {
26
26
  * @param {'all' | 'visible' | 'hidden'} [visibility] - Visibility filter of the searched records. Default: "all".
27
27
  * @param {number} [offset] - Drilldown mode offset. Pass it only together with a single accessible type in `types`.
28
28
  * @param {number} [limit] - Drilldown mode page size. Pass it only together with a single accessible type in `types`, otherwise the API answers 400 "Drilldown mode (limit/offset) requires exactly one accessible type in `types`". Without it every group is returned in full.
29
+ * @param {string} [langCode] - Language code. Default: "en_US". The search index is per language: a word of a second locale is found only when its own langCode is sent (CMS-912).
29
30
  * @returns {Promise<IGlobalSearchResponse | IError>} Returns the query and the found records grouped by entity type.
30
31
  * @throws {IError} When isShell=false and an error occurs during the fetch
31
- * @description Searches names and attribute values of visible records across entity types.
32
+ * @description Searches names and attribute values of visible records across entity types. The index is per language — the request carries `langCode`, so a bilingual project finds a word of either locale.
32
33
  * @see {@link https://js-sdk.oneentry.cloud/docs/search/globalSearch globalSearch} documentation.
33
34
  */
34
- globalSearch(query: string, types?: TGlobalSearchEntityType[], visibility?: 'all' | 'visible' | 'hidden', offset?: number, limit?: number): Promise<IGlobalSearchResponse | IError>;
35
+ globalSearch(query: string, types?: TGlobalSearchEntityType[], visibility?: 'all' | 'visible' | 'hidden', offset?: number, limit?: number, langCode?: string): Promise<IGlobalSearchResponse | IError>;
35
36
  }
@@ -64,19 +64,23 @@ class SearchApi extends asyncModules_1.default {
64
64
  * @param {'all' | 'visible' | 'hidden'} [visibility] - Visibility filter of the searched records. Default: "all".
65
65
  * @param {number} [offset] - Drilldown mode offset. Pass it only together with a single accessible type in `types`.
66
66
  * @param {number} [limit] - Drilldown mode page size. Pass it only together with a single accessible type in `types`, otherwise the API answers 400 "Drilldown mode (limit/offset) requires exactly one accessible type in `types`". Without it every group is returned in full.
67
+ * @param {string} [langCode] - Language code. Default: "en_US". The search index is per language: a word of a second locale is found only when its own langCode is sent (CMS-912).
67
68
  * @returns {Promise<IGlobalSearchResponse | IError>} Returns the query and the found records grouped by entity type.
68
69
  * @throws {IError} When isShell=false and an error occurs during the fetch
69
- * @description Searches names and attribute values of visible records across entity types.
70
+ * @description Searches names and attribute values of visible records across entity types. The index is per language — the request carries `langCode`, so a bilingual project finds a word of either locale.
70
71
  * @see {@link https://js-sdk.oneentry.cloud/docs/search/globalSearch globalSearch} documentation.
71
72
  */
72
- async globalSearch(query, types, visibility, offset, limit) {
73
+ async globalSearch(query, types, visibility, offset, limit, langCode = this.state.lang) {
73
74
  // `types` is sent comma-separated — the array stringifies to exactly that.
75
+ // `langCode` is not optional decoration: the index is per language, and
76
+ // without it a word of a second locale answers an empty group (CMS-912).
74
77
  const params = {
75
78
  q: query,
76
79
  types,
77
80
  visibility,
78
81
  offset,
79
82
  limit,
83
+ langCode,
80
84
  };
81
85
  const data = await this._fetchGet(`?` + this._queryParamsToString(params));
82
86
  const validated = await this._validateResponse(data, schema('GlobalSearchResponseSchema'));