@ryuzaki13/react-foundation-lib 1.4.0 → 1.4.2

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.
Files changed (101) hide show
  1. package/README.md +172 -114
  2. package/dist/chunks/{formatters-CBgrBeSA.js → formatters-D33Uh3si.js} +7 -8
  3. package/dist/chunks/{formatters-CBgrBeSA.js.map → formatters-D33Uh3si.js.map} +1 -1
  4. package/dist/chunks/odata-service-CWkHASdw.js +2211 -0
  5. package/dist/chunks/odata-service-CWkHASdw.js.map +1 -0
  6. package/dist/chunks/{validate-BW-7BZy_.js → validate-DyzojM0w.js} +2 -2
  7. package/dist/chunks/{validate-BW-7BZy_.js.map → validate-DyzojM0w.js.map} +1 -1
  8. package/dist/error-report/index.js +2 -2
  9. package/dist/excel/index.js +20 -21
  10. package/dist/excel/index.js.map +1 -1
  11. package/dist/formatters/index.js +4 -5
  12. package/dist/{odata → odata-service}/buildSeparatedArrays.d.ts +1 -1
  13. package/dist/odata-service/buildSeparatedArrays.d.ts.map +1 -0
  14. package/dist/{odata/types.d.ts → odata-service/collectionTypes.d.ts} +1 -1
  15. package/dist/odata-service/collectionTypes.d.ts.map +1 -0
  16. package/dist/{odata → odata-service}/dateUtils.d.ts +1 -1
  17. package/dist/odata-service/dateUtils.d.ts.map +1 -0
  18. package/dist/{odata → odata-service}/findCollectionPairs.d.ts +1 -1
  19. package/dist/odata-service/findCollectionPairs.d.ts.map +1 -0
  20. package/dist/odata-service/index.d.ts +5 -0
  21. package/dist/odata-service/index.d.ts.map +1 -1
  22. package/dist/odata-service/index.js +2 -2
  23. package/dist/{odata/store.d.ts → odata-service/odataCollectionStore.d.ts} +1 -1
  24. package/dist/odata-service/odataCollectionStore.d.ts.map +1 -0
  25. package/dist/query-client/index.js.map +1 -1
  26. package/dist/query-client/queryClient.d.ts +1 -3
  27. package/dist/query-client/queryClient.d.ts.map +1 -1
  28. package/package.json +1 -5
  29. package/src/array/README.mdx +1630 -0
  30. package/src/binary/README.mdx +1405 -0
  31. package/src/bounded-copy-stack/README.mdx +257 -0
  32. package/src/context-menu/README.mdx +326 -0
  33. package/src/copy/README.mdx +992 -0
  34. package/src/crypto/README.mdx +1052 -0
  35. package/src/currency/README.mdx +155 -0
  36. package/src/date-segments/README.mdx +319 -0
  37. package/src/dom/README.mdx +2179 -0
  38. package/src/error/README.mdx +95 -0
  39. package/src/error-report/README.mdx +612 -0
  40. package/src/excel/README.mdx +449 -0
  41. package/src/file/README.mdx +1957 -0
  42. package/src/fingerprint/README.mdx +100 -0
  43. package/src/form/README.mdx +101 -0
  44. package/src/formatters/README.mdx +469 -0
  45. package/src/formatters/boolean/README.mdx +494 -0
  46. package/src/formatters/date/README.mdx +732 -0
  47. package/src/formatters/number/README.mdx +542 -0
  48. package/src/formatters/pipeline/README.mdx +629 -0
  49. package/src/formatters/rowBased/README.mdx +486 -0
  50. package/src/formatters/strings/README.mdx +442 -0
  51. package/src/formatters/valueState/README.mdx +459 -0
  52. package/src/formulas/README.mdx +1283 -0
  53. package/src/foundationLib.mdx +70 -54
  54. package/src/hooks/README.mdx +988 -0
  55. package/src/http/README.mdx +126 -0
  56. package/src/media/README.mdx +1853 -0
  57. package/src/notifications/README.mdx +323 -0
  58. package/src/number-scale/README.mdx +318 -0
  59. package/src/odata-service/README.mdx +2916 -0
  60. package/src/presets/README.mdx +136 -0
  61. package/src/pwa/README.mdx +394 -0
  62. package/src/query-client/README.mdx +2500 -0
  63. package/src/range-output/README.mdx +161 -0
  64. package/src/seo/README.mdx +279 -0
  65. package/src/session-storage/README.mdx +125 -0
  66. package/src/string-comparison/README.mdx +96 -0
  67. package/src/table/README.mdx +3106 -0
  68. package/src/three-scene/README.mdx +248 -0
  69. package/src/types/README.mdx +172 -0
  70. package/src/utils/README.mdx +1724 -0
  71. package/src/validators/README.mdx +239 -0
  72. package/src/virtualizer/README.mdx +138 -0
  73. package/src/xml/README.mdx +149 -0
  74. package/dist/chunks/dateUtils-B5V5aXB-.js +0 -847
  75. package/dist/chunks/dateUtils-B5V5aXB-.js.map +0 -1
  76. package/dist/chunks/odata-service-B6TxBSLm.js +0 -1277
  77. package/dist/chunks/odata-service-B6TxBSLm.js.map +0 -1
  78. package/dist/odata/buildSeparatedArrays.d.ts.map +0 -1
  79. package/dist/odata/dateUtils.d.ts.map +0 -1
  80. package/dist/odata/findCollectionPairs.d.ts.map +0 -1
  81. package/dist/odata/index.d.ts +0 -6
  82. package/dist/odata/index.d.ts.map +0 -1
  83. package/dist/odata/index.js +0 -102
  84. package/dist/odata/index.js.map +0 -1
  85. package/dist/odata/store.d.ts.map +0 -1
  86. package/dist/odata/types.d.ts.map +0 -1
  87. package/src/binary/README.md +0 -110
  88. package/src/docs/browserAndReact.mdx +0 -150
  89. package/src/docs/filesAndExcel.mdx +0 -163
  90. package/src/docs/formatters-runtime.mdx +0 -455
  91. package/src/docs/formatters.mdx +0 -380
  92. package/src/docs/odataAndTables.mdx +0 -178
  93. package/src/docs/pureUtilities.mdx +0 -96
  94. package/src/docs/runtimeAndState.mdx +0 -155
  95. package/src/file/README.md +0 -289
  96. package/src/formatters/date/README.md +0 -520
  97. package/src/formatters/number/README.md +0 -123
  98. package/src/formatters/valueState/README.md +0 -275
  99. package/src/formulas/README.md +0 -42
  100. package/src/pwa/README.md +0 -638
  101. package/src/query-client/README.md +0 -565
