Формат id
Один формат для всех первичных id в Agentfy, Bonsite, Securify, Shiffty и Store Deputy (решение владельца 2026-10-15). Эта страница — эталон: остальные четыре продукта копируют отсюда модуль, тест-вектор и таблицу.
<prefix>-<adjective>-<noun>-<suffix>
user-calm-rivers-k8m4p2d7n9x3Новые id выдаются только в каноническом виде. Прежний формат каждого продукта по-прежнему принимается для уже выданных id (LEGACY_ID_FORMATS), но никогда не генерируется. Исключение — AGNT2-588: organization-* меняется на org-* с сохранением всего хвоста; формат слов и суффикса не меняется.
Формат
| Часть | Правило |
|---|---|
prefix | Полное имя модели в kebab-case, но слово Organization сокращается до org (OrganizationMember → org-member, AgentMcpServer → agent-mcp-server), нижний регистр, до 32 символов, сам может содержать -. Реестр — ID_ENTITIES в api/src/slices/admin/idMigration/data/idInventory.ts. |
adjective | Одно слово из adjectives пакета human-id 4.1.3 — 200 слов. |
noun | Одно слово из nouns human-id 4.1.3 — 300 слов. Без verb/adverb, без своих словарей. |
suffix | Ровно 12 символов Crockford base32 в нижнем регистре: 0123456789abcdefghjkmnpqrstvwxyz (без i, l, o, u). 60 бит. |
| случайность | Криптографическая: по умолчанию Node crypto.randomBytes. Слова — отбраковкой по маске, суффикс — customRandom из nanoid 3.3.11. |
| уникальность | Первичный ключ. При конфликте вся запись повторяется, не более 5 раз (withIdRetry). Слова в уникальность не входят. |
| длина | Не более 32 + 1 + 7 + 1 + 8 + 1 + 12 = 62 символов. Любое принимаемое значение, включая legacy, — не более 80 (ID_MAX_LENGTH). |
Словари — массивы, которые экспортирует пакет, в порядке пакета, приведённые к нижнему регистру; слово с символом вне [a-z] при загрузке отбрасывается (в 4.1.3 таких нет). Зависимости закреплены точно, без ^: "human-id": "4.1.3", "nanoid": "3.3.11" (CommonJS).
Пример user-calm-river-… из общей задачи не является валидным id: слова river в human-id нет (есть rivers).
Генерация — точный алгоритм
Эталон, одинаковый байт в байт во всех продуктах (зафиксирован координатором по SHIF-28, Agentfy/shiffty#37). formatId(prefix, random = crypto.randomBytes) читает из источника random(n) (отдаёт n байт) по порядку:
- adjective —
index(200) - noun —
index(300) - suffix —
customRandom(ID_SUFFIX_ALPHABET, 12, random)()из nanoid 3.3.11 с тем же источником
index(n) читает минимальное число целых байт, вмещающее n − 1 (big-endian), обрезает значение маской до битовой длины n − 1 и повторяет, пока значение не меньше n:
| n | бит | байт на попытку | маска | принимается, если |
|---|---|---|---|---|
| 200 (adjectives) | 8 | 1 | 255 | значение < 200 |
| 300 (nouns) | 9 | 2 | 511 | (b0·256 + b1) & 511 < 300 |
customRandom nanoid для алфавита из 32 символов и длины 12 запрашивает ceil(1.6 × 31 × 12 / 32) = 19 байт одним вызовом, читает их с последнего к первому, берёт byte & 31 (все значения в диапазоне) и останавливается на 12 символах; первые 7 байт запроса не используются.
Контрольный тест-вектор
Источник отдаёт байты 0, 1, 2, 3, … по порядку (каждый вызов продолжает счёт):
formatId('user', counter) === 'user-afraid-swans-nmkjhgfedcba'Байт 0 → adjectives[0] = afraid; байты 1, 2 → 258 & 511 = 258 → nouns[258] = swans; один запрос байт 3…21, прочитанных с 21 до 10 → символы 21…10 алфавита = nmkjhgfedcba. Читается ровно 22 байта. Эту строку проверяет набор тестов каждого продукта; другой результат означает другой словарь, порядок, алфавит или алгоритм — то есть неверную копию.
Проверка
isCanonicalId(value, prefix?) принимает ровно
^(<префиксы реестра, длинные первыми, экранированные>)-<adjective>-<noun>-[0-9abcdefghjkmnpqrstvwxyz]{12}$с проверкой обоих слов по словарям (а не просто [a-z]+). prefix — один префикс, список или не указан (любой корректный префикс). Нормализации нет: верхний регистр, похожие Unicode-символы, percent-encoding и пробелы отвергаются. Число хвостовых сегментов фиксировано, поэтому префикс с - однозначен: team-api-key-… никогда не id team.
isLegacyId(value, prefix?) принимает LEGACY_ID_FORMATS продукта. isId(value, prefix) — то, что используют валидаторы DTO (@IsEntityId), pipe и gateway, — это isCanonicalId || isLegacyId. Проверка ничего не ищет и доступа не даёт.
Не канонический вид (тест в каждом продукте): третье слово (verb), суффикс из 6 или 20 символов, hex-суффикс другой длины, i/l/o/u в суффиксе, слово вне словарей — если значение не совпадает с legacy-форматом продукта. 12 hex-символов — подмножество алфавита Crockford, поэтому …-a1b2c3d4e5f6 является каноническим; различить это нельзя и не нужно.
Модуль
api/src/slices/setup/id/domain/idFormat.ts — одинаковый во всех пяти репозиториях, кроме LEGACY_ID_FORMATS. Экспорты:
| Экспорт | |
|---|---|
ID_SUFFIX_ALPHABET, ID_SUFFIX_LENGTH = 12 | суффикс |
ID_WORDS { adjectives, nouns } | словари после загрузки |
formatId(prefix, random?) | единственный способ создать новый id |
isCanonicalId(value, prefix?), isLegacyId(value, prefix?), isId(value, prefix) | проверка |
LEGACY_ID_FORMATS, legacyIdFormat(value, prefix?) | прежние форматы продукта |
assertIdPrefix, idPrefixOf, idPattern, ID_MAX_LENGTH | проверка префикса, шаблон OpenAPI |
Реестр префиксов остаётся там, где он у продукта. Чистый TypeScript (без enum и parameter properties), поэтому Node загружает его напрямую.
Прежние форматы по продуктам
| Продукт | LEGACY_ID_FORMATS (принимается, не генерируется) |
|---|---|
| Agentfy | <prefix>-<adjective>-<noun>-<verb>-<20 Crockford> — adjective/noun/verb из human-id 4.1.3, записи на dev после AGNT2-535/537; голый UUID в нижнем регистре — исторические записи, которые ещё переводит окно AGNT2-535 |
| Bonsite | 3 слова + 20 Crockford |
| Securify | 2 слова + 12 hex (неотличимо от канонического — см. выше) |
| Shiffty | 2 слова из своего словаря + 6 Crockford |
| Store Deputy | 2 слова + 12 [a-z0-9] |
Agentfy: окно перевода
Исходное окно выполнено на dev в AGNT2-580. Решение владельца AGNT2-588 (2026-10-24) заменяет часть правила AGNT2-537: Organization, OrganizationMember, OrganizationCredential и OrganizationPolicy получают префиксы org, org-member, org-credential и org-policy. OrganizationAudit остаётся техническим UUID.
Одна команда api/scripts/id-migrate.ts работает и на legacy-базе (сразу выдаёт текущие префиксы), и после прежней миграции (заменяет только organization- на org-, сохраняя весь хвост). Платформенный ID — org-silver-days-zxy8s9bpp6hp. Старые HTTP-адреса отвечают not found, без алиасов. Исторические ссылки провайдеров разрешаются по цепочке записанных переходов.
Перед запуском нужны остановка писателей, зашифрованный backup и проверка восстановления. В одной транзакции обновляются ссылки, привязки шифрования и значения по умолчанию, отзываются сессии и подписанные контексты. Повтор меняет 0 ID. Сид существующей локальной базы требует сначала явной миграции и не создаёт дубли. История переходов, аудит и непрозрачные пользовательские данные не подвергаются глобальной замене строк. Процедура — specs/AGNT2-535-id/transition.md; фактические результаты — в отчёте AGNT2-588.