@@ -0,0 +1,1052 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/API/Crypto" />
4
+
5
+ # Идентификаторы и строковые hash через `@ryuzaki13/react-foundation-lib/crypto`
6
+
7
+ Модуль `crypto` предоставляет небольшие функции для двух разных задач:
8
+
9
+ - получить детерминированный компактный hash из строки;
10
+ - создать случайный UUID версии 4.
11
+
12
+ Детерминированный результат всегда повторяется для той же строки. Случайный UUID, наоборот, создаётся заново при каждом вызове. Это принципиально разные виды идентификаторов, и выбирать между ними нужно по смыслу задачи.
13
+
14
+ Несмотря на имя entrypoint, строковые hash этого модуля не являются криптографическими. Они не подходят для паролей, подписей, access token, проверки безопасности или сокрытия персональных данных.
15
+
16
+ README рассчитан на разработчика, который использует опубликованный пакет без доступа к исходному коду. Здесь объясняются базовые понятия, точные форматы результатов, browser runtime, Unicode, коллизии, React/SSR, тестирование и безопасные границы применения.
17
+
18
+ ## Содержание
19
+
20
+ - [Назначение и границы модуля](#назначение-и-границы-модуля)
21
+ - [Установка и импорт](#установка-и-импорт)
22
+ - [Минимальные понятия](#минимальные-понятия)
23
+ - [Как выбрать API](#как-выбрать-api)
24
+ - [Быстрый старт](#быстрый-старт)
25
+ - [Общие правила строковых hash](#общие-правила-строковых-hash)
26
+ - [`hashString`](#hashstring)
27
+ - [`hashString128`](#hashstring128)
28
+ - [`hashString128Base64Url`](#hashstring128base64url)
29
+ - [`stringToElementId`](#stringtoelementid)
30
+ - [`uuidv4`](#uuidv4)
31
+ - [Подготовка сложных данных](#подготовка-сложных-данных)
32
+ - [Безопасность](#безопасность)
33
+ - [Коллизии и обрезание результата](#коллизии-и-обрезание-результата)
34
+ - [Browser, Node.js и SSR](#browser-nodejs-и-ssr)
35
+ - [React-рецепты](#react-рецепты)
36
+ - [Тестирование](#тестирование)
37
+ - [Производительность](#производительность)
38
+ - [Ограничения и частые ошибки](#ограничения-и-частые-ошибки)
39
+ - [Краткий справочник API](#краткий-справочник-api)
40
+ - [Вопросы и ответы](#вопросы-и-ответы)
41
+
42
+ ## Назначение и границы модуля
43
+
44
+ ### Что делает `crypto`
45
+
46
+ - строит короткий 32-bit hash в base36;
47
+ - строит более длинный строковый hash в 32-символьном hexadecimal формате;
48
+ - кодирует тот же длинный hash в компактный URL-safe base64 без padding;
49
+ - добавляет префикс к длинному hash для получения DOM-like ID;
50
+ - создаёт UUID v4 через Web Crypto;
51
+ - использует native `crypto.randomUUID`, если он доступен;
52
+ - имеет fallback UUID через `crypto.getRandomValues`.
53
+
54
+ ### Чего модуль не делает
55
+
56
+ - не шифрует и не расшифровывает данные;
57
+ - не вычисляет SHA-256, SHA-512, HMAC или цифровую подпись;
58
+ - не хеширует пароли безопасным password hashing алгоритмом;
59
+ - не гарантирует отсутствие коллизий;
60
+ - не проверяет уникальность результата по базе данных;
61
+ - не нормализует регистр, пробелы, Unicode или JSON;
62
+ - не сериализует объект автоматически;
63
+ - не хранит созданные UUID;
64
+ - не создаёт React hydration-safe ID вместо `useId`;
65
+ - не превращает UUID в право доступа или security token.
66
+
67
+ ### Две группы API
68
+
69
+ | Группа | Функции | Свойство |
70
+ | ----------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
71
+ | Детерминированные hash | `hashString`, `hashString128`, `hashString128Base64Url`, `stringToElementId` | Та же точная строка даёт тот же результат. |
72
+ | Случайный идентификатор | `uuidv4` | Каждый вызов должен создавать новое случайное значение. |
73
+
74
+ ## Установка и импорт
75
+
76
+ Установите пакет:
77
+
78
+ ```bash
79
+ npm install @ryuzaki13/react-foundation-lib
80
+ ```
81
+
82
+ Импортируйте функции только из опубликованного subpath `/crypto`:
83
+
84
+ ```ts
85
+ import { hashString, hashString128, hashString128Base64Url, stringToElementId, uuidv4 } from "@ryuzaki13/react-foundation-lib/crypto";
86
+ ```
87
+
88
+ Корневой импорт пакета не поддерживается:
89
+
90
+ ```ts
91
+ // Неправильно.
92
+ import { uuidv4 } from "@ryuzaki13/react-foundation-lib";
93
+
94
+ // Правильно.
95
+ import { uuidv4 } from "@ryuzaki13/react-foundation-lib/crypto";
96
+ ```
97
+
98
+ Entrypoint `/crypto` не импортирует React и не содержит hooks.
99
+
100
+ Модуль поставляется как ESM. Часть API использует browser globals:
101
+
102
+ | API | Требуемые возможности runtime |
103
+ | ------------------------ | --------------------------------------------------------- |
104
+ | `hashString` | Обычные `String`, `Number` и bitwise operations |
105
+ | `hashString128` | Дополнительно `Math.imul` |
106
+ | `hashString128Base64Url` | Дополнительно глобальный `btoa` |
107
+ | `stringToElementId` | Глобальный `btoa`, потому что вызывает предыдущую функцию |
108
+ | `uuidv4` | Глобальный Web Crypto `crypto` |
109
+
110
+ Наличие одноимённых API в конкретной версии Node.js не является частью browser-oriented контракта пакета. Проверяйте среду, если вызываете эти функции вне браузера.
111
+
112
+ ## Минимальные понятия
113
+
114
+ ### Идентификатор
115
+
116
+ Идентификатор — строка, по которой приложение отличает одну сущность от другой:
117
+
118
+ ```ts
119
+ const userId = "user-42";
120
+ ```
121
+
122
+ Хороший идентификатор должен соответствовать задаче. Иногда он должен повторяться для одинакового входа, а иногда должен быть новым для каждой созданной сущности.
123
+
124
+ ### Детерминированность
125
+
126
+ Детерминированная функция всегда возвращает одинаковый результат для одинакового входа:
127
+
128
+ ```ts
129
+ hashString128("user:42") === hashString128("user:42"); // true
130
+ ```
131
+
132
+ Это удобно для:
133
+
134
+ - cache-like ключей;
135
+ - стабильных DOM ID по известному business key;
136
+ - fingerprint нормализованной конфигурации;
137
+ - группировки или быстрого предварительного сравнения.
138
+
139
+ ### Случайность
140
+
141
+ `uuidv4()` использует криптографически стойкий генератор случайных байтов среды:
142
+
143
+ ```ts
144
+ const first = uuidv4();
145
+ const second = uuidv4();
146
+
147
+ console.log(first === second); // Практически ожидается false.
148
+ ```
149
+
150
+ Случайный UUID удобен, когда новая сущность должна получить новый ID независимо от её содержимого.
151
+
152
+ ### Hash
153
+
154
+ Hash-функция превращает вход произвольной длины в результат ограниченной длины:
155
+
156
+ ```text
157
+ "user:42" → "fa3326027b653e8049d56072c88378f0"
158
+ ```
159
+
160
+ Поскольку возможных входных строк бесконечно много, а возможных результатов конечное количество, две разные строки теоретически могут получить одинаковый hash. Это называется коллизией.
161
+
162
+ ### Hash не является шифрованием
163
+
164
+ Шифрование предполагает возможность расшифровать данные при наличии ключа. Эти hash-функции не имеют операции обратного восстановления.
165
+
166
+ Но отсутствие обратной функции не делает их безопасным способом скрыть данные. Короткий или предсказуемый вход можно подобрать перебором:
167
+
168
+ ```ts
169
+ hashString128("user@example.com"); // Это не безопасная анонимизация email.
170
+ ```
171
+
172
+ ### Base36
173
+
174
+ Base36 использует цифры `0-9` и латинские буквы `a-z`. Например, десятичное число можно записать короче за счёт 36 возможных символов.
175
+
176
+ `hashString` возвращает именно такой формат:
177
+
178
+ ```text
179
+ d5nw5e
180
+ ```
181
+
182
+ ### Hexadecimal
183
+
184
+ Hexadecimal, или base16, использует:
185
+
186
+ ```text
187
+ 0123456789abcdef
188
+ ```
189
+
190
+ `hashString128` всегда возвращает 32 lowercase hex-символа.
191
+
192
+ ### Base64 URL-safe
193
+
194
+ Обычный base64 может содержать `+`, `/` и padding `=`. URL-safe вариант заменяет проблемные символы:
195
+
196
+ - `+` → `-`;
197
+ - `/` → `_`;
198
+ - завершающие `=` удаляются.
199
+
200
+ `hashString128Base64Url` возвращает 22 символа из набора:
201
+
202
+ ```text
203
+ A-Z a-z 0-9 - _
204
+ ```
205
+
206
+ ### UUID v4
207
+
208
+ UUID v4 — случайный идентификатор стандартного формата:
209
+
210
+ ```text
211
+ xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
212
+ ```
213
+
214
+ - общая длина — 36 символов;
215
+ - дефисы находятся в фиксированных позициях;
216
+ - `4` обозначает версию 4;
217
+ - первый символ группы `y` соответствует UUID variant.
218
+
219
+ Пример:
220
+
221
+ ```text
222
+ 550e8400-e29b-41d4-a716-446655440000
223
+ ```
224
+
225
+ ## Как выбрать API
226
+
227
+ | Задача | API |
228
+ | -------------------------------------------------------------- | ----------------------------------- |
229
+ | Нужен очень короткий non-security hash | `hashString` |
230
+ | Нужен стабильный длинный hash для сравнения/config fingerprint | `hashString128` |
231
+ | Нужен компактный URL-safe вариант длинного hash | `hashString128Base64Url` |
232
+ | Нужен стабильный DOM-like ID по строке | `stringToElementId` |
233
+ | Нужен новый случайный ID для создаваемой сущности | `uuidv4` |
234
+ | Нужна связь label/input внутри React-компонента с SSR | React `useId`, не этот модуль |
235
+ | Нужен password hash | Специализированный password hasher |
236
+ | Нужна криптографическая целостность или подпись | Web Crypto/серверная crypto-система |
237
+ | Нужен уникальный business ID с централизованной проверкой | Backend/database contract |
238
+
239
+ ## Быстрый старт
240
+
241
+ ### Стабильный hash
242
+
243
+ ```ts
244
+ import { hashString128 } from "@ryuzaki13/react-foundation-lib/crypto";
245
+
246
+ const fingerprint = hashString128("customer:42");
247
+
248
+ console.log(fingerprint); // Одна и та же строка всегда даст тот же результат.
249
+ ```
250
+
251
+ ### Стабильный ID DOM-элемента
252
+
253
+ ```ts
254
+ import { stringToElementId } from "@ryuzaki13/react-foundation-lib/crypto";
255
+
256
+ const rowId = stringToElementId("customer:42");
257
+ const cellId = stringToElementId("customer:42:name", "cell");
258
+ ```
259
+
260
+ Примерный формат:
261
+
262
+ ```text
263
+ row-<22 URL-safe символа>
264
+ cell-<22 URL-safe символа>
265
+ ```
266
+
267
+ ### Новый UUID
268
+
269
+ ```ts
270
+ import { uuidv4 } from "@ryuzaki13/react-foundation-lib/crypto";
271
+
272
+ const draft = {
273
+ id: uuidv4(),
274
+ title: "Новая запись"
275
+ };
276
+ ```
277
+
278
+ ## Общие правила строковых hash
279
+
280
+ ### Вход всегда строка
281
+
282
+ Все hash-функции принимают `string`:
283
+
284
+ ```ts
285
+ hashString128("42"); // Корректно.
286
+ hashString128(42); // Ошибка TypeScript.
287
+ ```
288
+
289
+ Число `42` и строка `"42"` — разные типы. Если вы осознанно преобразуете значение, делайте это явно:
290
+
291
+ ```ts
292
+ hashString128(String(42));
293
+ ```
294
+
295
+ ### Обрабатывается точная последовательность UTF-16 code units
296
+
297
+ JavaScript хранит строки как последовательности UTF-16 code units. Реализация проходит строку через `charCodeAt`.
298
+
299
+ Поэтому имеют значение:
300
+
301
+ - регистр букв;
302
+ - пробелы и переводы строк;
303
+ - порядок символов;
304
+ - composed/decomposed форма Unicode;
305
+ - каждый code unit surrogate pair.
306
+
307
+ ```ts
308
+ hashString128("User") !== hashString128("user");
309
+ hashString128("A") !== hashString128(" A ");
310
+ ```
311
+
312
+ ### Unicode не нормализуется
313
+
314
+ Визуально одинаковые строки могут иметь разное внутреннее представление:
315
+
316
+ ```ts
317
+ const composed = "Café";
318
+ const decomposed = "Cafe\u0301";
319
+
320
+ console.log(composed === decomposed); // false
321
+ console.log(hashString128(composed) === hashString128(decomposed)); // false
322
+ ```
323
+
324
+ Если доменная модель считает их одинаковыми, нормализуйте строку на явно выбранной boundary:
325
+
326
+ ```ts
327
+ const canonical = input.normalize("NFC");
328
+ const hash = hashString128(canonical);
329
+ ```
330
+
331
+ Если буквальная форма важна, не нормализуйте её.
332
+
333
+ ### Пустая строка допустима
334
+
335
+ Функции не отклоняют пустой вход:
336
+
337
+ ```ts
338
+ hashString("");
339
+ hashString128("");
340
+ hashString128Base64Url("");
341
+ ```
342
+
343
+ Пустая строка имеет свой стабильный hash. Проверять обязательность входа должен вызывающий код.
344
+
345
+ ### Разные hash не означают разную силу защиты
346
+
347
+ `hashString128` и `hashString128Base64Url` кодируют один и тот же внутренний результат разными способами. Base64Url-версия короче, но не безопаснее и не слабее hex-версии с точки зрения коллизий.
348
+
349
+ ## `hashString`
350
+
351
+ Возвращает компактный FNV-1a-like 32-bit hash в base36.
352
+
353
+ ### Сигнатура
354
+
355
+ ```ts
356
+ declare function hashString(input: string): string;
357
+ ```
358
+
359
+ ### Формат результата
360
+
361
+ - только символы `0-9` и `a-z`;
362
+ - lowercase;
363
+ - переменная длина от 1 до 7 символов;
364
+ - результат соответствует unsigned 32-bit значению, записанному в base36.
365
+
366
+ ### Примеры текущей реализации
367
+
368
+ ```ts
369
+ hashString(""); // "ztntfp"
370
+ hashString("user:42"); // "d5nw5e"
371
+ hashString("строка"); // "hb03a2"
372
+ ```
373
+
374
+ ### Когда использовать
375
+
376
+ - компактный ключ небольшой UI-коллекции;
377
+ - некритичный short fingerprint;
378
+ - bucket/group key, где коллизия дополнительно обрабатывается;
379
+ - диагностическая метка, не являющаяся источником истины.
380
+
381
+ ### Когда не использовать
382
+
383
+ - primary key;
384
+ - проверка уникальности большого набора;
385
+ - security decision;
386
+ - контроль целостности данных;
387
+ - password/token/signature;
388
+ - ситуация, где коллизия приводит к потере или перезаписи данных.
389
+
390
+ 32-bit пространство сравнительно мало. Не делайте вывод, что разные входы обязательно имеют разные результаты.
391
+
392
+ ## `hashString128`
393
+
394
+ Возвращает длинный non-cryptographic hash в hexadecimal формате.
395
+
396
+ ### Сигнатура
397
+
398
+ ```ts
399
+ declare function hashString128(input: string): string;
400
+ ```
401
+
402
+ ### Формат результата
403
+
404
+ - ровно 32 символа;
405
+ - только `0-9` и `a-f`;
406
+ - lowercase;
407
+ - четыре внутренних unsigned 32-bit слова записываются по 8 hex-символов.
408
+
409
+ ```ts
410
+ const result = hashString128("user:42");
411
+
412
+ console.log(result); // "fa3326027b653e8049d56072c88378f0"
413
+ console.log(result.length); // 32
414
+ ```
415
+
416
+ ### Когда использовать
417
+
418
+ - fingerprint канонической строки конфигурации;
419
+ - стабильный cache-like key;
420
+ - сравнение версий данных с допустимым non-adversarial collision risk;
421
+ - стабильная техническая метка, для которой 32-bit hash слишком короток.
422
+
423
+ ### Важное ограничение
424
+
425
+ Название `128` описывает формат из 128 output bits, а не криптографическую стойкость. Внутри используется собственное быстрое смешивание четырёх 32-bit состояний. Не приравнивайте функцию к MD5, SHA-256 или другому cryptographic digest.
426
+
427
+ ## `hashString128Base64Url`
428
+
429
+ Возвращает тот же длинный внутренний hash в более компактной URL-safe форме.
430
+
431
+ ### Сигнатура
432
+
433
+ ```ts
434
+ declare function hashString128Base64Url(input: string): string;
435
+ ```
436
+
437
+ ### Формат результата
438
+
439
+ - ровно 22 символа;
440
+ - символы `A-Z`, `a-z`, `0-9`, `-`, `_`;
441
+ - без `+`, `/` и padding `=`;
442
+ - регистр имеет значение.
443
+
444
+ ```ts
445
+ const result = hashString128Base64Url("user:42");
446
+
447
+ console.log(result); // "-jMmAntlPoBJ1WByyIN48A"
448
+ console.log(result.length); // 22
449
+ ```
450
+
451
+ Результат может начинаться с `-` или `_`. Это допустимая часть Base64Url-алфавита.
452
+
453
+ ### Когда использовать
454
+
455
+ - segment технического URL;
456
+ - компактный ключ для browser storage;
457
+ - DOM-like ID вместе с безопасным префиксом;
458
+ - data attribute;
459
+ - короткое представление того же результата, который можно было записать через `hashString128`.
460
+
461
+ ### Runtime
462
+
463
+ Функция преобразует 16 байтов через глобальный `btoa`. В среде без `btoa` будет выброшена ошибка. Функция не содержит fallback и не перехватывает её.
464
+
465
+ ### Это не кодирование исходной строки
466
+
467
+ Base64Url здесь кодирует байты hash, а не исходный `input`. Декодирование результата вернёт 16 байтов hash и не восстановит исходную строку.
468
+
469
+ ## `stringToElementId`
470
+
471
+ Добавляет префикс к результату `hashString128Base64Url`.
472
+
473
+ ### Сигнатура
474
+
475
+ ```ts
476
+ declare function stringToElementId(input: string, prefix?: string): string;
477
+ ```
478
+
479
+ ### Параметры
480
+
481
+ | Параметр | Тип | По умолчанию | Назначение |
482
+ | -------- | -------- | ------------ | ----------------------------------------- |
483
+ | `input` | `string` | — | Строка, определяющая стабильную часть ID. |
484
+ | `prefix` | `string` | `"row"` | Читаемая часть перед hash. |
485
+
486
+ ### Формула результата
487
+
488
+ ```ts
489
+ `${prefix}-${hashString128Base64Url(input)}`;
490
+ ```
491
+
492
+ ### Примеры текущей реализации
493
+
494
+ ```ts
495
+ stringToElementId("user:42");
496
+ // "row--jMmAntlPoBJ1WByyIN48A"
497
+
498
+ stringToElementId("user:42", "cell");
499
+ // "cell--jMmAntlPoBJ1WByyIN48A"
500
+ ```
501
+
502
+ Двойной дефис в примере корректен: первый добавлен между prefix и hash, второй является первым символом Base64Url hash.
503
+
504
+ ### Стабильность
505
+
506
+ Одинаковые `input` и `prefix` дают одинаковый результат:
507
+
508
+ ```ts
509
+ stringToElementId("row:42", "cell") === stringToElementId("row:42", "cell"); // true
510
+ ```
511
+
512
+ Разные префиксы разводят namespaces:
513
+
514
+ ```ts
515
+ stringToElementId("42", "row");
516
+ stringToElementId("42", "cell");
517
+ ```
518
+
519
+ ### Prefix не очищается
520
+
521
+ Функция вставляет `prefix` буквально. Она не удаляет пробелы и не экранирует CSS:
522
+
523
+ ```ts
524
+ stringToElementId("42", "my row"); // Содержит пробел в ID.
525
+ stringToElementId("42", ""); // Начинается с дефиса.
526
+ ```
527
+
528
+ Используйте короткий контролируемый prefix из букв, цифр, `_` или `-`. Не передавайте произвольный пользовательский ввод как prefix.
529
+
530
+ ### Поиск элемента
531
+
532
+ При default или безопасном prefix можно использовать:
533
+
534
+ ```ts
535
+ const id = stringToElementId("row:42");
536
+ const element = document.getElementById(id);
537
+ ```
538
+
539
+ Если ID участвует в CSS selector и его состав не полностью контролируется, экранируйте значение:
540
+
541
+ ```ts
542
+ const element = document.querySelector(`#${CSS.escape(id)}`);
543
+ ```
544
+
545
+ ### Когда выбрать React `useId`
546
+
547
+ Для связи `label`, `input`, `aria-describedby` и других элементов внутри React-компонента обычно лучше `useId`:
548
+
549
+ ```tsx
550
+ const id = useId();
551
+
552
+ return (
553
+ <>
554
+ <label htmlFor={id}>Имя</label>
555
+ <input id={id} />
556
+ </>
557
+ );
558
+ ```
559
+
560
+ `useId` согласован с React SSR и hydration. `stringToElementId` нужен, когда ID должен определяться внешним стабильным business/technical key, одинаковым в разных местах.
561
+
562
+ ### Коллизия всё ещё возможна
563
+
564
+ Добавление prefix не делает hash уникальным. Если два input получили одинаковый hash и prefix совпадает, ID тоже совпадёт.
565
+
566
+ ## `uuidv4`
567
+
568
+ Создаёт случайный UUID версии 4 через глобальный Web Crypto API.
569
+
570
+ ### Сигнатура
571
+
572
+ ```ts
573
+ declare function uuidv4(): string;
574
+ ```
575
+
576
+ ### Алгоритм выбора
577
+
578
+ 1. Если `crypto.randomUUID` доступен, функция сразу возвращает его результат.
579
+ 2. Иначе создаёт `Uint8Array(16)`.
580
+ 3. Заполняет его через `crypto.getRandomValues`.
581
+ 4. Устанавливает version bits для UUID v4.
582
+ 5. Устанавливает variant bits.
583
+ 6. Форматирует байты как lowercase строку `8-4-4-4-12`.
584
+
585
+ ```text
586
+ xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx
587
+ ```
588
+
589
+ ### Пример создания сущности
590
+
591
+ ```ts
592
+ type Draft = {
593
+ id: string;
594
+ title: string;
595
+ };
596
+
597
+ export function createDraft(title: string): Draft {
598
+ return {
599
+ id: uuidv4(),
600
+ title
601
+ };
602
+ }
603
+ ```
604
+
605
+ ### Функция не проверяет результат native API
606
+
607
+ Если существует `crypto.randomUUID`, его результат возвращается как есть. Дополнительная проверка UUID-формата не выполняется. В обычном браузере Web Crypto соблюдает контракт; в тестовом mock ответственность лежит на тесте.
608
+
609
+ ### Ошибки не перехватываются
610
+
611
+ Если глобальный `crypto` отсутствует либо недоступны и `randomUUID`, и `getRandomValues`, функция выбросит runtime error. Она не имеет Math.random fallback и не возвращает `null`.
612
+
613
+ Это правильная граница: слабая псевдослучайность не подменяет Web Crypto незаметно.
614
+
615
+ ### UUID не гарантируется базой данных
616
+
617
+ Вероятность случайного совпадения очень мала, но математическая гарантия отсутствует. Для authoritative storage всё равно используйте unique constraint и корректно обрабатывайте конфликт.
618
+
619
+ ### Не обрезайте UUID без оценки риска
620
+
621
+ ```ts
622
+ uuidv4().slice(0, 8);
623
+ ```
624
+
625
+ Такой результат содержит только небольшую часть UUID и резко уменьшает пространство вариантов. Это может быть приемлемо для временной UI-метки, но не для долговечного уникального ключа.
626
+
627
+ ### UUID не является автоматически security token
628
+
629
+ UUID v4 создаётся из криптографической случайности, но его назначение — идентификация. Требования к session token, reset link или access credential включают не только случайность, но также хранение, срок жизни, отзыв, transport и threat model. Используйте контракт системы безопасности, а не предположение «UUID выглядит случайным».
630
+
631
+ ## Подготовка сложных данных
632
+
633
+ ### Объект нельзя передать напрямую
634
+
635
+ ```ts
636
+ const config = { page: 1, sort: "name" };
637
+
638
+ hashString128(config); // Ошибка TypeScript: ожидается string.
639
+ ```
640
+
641
+ Сначала нужен детерминированный serializer.
642
+
643
+ ### Почему обычный `JSON.stringify` может быть недостаточен
644
+
645
+ Порядок object keys может различаться:
646
+
647
+ ```ts
648
+ const first = { page: 1, sort: "name" };
649
+ const second = { sort: "name", page: 1 };
650
+
651
+ JSON.stringify(first) !== JSON.stringify(second);
652
+ ```
653
+
654
+ Если модель считает эти объекты одинаковыми, используйте каноническое представление. В foundation-пакете для JSON-like технических значений есть `stableStringify`:
655
+
656
+ ```ts
657
+ import { hashString128 } from "@ryuzaki13/react-foundation-lib/crypto";
658
+ import { stableStringify } from "@ryuzaki13/react-foundation-lib/utils";
659
+
660
+ const fingerprint = hashString128(stableStringify(config));
661
+ ```
662
+
663
+ Перед использованием определите доменные правила:
664
+
665
+ - важен ли порядок массивов;
666
+ - нужно ли сохранять внешний whitespace;
667
+ - одинаковы ли uppercase/lowercase;
668
+ - как представляются даты;
669
+ - допустимы ли `undefined`, функции или циклические ссылки;
670
+ - на какой boundary выполняется нормализация.
671
+
672
+ Hash не исправляет неоднозначную сериализацию. Он только детерминированно обрабатывает полученную строку.
673
+
674
+ ### Составной строковый ключ
675
+
676
+ Простое объединение может быть неоднозначным:
677
+
678
+ ```ts
679
+ `${first}:${second}`;
680
+ ```
681
+
682
+ Если значения сами содержат `:`, разные пары способны сформировать одинаковую строку. Используйте однозначную serialization scheme:
683
+
684
+ ```ts
685
+ const canonical = stableStringify([first, second]);
686
+ const key = hashString128Base64Url(canonical);
687
+ ```
688
+
689
+ ## Безопасность
690
+
691
+ ### Матрица допустимости
692
+
693
+ | Задача | Подходит ли модуль | Почему |
694
+ | --------------------------------------------- | --------------------- | ---------------------------------------------------------- |
695
+ | UI/cache fingerprint некритичных данных | Да | Быстро и детерминированно. |
696
+ | Stable DOM-like ID | Да, с учётом коллизий | Есть компактный URL-safe формат и prefix. |
697
+ | Случайный ID новой сущности | Да | `uuidv4` использует Web Crypto. |
698
+ | Проверка пароля | Нет | Нужен slow password hashing с salt и политикой параметров. |
699
+ | Подпись запроса | Нет | Нужен keyed cryptographic MAC/signature. |
700
+ | Проверка загруженного файла по trusted digest | Нет | Нужен cryptographic digest и доверенный expected value. |
701
+ | Сокрытие email/телефона | Нет | Предсказуемый вход можно подобрать перебором. |
702
+ | Access token | Не автоматически | UUID не заменяет полный security contract. |
703
+ | Уникальность записи в базе | Недостаточно | Нужен unique constraint/authoritative allocation. |
704
+
705
+ ### Не принимайте security decision по этим hash
706
+
707
+ Нельзя предоставлять доступ, считать payload доверенным или пропускать validation только потому, что hash совпал. Non-cryptographic hash не защищён от намеренно подобранных коллизий.
708
+
709
+ ### Не храните hash как доказательство исходного секрета
710
+
711
+ ```ts
712
+ hashString128(password);
713
+ ```
714
+
715
+ Так хранить пароль нельзя. Функция быстрая, не использует salt и специально не предназначена для сопротивления перебору.
716
+
717
+ ### Не путайте Base64Url с безопасностью
718
+
719
+ Base64Url — только формат записи байтов. Он не шифрует и не подписывает значение.
720
+
721
+ ## Коллизии и обрезание результата
722
+
723
+ ### Что гарантирует детерминированность
724
+
725
+ - одинаковая точная строка даёт одинаковый hash;
726
+ - если два hash различаются, исходные строки точно различались;
727
+ - если два hash совпали, исходные строки могли быть одинаковыми или попасть в коллизию.
728
+
729
+ Последний пункт особенно важен: совпадение hash не доказывает равенство исходных данных.
730
+
731
+ ### Сохраняйте исходный ключ, если ошибка недопустима
732
+
733
+ Для map/cache можно хранить bucket по hash, а внутри дополнительно сравнивать каноническую строку:
734
+
735
+ ```ts
736
+ type Entry<T> = {
737
+ canonical: string;
738
+ value: T;
739
+ };
740
+ ```
741
+
742
+ Так коллизия hash не смешает разные данные.
743
+
744
+ ### Не обрезайте длинный hash без причины
745
+
746
+ ```ts
747
+ hashString128Base64Url(input).slice(0, 8);
748
+ ```
749
+
750
+ Обрезание уменьшает число возможных результатов и повышает вероятность коллизии. Если короткая форма обязательна, оцените размер набора, последствия совпадения и наличие secondary equality check.
751
+
752
+ ### Hex и Base64Url имеют одинаковую collision policy
753
+
754
+ 32 hex-символа и 22 Base64Url-символа представляют один внутренний результат. Выбирайте по требованиям к формату, а не по предполагаемой «надёжности».
755
+
756
+ ### Версионируйте долговечный fingerprint
757
+
758
+ Если результат сохраняется в БД, URL или файле на годы, алгоритм становится частью формата данных. Добавьте версию схемы:
759
+
760
+ ```ts
761
+ const fingerprint = `v1:${hashString128(canonical)}`;
762
+ ```
763
+
764
+ Тогда будущая замена алгоритма сможет использовать `v2:` и явную миграцию. Не предполагайте, что внутренний алгоритм любой utility-функции никогда не изменится между major versions.
765
+
766
+ ## Browser, Node.js и SSR
767
+
768
+ ### Чистые строковые варианты
769
+
770
+ `hashString` и `hashString128` используют стандартные операции JavaScript и не обращаются к DOM или Web Crypto.
771
+
772
+ ### `btoa` boundary
773
+
774
+ `hashString128Base64Url` и `stringToElementId` используют глобальный `btoa`. В browser он обычно доступен. В server/test runtime его наличие нужно проверить или предоставить совместимый polyfill на уровне среды.
775
+
776
+ Не добавляйте случайный локальный encoder с другим byte order: server и client должны выдавать одинаковый результат.
777
+
778
+ ### Web Crypto boundary
779
+
780
+ `uuidv4` обращается к глобальному `crypto`. В runtime без Web Crypto функция завершится ошибкой.
781
+
782
+ ### Hydration
783
+
784
+ Не вызывайте `uuidv4()` прямо во время server и client render одного и того же компонента:
785
+
786
+ ```tsx
787
+ // Неправильно для SSR и нестабильно при повторных рендерах.
788
+ const id = uuidv4();
789
+ ```
790
+
791
+ Server и client создадут разные строки, а каждый render создаст новый UUID.
792
+
793
+ Для DOM/ARIA ID используйте `useId`. Для ID новой domain entity вызывайте `uuidv4` в момент пользовательской команды или на authoritative creation boundary.
794
+
795
+ Детерминированный `stringToElementId` может совпасть на server и client только если:
796
+
797
+ - обе стороны получили совершенно одинаковый input и prefix;
798
+ - обе стороны используют ту же версию алгоритма;
799
+ - обе среды поддерживают нужный `btoa` contract.
800
+
801
+ ## React-рецепты
802
+
803
+ ### Новый ID при добавлении элемента
804
+
805
+ ```tsx
806
+ function DraftList() {
807
+ const [items, setItems] = useState<Array<{ id: string; title: string }>>([]);
808
+
809
+ const addItem = () => {
810
+ setItems((current) => [
811
+ ...current,
812
+ {
813
+ id: uuidv4(),
814
+ title: "Новый элемент"
815
+ }
816
+ ]);
817
+ };
818
+
819
+ return (
820
+ <button type="button" onClick={addItem}>
821
+ Добавить
822
+ </button>
823
+ );
824
+ }
825
+ ```
826
+
827
+ UUID создаётся только при команде добавления, а не при каждом render.
828
+
829
+ ### Client-only ID, сохранённый в state
830
+
831
+ ```tsx
832
+ function ClientOnlyDraft() {
833
+ const [id] = useState(() => uuidv4());
834
+
835
+ return <span>{id}</span>;
836
+ }
837
+ ```
838
+
839
+ Такой вариант сохраняет ID между client renders, но не решает SSR hydration. В development Strict Mode initializer может быть вызван повторно для проверки чистоты; использовать UUID как side effect вне state нельзя.
840
+
841
+ ### Стабильный row ID по business key
842
+
843
+ ```tsx
844
+ function CustomerRow({ customerId }: { customerId: string }) {
845
+ const rowId = stringToElementId(customerId, "customer-row");
846
+
847
+ return <tr id={rowId}>{/* Ячейки строки. */}</tr>;
848
+ }
849
+ ```
850
+
851
+ Если `customerId` изменится, изменится и DOM ID. Если две строки имеют одинаковый customer ID, они получат одинаковый DOM ID.
852
+
853
+ ## Тестирование
854
+
855
+ ### Проверяйте детерминированность и формат
856
+
857
+ ```ts
858
+ import { describe, expect, it } from "vitest";
859
+ import { hashString128, hashString128Base64Url } from "@ryuzaki13/react-foundation-lib/crypto";
860
+
861
+ describe("fingerprint", () => {
862
+ it("стабилен для одинакового входа", () => {
863
+ expect(hashString128("row:42")).toBe(hashString128("row:42"));
864
+ });
865
+
866
+ it("имеет ожидаемый формат", () => {
867
+ expect(hashString128("row:42")).toMatch(/^[0-9a-f]{32}$/);
868
+ expect(hashString128Base64Url("row:42")).toMatch(/^[0-9A-Za-z_-]{22}$/);
869
+ });
870
+ });
871
+ ```
872
+
873
+ Тест `hash(a) !== hash(b)` для одной выбранной пары не доказывает отсутствие коллизий вообще.
874
+
875
+ ### Test vectors текущей реализации
876
+
877
+ | Input | `hashString` | `hashString128` | `hashString128Base64Url` |
878
+ | ----------- | ------------ | ---------------------------------- | ------------------------ |
879
+ | `""` | `ztntfp` | `c431d55b3afb3e003e41d50fc08b3e54` | `xDHVWzr7PgA-QdUPwIs-VA` |
880
+ | `"user:42"` | `d5nw5e` | `fa3326027b653e8049d56072c88378f0` | `-jMmAntlPoBJ1WByyIN48A` |
881
+ | `"строка"` | `hb03a2` | `86aac3ec19e5093dc3b2f1985cfd3b49` | `hqrD7BnlCT3DsvGYXP07SQ` |
882
+
883
+ Exact vectors полезны, если hash является частью сериализуемого формата. Если формат не долговечный, часто достаточно проверять детерминированность и regex.
884
+
885
+ ### Mock native `randomUUID`
886
+
887
+ ```ts
888
+ import { afterEach, expect, it, vi } from "vitest";
889
+
890
+ afterEach(() => {
891
+ vi.unstubAllGlobals();
892
+ });
893
+
894
+ it("использует native randomUUID", () => {
895
+ const randomUUID = vi.fn(() => "00000000-0000-4000-8000-000000000000");
896
+ vi.stubGlobal("crypto", { randomUUID } as unknown as Crypto);
897
+
898
+ expect(uuidv4()).toBe("00000000-0000-4000-8000-000000000000");
899
+ expect(randomUUID).toHaveBeenCalledOnce();
900
+ });
901
+ ```
902
+
903
+ ### Mock fallback `getRandomValues`
904
+
905
+ ```ts
906
+ it("устанавливает version и variant bits", () => {
907
+ const getRandomValues = vi.fn((bytes: Uint8Array) => {
908
+ bytes.set([0, 1, 2, 3, 4, 5, 0xff, 7, 0xff, 9, 10, 11, 12, 13, 14, 15]);
909
+ return bytes;
910
+ });
911
+
912
+ vi.stubGlobal("crypto", { getRandomValues } as unknown as Crypto);
913
+
914
+ expect(uuidv4()).toBe("00010203-0405-4f07-bf09-0a0b0c0d0e0f");
915
+ });
916
+ ```
917
+
918
+ Всегда восстанавливайте global mocks после теста, иначе они повлияют на другие test cases.
919
+
920
+ ### Проверка реального UUID без mock
921
+
922
+ Если среда предоставляет Web Crypto:
923
+
924
+ ```ts
925
+ expect(uuidv4()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
926
+ ```
927
+
928
+ Не фиксируйте конкретное случайное значение без mock.
929
+
930
+ ## Производительность
931
+
932
+ ### Сложность hash
933
+
934
+ Каждая hash-функция проходит строку один раз. Временная сложность — `O(n)`, где `n` является числом UTF-16 code units.
935
+
936
+ Алгоритмы работают быстро и синхронно. Но длинная строка всё равно блокирует текущий JavaScript thread на время обработки.
937
+
938
+ ### Не хешируйте одно и то же на каждом render без необходимости
939
+
940
+ Для большого canonical payload можно вычислять fingerprint на boundary изменения или мемоизировать по стабильной ссылке:
941
+
942
+ ```tsx
943
+ const fingerprint = useMemo(() => hashString128(stableStringify(config)), [config]);
944
+ ```
945
+
946
+ `useMemo` помогает только если зависимость `config` действительно стабильна между неизменившимися renders.
947
+
948
+ ### UUID и hex table
949
+
950
+ Fallback `uuidv4` создаёт таблицу из 256 hex-строк при каждом вызове. Для обычного создания ID это несущественно. Не используйте функцию как tight-loop генератор миллионов значений на UI thread без измерений.
951
+
952
+ ## Ограничения и частые ошибки
953
+
954
+ ### Использование `hashString` как уникального primary key
955
+
956
+ 32-bit hash слишком мал для такого обещания. Используйте UUID/backend ID или храните исходный ключ и обрабатывайте коллизии.
957
+
958
+ ### Hash пароля или токена
959
+
960
+ ```ts
961
+ const storedPassword = hashString128(password); // Небезопасно.
962
+ ```
963
+
964
+ Эта функция быстрая, несолёная и non-cryptographic.
965
+
966
+ ### Нестабильная сериализация объекта
967
+
968
+ Если порядок ключей или нормализация входа плавают, hash тоже будет меняться. Сначала создайте canonical string.
969
+
970
+ ### Ожидание одинакового hash для визуально одинакового Unicode
971
+
972
+ Unicode normalization не выполняется. Выберите NFC/NFD policy явно, если это требуется доменом.
973
+
974
+ ### Обрезание UUID или hash
975
+
976
+ Обрезание уменьшает пространство результатов. Нельзя сохранять прежние гарантии после `.slice(...)`.
977
+
978
+ ### `uuidv4()` внутри render
979
+
980
+ Новый UUID будет создаваться при каждом render и может нарушить SSR hydration. Создавайте entity ID на command boundary; для DOM relationships используйте `useId`.
981
+
982
+ ### Небезопасный prefix
983
+
984
+ `stringToElementId` не очищает prefix. Пробелы, кавычки или CSS-special characters останутся в строке.
985
+
986
+ ### Ожидание server portability
987
+
988
+ `btoa` и global Web Crypto могут отсутствовать в server/test runtime. Тестируйте целевую среду и не подменяйте алгоритм несовместимой реализацией.
989
+
990
+ ### Сравнение только по hash
991
+
992
+ Совпавший hash не доказывает полное равенство. Для критичных данных сравнивайте исходную canonical string или содержимое.
993
+
994
+ ## Краткий справочник API
995
+
996
+ | API | Вход | Результат | Детерминированность | Runtime | Security |
997
+ | ------------------------ | -------------------------- | ------------------------- | ------------------- | ------------------------ | ----------------------------- |
998
+ | `hashString` | `string` | 1–7 base36 символов | Да | Обычный JS | Non-cryptographic, 32-bit |
999
+ | `hashString128` | `string` | 32 lowercase hex-символа | Да | Обычный JS + `Math.imul` | Non-cryptographic |
1000
+ | `hashString128Base64Url` | `string` | 22 URL-safe символа | Да | Дополнительно `btoa` | Тот же hash, другой формат |
1001
+ | `stringToElementId` | `input`, optional `prefix` | `${prefix}-${22 symbols}` | Да | Дополнительно `btoa` | Коллизия возможна |
1002
+ | `uuidv4` | Нет | UUID v4 string | Нет | Global Web Crypto | Random ID, не security system |
1003
+
1004
+ ## Вопросы и ответы
1005
+
1006
+ ### Какую hash-функцию выбрать по умолчанию?
1007
+
1008
+ Для некритичного устойчивого fingerprint обычно выбирайте `hashString128`. Если важна компактность и URL-safe формат — `hashString128Base64Url`. `hashString` оставляйте для сценариев, где 32-bit коллизии безопасно обрабатываются.
1009
+
1010
+ ### Почему Base64Url-результат короче hex?
1011
+
1012
+ Hex кодирует 4 bits одним символом, а Base64 — 6 bits. Поэтому те же 16 байтов помещаются в 22 Base64Url-символа вместо 32 hex-символов.
1013
+
1014
+ ### Можно ли восстановить исходную строку из hash?
1015
+
1016
+ Прямой операции восстановления нет. Но короткие и предсказуемые значения можно подобрать перебором, поэтому hash не является безопасным сокрытием данных.
1017
+
1018
+ ### Гарантирует ли `hashString128`, что коллизий не будет?
1019
+
1020
+ Нет. Более длинный результат уменьшает практический риск для non-adversarial сценариев, но математическая и security-гарантия отсутствует.
1021
+
1022
+ ### Почему `Café` иногда даёт другой hash?
1023
+
1024
+ В Unicode визуально одинаковый текст может состоять из разных code points/code units. Модуль не выполняет normalization. Если домен требует равенства, приведите обе строки к одной форме, например NFC, до хеширования.
1025
+
1026
+ ### Можно ли использовать `stringToElementId` для React label/input?
1027
+
1028
+ Можно при стабильном внешнем key, но для локальной связи внутри SSR React-компонента предпочтительнее `useId`.
1029
+
1030
+ ### Уникален ли `stringToElementId` во всём документе?
1031
+
1032
+ Только если вызывающий код обеспечивает уникальные input/prefix и принимает риск hash-коллизии. Функция не проверяет уже существующие DOM IDs.
1033
+
1034
+ ### Почему ID иногда содержит два дефиса подряд?
1035
+
1036
+ Один дефис добавляется после prefix, а Base64Url hash сам может начинаться с `-`. Это нормальный результат.
1037
+
1038
+ ### Можно ли вызвать `uuidv4` на сервере?
1039
+
1040
+ Только если server runtime предоставляет совместимый global Web Crypto contract. Для переносимой server-логики используйте принятый в серверном проекте crypto API и проверьте, что формат/политика совпадают.
1041
+
1042
+ ### Можно ли использовать UUID как React `key`?
1043
+
1044
+ Да, если UUID один раз сохранён в самой entity. Нельзя генерировать новый `uuidv4()` прямо внутри `.map()` на каждом render: ключи будут постоянно меняться, и React станет пересоздавать элементы.
1045
+
1046
+ ### Нужно ли проверять UUID на конфликт в базе?
1047
+
1048
+ Да. Очень малая вероятность не заменяет unique constraint у authoritative storage.
1049
+
1050
+ ### Стабильны ли exact hash между версиями пакета?
1051
+
1052
+ В рамках текущей реализации — да. Если вы сохраняете результат надолго или используете его как внешний протокол, версионируйте формат и зафиксируйте test vectors. Это позволит безопасно обнаружить или мигрировать будущую смену алгоритма.