Журнал изменений
Что нового.
Каждое усовершенствование платформы, как только она появится.
· kamo-signer-monorepo
Публичная квота/оценочный документ по адресу /{locale}/q/{token}
Поверхность, обращенная к клиенту для системы торговых котировок. Серверная версия, таким образом, Брендинг организации находится в первой краске — небрендовая вспышка на коммерческом документе Это именно то, что это не должно делать, и именно поэтому страница издает фирменный обычай. свойства встраиваются через QuoteBrandVars, а не меняют их после гидратации. Живет в kamo-signer, потому что в этом приложении уже есть вход HostRegexp. охват, псевдонимы DNS и тематический пакет с именем хостаOverride — и потому e-sign - это один прыжок, так как EsignGenericEnvelope связывает любой Запись через (context type, context id). Повторное использование было жизнеспособным только после /{locale}/ i18n work landed; все 31 строка являются ключами перевода, полученными из словаря. Формат денег и дат через next-intl с использованием собственной валюты котировки и Разрешенная сервером часовая зона (член-получатель -> org -> UTC), никогда не устройство зрителя. По истечении срока действия в неправильной зоне можно назвать дату, на которую уже поступило предложение. мертв. Добавлен первый реальный блок печати @media. "Счет за печать" в камо-интернале Всегда печатал весь хром приложения, потому что не существовало правила печати @media. где угодно; здесь та же разметка печатается как чистый Письменный документ с брендом сохранившийся акцент (печатно-цветовой: точный — бесцветная цитата читается как черновик), панель действий подавлена, а взлом страницы внутри: избегайте строк и итогов. Терминальные состояния намеренно асимметричны: EXPIRED получает честную фирменную страницу назвать дату и контактные данные организации, безопасно, потому что звонящий уже доказанное владение 256-битным токеном; неизвестный/отозванный/неправильный-орг все сжимаются до одного Общая страница, поэтому поверхность не может быть исследована. Клиент указывает причину Перед рендерингом. Токен никогда не перемещается дальше по URL - решение на стороне сервера публикует его в виде Тело и BFF пересылают реальный IP-адрес клиента явно, потому что APIService печатает X-Forwarded-For из своего собственного входящего запроса, который в противном случае поставил бы каждый Просмотрщик в одном ковше с ограничением скорости, нажатом на IP-пакет. Добавляет SECURITYSERVICE URL в configmap (проверено: kamowssecurity-service существует) В пространстве имен Камо.
· SecurityService
Quote REST surface — staff management and public share links
Staff endpoints at /api/security/commerce/quotes and an unauthenticated resolver at /api/security/public/quotes. QuoteController extends BaseLosController rather than copying POSController's preamble: CallerSessionResolver accepts the X-OTK attribute, the X-***-Token header AND the *** cookie, where POSController's private getOrgId reads only the OTK attribute. Every handler is rights-gated (VIEW/MANAGE/SEND_QUOTES). Most of the commerce surface is session-only — any authenticated member of any org can mutate orders — and that is deliberately not repeated here, because a quote commits the org to a price in front of a customer. QuotePublicController needs no security-config carve-out: the Spring chain is anyRequest().permitAll() and OTKPreAuthFilter only acts on an X-OTK header, which a browser following an emailed link never sends. Design points: - The token arrives in the POST body, never a URL. APIApp.forward() logs request lines, and a bearer credential in a path would land in those logs. - Not an enumeration oracle: unknown, revoked and org-mismatched tokens all return one indistinguishable generic failure. Only EXPIRED gets a specific, honest answer, which is safe because the caller already proved possession of a 256-bit token. - The org derived from the request host is cross-checked against the org on the link row. Without it, a valid link for org A opened under org B's host would render A's pricing wearing B's branding — a phishing primitive. - no-store / Referrer-Policy: no-referrer / X-Robots-Tag: noindex on every response. The page loads logos from theme.<org>, and a Referer would carry the token off-origin. PublicLinkRateLimiter is Redis-backed because SecurityService is multi-replica — in-process counters would be a limiter in name only. It is explicitly DoS and log-hygiene protection, NOT the anti-guessing control: 256 bits of entropy is. Failures are counted far more harshly than successes, since one office opening a forwarded quote fifty times is normal while fifty misses is not. It fails open, so a Redis outage cannot take the quote page down. QuoteEmailClient reuses the EmailService transactional path (X-Internal-Auth, QUOTE_SENT template) and sends only the headline figure plus the link — never the itemised pricing, so a forwarded email is not itself the disclosure. Pre-existing test failures unchanged by this commit (verified by stashing this work and re-running on clean HEAD): **************** NPE on a null EntityManager, and the legacy com.retval SecurityServiceApplicationTests missing a @SpringBootConfiguration. REQUIRES a KamoInitializerService run before these endpoints work: the quote tables are new and ddl-auto is none here, so the service boots normally and only the quote endpoints 500 on a missing relation until KI has run.
· kamo-shared-library
Quote/estimate domain — entities, pricing engine, share links
Adds a first-class Quote alongside the existing commerce spine. The pre-existing "ServiceQuote" is an Intent plus a JSON blob carrying one offering and one flat amount, with no recipient, no expiry and no UI; it is not a foundation. Extending Intent directly was rejected too: commerce_intents is shared by LOAN_APPLICATION, RENTAL_REQUEST, SUBSCRIPTION_SIGNUP and PURCHASE_REQUEST, so ~25 quote columns would put permanent NULLs on the mortgage and rental paths. Entities **************** Quote engagement-linked, versioned, valid_until + valid_for_days QuoteLineItem snapshots name/sku/price so a sent quote never re-prices QuoteShareLink bearer credential, SHA-256 at rest, revoke/expiry/view counts QuoteLinkView per-open audit (ip, user-agent) QuoteEvent append-only lifecycle trail Totals are server-authoritative. QuoteTotals is the single writer of every money field and the request DTOs carry no totals at all — deliberately unlike createRetailOrder, which takes subtotal/tax/total verbatim and can be handed a populated order with total = 0. Line discounts apply before the quote-level discount, discounts can never exceed their base, intermediate maths stays at scale 4 and only the grand total rounds to the currency's minor unit. Share tokens: 32 bytes of SecureRandom as 43 base64url chars (256 bits). Same entropy as the e-sign signer token but 21 chars shorter, keeping the emailed URL under the 76-char plain-text wrap limit. Only the SHA-256 is persisted. Tax is honest rather than invented: the platform has no tax engine (TaxClass/ TaxZone/TaxRate are authorable and read by no calculation), so ENGINE mode reports NOT_CONFIGURED instead of fabricating a figure on a commercial document. QuoteLineItem.taxClassId is UUID, matching TaxClass's real PK — Offering.taxClassId is a Long against a UUID-keyed table and is structurally unresolvable. Also: rights 178-180 (VIEW/MANAGE/SEND_QUOTES) and the QUOTE_SENT canonical email template, registered in CANONICAL_KEYS (absent there means a permanent 404). 35 tests covering the pricing engine and token hygiene. Full suite: 896 passing. Schema note: these are new tables. KamoInitializerService must run before SecurityService serves the quote endpoints — ddl-auto is none there, so the service boots fine and the endpoints 500 on a missing relation until it does.
· KlusterServices
Сдерживание выхода для аватара, как CiliumNetworkPolicy
Восстанавливает то, что 8f6c672 вернул, в единственной форме, которая действительно работает здесь. Предыдущая попытка провалилась, потому что простая сетевая политика не может выразить это. Топология кластера. Три из четырех критических зависимостей Службы безопасности живут на узел, а не в стручковой сети — cockroachdb-0 и nats-k1m1-0 запускают hostNetwork; MinIO - это процесс хостинга, поэтому подселектор никогда не может сравниться с ними. The Неочевидная часть, и причина, по которой первая попытка патча также не удалась, заключается в том, что ipBlock для подсети узла также не фиксирует его: Cilium классифицирует узел как Зарезервированный идентификатор хоста / удаленного узла, а правила CIDR намеренно не совпадают Зарезервированные личности. Сущности — единственная конструкция, которая выражает это. Оба режима отказа были воспроизведены против выброшенной канарейки перед написанием. Это, скорее, против производства. Канарейка придерживалась того же правила, что и Отличительная метка, как версия ipBlock была поймана молча блокируя CockroachDB, NATS и MinIO выглядят вполне разумно. Проверено на канаре, затем подтверждено на службе безопасности без перезапуска и без перезапуска. Ошибки подключения: Allow dns, crdb:26257, nats:4222, minio:9000, redis:6379, siblings:80, world:443 BLOCK node:22, node:443, kube-api:443, redis-via-node:6379, 169.254.169.254, loopback:9000 Последние четыре являются точкой: SSRF в растворителе теперь некуда идти. The Защита в коде остается основным контролем - это единственный слой, который понимает Обновление DNS — и это защита за ним. Заголовок несет канарейку. Любой, кто меняет это, должен скорее повторить это. чем причина; модель идентичности Cilium ведет себя не так просто Интуиция сетевой политики предполагает, что нет инсценировки.
· kamo-internal
Показать facepile для групповых разговоров
Нить с пятью людьми на ней была представлена одним произвольным лицом, которое читается как сообщение от одного человека. До трех участников теперь пересекаются, поэтому «Несколько человек» разборчиво на первый взгляд, без необходимости разбирать строку имени. Зажат в три: мимо того, что круги сжимаются ниже узнаваемости и ряда становится текстурой, а не информацией, а остальная часть уже Текст "и N других" рядом с ним. Читатель отфильтрован из собственного списка потоков, а другая сторона ведет — Лица читаются слева направо, поэтому основной корреспондент должен быть самым левым. И тот, который носит непрочитанный значок. Каждое лицо получает кольцо в ряд Цвет фона; без него стопка похожих фотографий сливается в одну форму. В настоящее время резолюция охватывает лишь первых нескольких участников, а не только ведущих участников. В противном случае второе и третье лица будут сидеть как монограммы постоянно.
· SecurityService
Инструмент цепочки, пожинать просроченные записи, раскрывать записи
Каждый режим отказа в этой функции молчит. Зацепленный выключатель, рабочий Очередь, отбрасывающая каждую задачу, провайдер, который начал возвращать HTML — каждый выглядит так, как будто он не работает. Точно так же, как «у этого корреспондента нет фотографии», что также верно. Поведение выглядит так. Не существует видимой разницы между работой и Он полностью мертв, поэтому его необходимо измерить. Метрики (Прометей уже был подключен, так что это в основном именование): удар кэша Соотношение, продолжительность цепи, отмеченная ярусом, который ответил: per-provider hit/miss, Глубина очередей рабочих и суммарная отброшенная задача. Последний из ранее использовавшихся Отбросьте политику, которая тихо падает по замыслу; теперь она увеличивает счетчик. счетчик объявляется перед исполнителем, потому что инициализаторы поля работают в Порядок источника и опора на ленивый захват нарушили бы безвредный порядок. Раскрытие информации идет к собственному лесорубу. Только уровень идентичности является личным. раскрытие — уровни домена передают правую сторону адреса, который Идентифицирует никого — и это записывается до звонка, а не после, поэтому След существует даже тогда, когда запрос не выполняется. Адрес написан полностью и намеренно: суть в том, чтобы ответить «с кем были переданы данные этого человека», И хэш не может на это ответить. Жнец закрывает утечку: удалитьExpiredBefore не существовало и ничего не называлось так Идентичность sender выросла навсегда. Занятый почтовый ящик касается тысяч корреспондентов Один раз каждый и оставляет отрицательный ряд позади для каждого. Тесты покрывают фетчер против настоящей розетки, потому что интересные случаи Поведение всех провайдеров, а не логика: 404 с действительным изображением (DuckDuckGo), мягкий 404, обслуживающий HTML, происхождение, занижающее длину контента, 1x1 stub и перенаправление, направленное в сеть стручков. Производственный клиент не может быть направлены на сервер теста обратной связи — его DNS-пин отказывается разрешать что-либо частный, в чем и заключается весь смысл — так что те, кто бежит против расслабленного клиента, и Пин утверждается отдельно.
· kamo-internal
Продвигайте проглоченную матрицу LLPA в настройки консоли
Панель приема может публиковать только таблицу ставок. Источник LLPA MATRIX документ, в котором теперь отображается карта «Продвижение как корректировка LLPA»: выберите существующую Версия тарифного листа LLPA корректируется (+ необязательная группа), и каждый утвержденный Ковш FICO/LTV становится условной ценовой корректировкой. Результат называет, как Многие были написаны и, честно говоря, сколько рядов было пропущено из-за ведра Его нельзя было прочитать или он уже присутствовал. Обсуждение Api.promoteAdjustments Отправлено в новый Gateway Relay.
· SecurityService
Реле шлюза для продвижения матрицы LLPA в условные настройки
**************** Повышать промо-корректировки, отражая pro-rate-sheet relay: God-eligible gate, organizationId серверная печать полезной нагрузки с помощью write(), downstream 4xx relayed verbatim. Пусть стюард повернется Пересмотр матрицы LLPA в реальные корректировки цен на каждый сценарий с консоли.
· VOIPService
Запись звонков, голосовая почта и доступ к транскриптам
Запись воспроизведения не оставила следов. Записанный звонок - самый плотный PHI платформенные магазины — неотредактированный разговор — и «кто слушал это» «Звонок» был безответным, что является первым вопросом о нарушении статьи 164.402. Оценка спрашивает. Проводите CallRecordingAuditor в каждое чтение, которое раскрывает его: - GET/recordings/{id}/audio CALL RECORDING/ DOWNLOAD - Получить **************** Call RECORDING / DOWNLOAD - GET/recordings/{id} CALL RECORDING + TRANSCRIPT/VIEW - Получить **************** Call RECORDING / DOWNLOAD - GET/instances/{id}/voicemails TRANSCRIPT/LIST, per row Расшифровка - это та, что скрывает. Это не медиа-загрузка; это езда внутри метаданных JSON и внутри каждой строки голосовой почты, и это одно и то же PHI как аудио в той форме, которая градирует. Это событие происходит потому, что Учет раскрытий собирается на один ресурс. Подача байта не закрывается, поэтому PhiAuditAvailabilityAdvice становится незарегистрируемым. читать в 503, а не в 500 — запрос был в порядке, мы декодируем Мы не можем отчитаться за PHI. Отказы также регистрируются; исследование Что ищет обнаружение. Также закрывает пробел в авторизации, обнаруженный при подключении: POST /recordings/lookup берется из органа запроса и никогда Проверил, что орг абонента владеет им, и решил проблему с поставщиком другого арендатора. Идентификаторы для их записи. Все остальные конечные точки в Сервис уже проходит через findByIdAndOrgId; этот сейчас тоже. Запись разрешения воспроизведения оставляется как есть и документируется на месте: Право собственности является единственным чеком, поэтому любой участник может воспроизвести любую запись в своей организации. Сужение означает решение, является ли «собственное расширение» граница, и какое право ее поднимает — это решение о продукте, а не решение о том, Угадайте, что делать здесь, и каждый доступ, по крайней мере, записывается сейчас.
· AIService
Record PHI access on every read of stored AI conversation history
Wires the shared §164.312(b) spine into the paths that read persisted prompts and completions. The assistant is the module the compliance assessment put OUTSIDE the boundary — prompts fan out to external providers under no BAA and are then kept indefinitely — so reads of that store are the disclosures most in need of a trail, and they had none. Audited: the session list and history search (one event per conversation, not one per request), a single conversation's metadata and its messages, the public widget reading its own transcript back, and the handoff bundle. Refusals are recorded too — probing another org's guids is what detection looks for — and an unauthenticated attempt is attributed to the sentinel org rather than dropped, since the recorder rejects what it cannot attribute and that is the event most worth keeping. The handoff bundle is a DISCLOSE, not a VIEW: it carries a visitor's conversation out of AIService and in front of a human in another product. That kind fails closed, so a lost record now withholds the transcript and answers 503 rather than serving data it cannot account for. Identifiers only. Never a message body, a title, or the search term — a member searching their assistant history types the borrower's name into it, and an audit trail that reproduced the prompt would just have moved the PHI into the log stream. Completes d428ee3, which a concurrent session swept up mid-change: the auditor was missing recordDeniedInternalView, and PublicAiChatController's new constructor argument had not reached its test.
· MediaService
Record chat message reads on the endpoints that return bodies
Sending a message left a row; reading one left nothing. Every chat, support and social thread on the platform renders from a handful of endpoints here, and none of them recorded anything, so "who read this conversation" had no answer — which is the first question a 164.402 breach assessment asks. ChatMessageAccessAuditor emits a PhiAccessEvent (CHAT_MESSAGE) from the five paths that hand a caller message text: GET **************** LIST GET /api/media/sessions/chat LIST POST **************** VIEW GET **************** LIST POST **************** VIEW One event per message, not one per request. "They opened the thread" is not an answer to "was this individual's message disclosed", and the message ids are already in hand at every call site, so a request-shaped row would have been a choice rather than a limitation. The thread read records AFTER the mute filter, so a row means a body that caller was actually handed. The chat list is in scope because each row carries up to 140 characters of the thread's last message — that is content, not metadata. Rows with an empty preview record nothing. Deliberately NOT recorded: unread counts, presence, last-seen, typing, read receipts, session-member lists, mutes, ticket detail (metadata plus status history, no bodies) and support reporting aggregates. A trail is only useful if a row means someone saw content; rows that stand for nothing bury the ones that do. Refusals are recorded too, against the thread guid — no message id exists yet at the point of refusal, and repeated refusals against thread guids is the shape probing has. An unauthenticated reader is attributed to a sentinel org rather than dropped: the recorder rejects unattributable events, and that is the single most interesting event this trail can hold. Client IP comes from X-Forwarded-For with remoteAddr as fallback — everything arrives via Traefik, so remoteAddr alone records the ingress pod. Public chat takes a second hop through APIService, which sets X-Forwarded-For to the visitor address and moves the browser's User-Agent to X-Original-User-Agent, so the auditor prefers that header; the literal User-Agent on that hop is APIService's own HTTP client and identifies nobody. Visitor reads are attributed to the org from X-Public-Chat-Org-Id with no member id: a visitor has no member identity of their own, but the org owns the records, so the read stays attributable. Identifiers only, never message text. An audit trail that reproduces the message it audits has just copied the PHI into the log stream. Also completes eed3817, which swept this work in mid-edit and left MediaController referencing PhiAccessKind before the import existed — that revision does not compile. This one does: mvn -o test is 16/16 green.
· DocsService
Запись байт-доступа на четырех путях документа, которые ничего не записывали
Загрузка уже зарегистрирована через *******************, и это было Единственная площадка для впрыска байтов. Конечные точки, обслуживающие одинаковые байты — /stream, /bulk-download, шаблон преобразованный-PDF и миниатюра страницы шаблона — ничего не записано, поэтому любой желающий байт документа Не оставляя следов, нужно было только выбрать другой URL. Все четверо теперь испускают PhiAccessEvent в точке доступа, которая была проверена. Байты вот-вот будут поданы. Большая загрузка излучает одно событие на документ, а не одно на запрос. "Они Экспортировано 400 документов" не является ответом на "была ли запись этого человека "раскрыт", и строки в документах - это то, что делает это ответственным шесть лет спустя. § 164.528. IP-адрес клиента исходит от X-Forwarded-For с удаленным Addr в качестве запасного варианта: каждый запрос Прибывает через Траефик, поэтому только удаленный Аддр будет записывать входной контейнер для Каждый доступ и быть бесполезным для обнаружения. Недостоверная попытка приписывается охранной организации, а не сбрасывается — диктофон отклоняет несоответствующие события, а запрос на документ байты без сеанса — это именно то событие, которое стоит сохранить. Это соответствует существующим строкам загрузки ImgLog, а не их замене: Это записи с изображением домена, это поверхностный след. Раскрытие информации собирается из. Примечание ImgLogService, который читает эти строки, по-прежнему Ноль абонентов в любом месте платформы.
· KlusterServices
Содержит выход из аватара; обратите внимание на бассейн выселения Redis
Служба безопасности теперь запускает фетчер, целевой URL которого выбран ненадежным. сторона — запись BIMI — это контент DNS, опубликованный тем, кто владеет отправляющим доменом; И каждый становится отправителем домена, отправив электронное письмо арендатору. Ничего в этом кластере ограничено, куда могла бы пойти такая просьба: ответы Редиса неаутентифицированы на 6379, MinIO на 9000 с корневыми учетными данными, CockroachDB на 26257 и метаданными узла Конечная точка доступна из любой точки. Решитель применяет свои собственные средства управления в коде, и они остаются основными. Защита, потому что они являются единственным слоем, который понимает перебиндинг DNS. Это Второй, поэтому баг там до сих пор не может добраться до частного адреса. Преднамеренно разрешить, а не разрешить список. Эти услуги законно доступны хостов, поэтому перечисление пунктов назначения будет неподъемным и сломается Тихо. Вместо этого он позволяет общедоступный интернет с каждым частным диапазоном, за исключением: Затем повторно разрешает конкретные порты в кластере, которые действительно необходимы. Скачал в стручки который выполняет ненадежные трюки - применение его в масштабах всего пространства имен будет отключено услуги, выход которых никогда не проверялся. Изменение Redis - это комментарий и удар по базам данных, а не изменение поведения. Это записывает, почему в кэше отправителя-аватара хранятся только короткие строки метаданных: maxmemory 2gb с орешками-lru выселяют по всему экземпляру, а DB 0 удерживает каждую живую сеанс, так что объемный байтовый кэш здесь будет подписывать пользователей наугад под давлением Неправильно диагностируется в течение нескольких дней.
· kamo-internal
Показать фотографии отправителя и получателя
Почтовый ящик отображал одну букву на одном общем градиенте для всех. Каждый ряд выглядел одинаково, а круг не содержал никакой информации. Теперь он показывает Фактическая фотография корреспондента, когда он известен, и двухбуквенная монограмма, когда он известен. Нет. Один SenderAvatar заменяет семь кругов инициалов. Это имело значение для больше, чем последовательность: каждый имел свое собственное представление о том, как разделить адрес. Кэш-память аватара заложена на этом разделении, поэтому любые разногласия между списком и Оборудование для чтения будет молча снижать скорость удара вдвое и показывать одному и тому же человеку два Различные способы на одном экране. ParseAddress — единственное место, где установлен RFC 5322. Адрес был разобран. Просмотры списка и потоков также прекращают печать необработанных заголовков Например, «Jane Doe <jane@acme.com>» и покажите имя. Идентичность живет в собственном магазине с ключом от адреса, а не на объектах сообщения. Оба списка просмотров опрос каждые 30 лет и на каждом уведомлении веб-сокета каждый раз. Замена их массива сообщений оптом — все, что сливается с сообщением, будет протирали два раза в минуту, поэтому пустой почтовый ящик мерцал фото до монограммы и обратно. навсегда. Ключ по адресу также означает, что ответ с опозданием не может отображаться на Неправильная строка, потому что строки не то, чем она закодирована. Запросы объединяются в одну партию на экран: пятьдесят строк, запрашивающих независимо, будут пятьдесят Запросы, и так как каждый звонок в эту службу сначала чеканит одноразовый ключ, На самом деле сто. Промах повторяется один раз после 45, потому что на стороне сервера Разрешение двухфазное, а на неизвестный адрес отвечают только по более позднему звонку. беседа ListView теперь виртуализируется. Он извлекает 50 строк страницы и перерисовывается Все они в AnimatePresence с индексом на каждом опросе; Добавление изображения к каждому сделало его самой тяжелой поверхностью в почтовом ящике. Отправители безответных и почтовых демонов получают нейтральную отметку, а не монограмму. Лицо рядом с уведомлением о отказе активно вводит в заблуждение. Фотографии декоративные (alt="") потому что имя всегда видимый текст рядом с ними, и нести referrerpolicy = "no-referrer", поэтому идентификатор потока никогда не достигает хоста изображения. Каждый аватар подается из нашего собственного происхождения - сторонние изображения извлекаются. Проверенная и повторно размещенная серверная сторона — вот что позволяет новый запрос Политика img-src привязана к собственным доменам арендатора. Эта политика построена на Прокси, а не следующий. конфигурация, поскольку эта платформа имеет белую маркировку: Статическая политика должна была бы назвать одного арендатора и разбить изображения для остальных. Он также закрывает уже существующий вектор отслеживания пикселей, начиная с Контакта. фотоурл URL-адрес свободного текста, отображаемый непосредственно в нескольких <img src>. Следующая/изображение больше не принимает удаленный SVG. Это был приемлемый риск, когда SVG были нашими собственными логотипами; не сейчас, когда платформа проглатывает знаки. Публикуется в сторонних DNS. Контакты абонентов сбрасывают жестко закодированный ORG ID = 1 и параметр запроса orgId, Соответствие изменения бэкэнда, который получает аренду от сессии.
· EmailService
Avatar endpoints; fix unauthenticated reachability and contact tenancy
Adds the mailbox side of sender avatars, and fixes three pre-existing holes this work would otherwise have built on top of. AUTHENTICATION. OTKPreAuthFilter used to reject a *bad* OTK but treat *no* OTK as anonymous-and-allowed, falling straight through to the filter chain — and the Spring Security chain here is anyRequest().permitAll(). Every mailbox, contact and calendar endpoint was therefore readable by anything that could reach the ClusterIP. Protected prefixes now fail closed. The fix lives in the filter rather than in the security chain deliberately: reshaping the chain would touch thirty unrelated surfaces at once, including two servlets and the DAV endpoints. The exempt list is the complete set of genuinely session-less entry points — the CAN-SPAM opt-out link, the OAuth redirect target where the tenant rides in the signed state parameter, and the server-to-server surfaces that authenticate with X-Internal-Auth. CONTACT TENANCY. Every contacts endpoint took orgId as a query parameter, or reached a book by id with no ownership check at all, so changing a number in the URL read another tenant's contacts — names, addresses, phone numbers, notes. The org now comes from the session and every book- or group-scoped operation proves the target belongs to it. Absent targets return 404 rather than 403 so an id in another tenant is not confirmed to exist. orgId is still accepted on the two list endpoints and ignored, so clients do not break mid-deploy. That change also fixes a silent bug: searchContacts derived orgId from bookId and passed null when no book was selected, into a query whose first predicate is contactBook.organization.id = :orgId. Book-less search therefore always returned nothing — which is exactly how the mail recipient autocomplete calls it. EMAILLOGOSERVICE. It fetched an operator-supplied URL with no scheme or address check, and transcoded the result on a Batik that resolves external references by default. A logo SVG containing <image **************** would have been dereferenced from inside the cluster. Now guarded on both ends. MESSAGEINDEXER. resolveMemberIdFromEmail pulled the entire members table into heap and filtered it in Java, once per indexed message, on a 1Gi pod — the code said so in a TODO. Same semantics, one indexed query. The avatar surface is one batched endpoint rather than a field on the message DTOs. Both list views poll every 30s and replace their message array wholesale, so an avatar carried on a message would be wiped twice a minute and the UI would flicker forever; and EmailMessage is shared by the folder list, reading pane, thread view and search, so changing its shape would touch all of them at once. The directory-photo endpoint is server-to-server only — exposed to a browser it would be a directory-enumeration oracle for the tenant.
· SecurityService
Sender-avatar resolution chain for the mailbox
Resolves a correspondent's email address to a picture, working outward from what Kamo already knows: member photo, then a photo someone attached to a contact, then the tenant's connected Microsoft/Google directory, then the sender domain's BIMI mark, then its favicon, then Gravatar. Anything unresolved renders a monogram, so the chain always terminates locally. The directory tier is worth calling out: the connect flows here already request admin.directory.user (Google) and User.ReadWrite.All + Directory.Read.All (Microsoft) alongside the mail scopes, so for Workspace and 365 tenants this returns authoritative photos with no re-consent and no third party involved. It should beat anything on the public internet and usually will. Two phases, never one. A request reads cache only; unknowns go to a bounded worker with DiscardPolicy — running an overflowed task on the caller would hand a request thread to a third-party host, and a discarded resolve costs nothing because the next poll asks again. A full-miss chain is DNS plus four HTTP calls, and a page carries dozens of addresses; nothing about opening a folder may depend on third-party availability. Decoupling also keeps resolution from correlating with a human opening a message, which would make it a read receipt — the exact behaviour the mailbox already blocks by default for remote images. This is the platform's first fetcher whose target URL is chosen by an untrusted party: a BIMI record is DNS content published by whoever owns a sending domain, and anyone becomes a sending domain by emailing a tenant. There is no egress NetworkPolicy and no forward proxy, so the controls are in code. SafeImageFetcher pins DNS — a custom resolver hands OkHttp only the addresses that passed PublicHostGuard, closing the rebinding window that defeats a naive pre-flight check — refuses redirects, caps bytes as they stream rather than trusting Content-Length, and requires an image content-type. SvgSanitizer parses and re-serializes against an allowlist instead of rasterizing. Batik 1.17 resolves external resources by default, so handing it a hostile SVG would dereference <image **************** from inside the cluster after the URL check already passed. The Tiny PS profile forbidding that is a statement about conforming files, not about bytes fetched from an attacker's URL. Provider miss semantics are all different and all cache-poisoning if treated as hits: Gravatar is asked with d=404 because every other default returns 200 with a placeholder; Google's no-match is a 404 carrying a byte-identical globe, so it is rejected by content hash; DuckDuckGo returns 404 with a real image body, so status decides. Bytes are re-hosted under a random name — never one derived from the address, which would let anyone probe whether a given person corresponds with this platform — and served through an authenticated route. Redis holds metadata only. That instance is 2GB with allkeys-lru on an emptyDir, sharing a keyspace with every live session; caching image bytes there would evict sessions and sign users out at random.
· kamo-shared-library
Sender identity model + org-scoped batch email lookups
Backing store and queries for the mailbox sender-avatar chain. SenderIdentityEntity is the durable tier of the avatar cache. org_id is part of the natural key rather than decoration: USERS.EMAIL is unique only per security provider, so an answer derived from tenant data must not be visible to another tenant. External-tier answers (BIMI/favicon/Gravatar) are genuinely global and share a sentinel org so every tenant shares the hit rate. Misses are stored as rows, not absences — without that, every unresolvable sender re-runs the whole provider chain on every inbox poll. OrgAvatarSettingsEntity splits the chain by what actually leaves the building. The identity tier defaults OFF because it discloses a digest of a correspondent's full address to a third party, which on a lending CRM is a decision about a borrower identifier rather than a display preference. The repository additions exist because none of this was reachable before: - ContactEmailRepository had no lookup by address at all — only substring LIKE for the autocomplete — so a contact that had a photo could not be resolved from a From: header. - Member/User batch lookups are org-scoped and take pre-lowercased input. LOWER() is applied to the stored column only; emails are persisted verbatim (EmbEmail has no normalizer) and CockroachDB equality is case-sensitive. - The avatar-id projections return ids rather than entities on purpose. Both Member.avatar and User.avatar are lazy associations onto a JOINED hierarchy: a caller outside a transaction gets LazyInitializationException, and one inside gets a base-type proxy that never narrows to AvatarPhoto, putting the file extension needed to build the URL out of reach. - **************** replaces a findAll() full-table scan that ran once per indexed message. PublicHostGuard is shared rather than duplicated because it is a security control: it decides whether an address is somewhere we will send a request that an untrusted party chose. Tested against the real bypass techniques — IPv4-mapped IPv6, 6to4, NAT64, CGNAT and link-local metadata. Right 177 gates the disclosure decision.
· kamo-shared-library
Audit spine for PHI access — §164.312(b)
The Security Rule requires recording access to ePHI, which means reads. The platform records changes well — LeadHistory, SystemAccessLog, UserHistory, LoanAuditEvent — and reads almost nowhere, which is why "who viewed this record" is currently unanswerable. That is also the first question a §164.402 breach assessment asks, so this one gap blocks both the audit control and the four-factor analysis. Capture layer only; no schema. The durable store sits behind PhiAccessLogWriter so call sites can be wired now and the sink can evolve — plain table, then append-only with a hash chain, then a signed export to object-locked storage — without rippling back through hundreds of call sites. Two properties carry the weight: - Failure handling belongs to the access kind, not the caller. EXPORT, DOWNLOAD and DISCLOSE fail closed: an unlogged bulk export is exactly the event you cannot reconstruct afterwards, and "we served the data but lost the record" is the worst answer to give an investigator. VIEW, LIST and SEARCH fail open and log loudly, because losing one line is not worth taking the product down. That asymmetry is deliberate and wants signing off rather than assuming. Callers under pressure resolve policy questions in whichever direction unblocks them, so the policy is not at the call site. - A denied read is still a read. Refused attempts are recorded, because the access pattern of someone probing records they cannot reach is what detection needs and is invisible if only successes are logged. Startup safety, which nearly went wrong: PhiAccessRecorder is a @Component and every service component-scans com.kamo, so a hard dependency on a writer bean would have failed context startup across ~40 services at once — a whole-platform outage from a feature nothing had called. The first fix, a @Configuration with @ConditionalOnMissingBean, does not work: component-scanned configuration is processed alongside user configuration rather than after it, so both writers register and injection fails on ambiguity. PhiAuditContextWiringTest caught that. The fallback now lives in the recorder via ObjectProvider.getIfUnique, where no ordering question exists, and ambiguous writers degrade to the default rather than failing startup. The default Slf4j writer is a starting point, not the destination — log lines are not tamper-evident and retention is 14 days against a six-year obligation. It exists so call-site wiring does not wait on the store. Identifiers are logged, content never is: an audit trail that reproduces the record it audits has just moved the PHI into the log stream.
· SecurityService
403 вместо 500, когда граница соответствия отказывается
Два сайта вызовов FeatureService.enableFeature теперь понимают границу HIPAA: - /включить карты PhiModuleNotPermittedException до 403 с указанием причины, Отражая обработку AppliedModelViolationException непосредственно над ней. Без Этот широкий улов превратил преднамеренный отказ от политики в 500. - Автоматическая петля в пути листинга предварительно проверяется с помощью isPhiPermitted и скипс. Он проходит через каждый завершенный ServiceType, просто перечисляя особенности организации, поэтому бросание на первый заблокированный модуль сделало бы Страница функций не загружается для арендатора PHI вместо того, чтобы скрывать одну запись. Инертный сегодня — каждая организация обрабатывает фи = ложно.
· kamo-shared-library
Enforce the compliance boundary at the feature switch
Wires PhiTenantGuard into FeatureService, the single choke point through which a module is switched on for an organization. This is where the boundary stops being a policy object and starts being enforcement. Both halves, because either alone leaves a gap: - enableFeature throws PhiModuleNotPermittedException, so a PHI tenant cannot switch on a module Kamo cannot stand behind as a business associate. - isFeatureEnabled(Organization, ServiceType) reports a module disabled whenever the tenant handles PHI and the module is outside the boundary, whatever the OrgFeature row says. The row can legitimately say active — it was enabled before the tenant became a PHI tenant, and enableFeature does not run again on that transition. isPhiPermitted is the non-throwing form for callers that iterate. FeatureController auto-enables every completed feature while *listing*, so a throw on the first blocked module would fail the whole features page rather than skip one entry. PhiServiceTypeMapping binds ServiceType to PhiModule and is total, with a test that fails the build on any unmapped value. That test earned its keep on first run: it caught ServiceType.STORAGE, which a grep of the enum had missed, and then AI_CHAT, DOC_MANAGER, DOCS_EDITOR and DOC_CLASSIFIER. Without it, five service types would have been silently outside the boundary — the same omission failure PhiModule's constructor closes, reintroduced one layer up. New modules for the previously unmapped service types, each with an explicit disposition: object storage, document editor and classifier, support tickets, HR, inventory, legal, calculator and activity feed are inside; clubhouse and games are BLOCKED_PENDING_WORK on the grounds that they have not been assessed, which is a reason to block rather than to assume. Every organization is handlesPhi=false today, so this changes no behaviour in production. It becomes load-bearing the moment the first tenant is flagged.
· kamo-shared-library
Автоназначение MERS-совместимого номера кредита при создании заявки
LoanFile.loanNumber был недействителен для создания (как для создания бланков, так и для продвижения лидов) И только когда стюард набрал один, который жестко блокировал MERS MIN чеканка (10-значный сегмент MIN - это номер кредита) и каждый нисходящий поток Идентификатор для безголового кредита. AppMortgageService теперь присваивает кредитный номер на обоих сайтах создания: чистый 10-значная цифра (MinMinter полоскает не цифры и нуждается в сегменте <=10 цифр, Таким образом, чистое число - единственная безопасная форма, уникальная для каждой организации через (org id, loan number) индекс с ограниченным повторением. Она не открыта для аннулирования полоса столкновения или недоступное репо — ухудшение до предшествующего руководства Поведение, а не нарушение создания приложений. предоставленный кредитором Номер все еще переопределяется через путь исправления кредитного файла. Никаких изменений схемы (The Schema Change) Колонка уже существует, индексируется.
· kamo-signer-monorepo
Даты подписывания в разрешенном часовом поясе получателя
SigningSession получает необходимую «зону времени» (ID INA), которую решает ESigService из зоны участника получателя, затем орга, затем UTC. Добро пожаловать на экран передает его явно форматировщику, поэтому дата истечения не зависит от Настройки устройства просмотра или по умолчанию UTC провайдера. Создание поля Требуемый, а не факультативный пойман на одной буквальной сессии строительной площадки (preview.ts) в момент составления. Также: - call setRequestLocale в локальной компоновке, поэтому переводы связываются с URL сегмент, а не полагаться только на заголовок промежуточного программного обеспечения Утверждать в Dockerfile, что сообщения / пережили «турбо-срубу», и документировать Почему .dockerignore должен держать .gitignore вне контекста сборки coupling prune отбрасывает переводы и изображения кораблей, визуализирующих необработанные ключи безошибочно, пока подписчик не откроет документ
· ESigService
Разрешите подписывающему отображать часовой пояс от участника, затем
Даты подписания были предоставлены в любой зоне, в которой клиент по умолчанию, поэтому конверт, срок действия которого истекает 2026-08-03T05:00Z, мог читать «3 августа» подписанту из Тихого океана Для кого ссылка уже мертва вечером 2 августа. SigningSession теперь имеет серверную IANA TimeZone: собственный член.timezone, когда они являются членом и установили один, в противном случае отправка Organization.timezone, далее UTC. SignerZone - это чистый растворитель с тестами. В том числе тот, который утверждает, что каждый TimezoneType отображает свою собственную зону. Номер не может поставлять клиентскую сторону RangeError. Также фиксируется SigningSession.locale, который испустил имя enum («SPANISH»). вместо кода BCP-47 («es»), используемого в локальном сегменте подписывающего URL.
· InitializerService
Создание HANDLES PHI и PHI EFFECTIVE DATE на оргах
Зеркала двух новых полей Организации, следующих за миграцией конвенции, поэтому колонки воспроизводимы в любой среде, а не только Существовал там, где проходил ddl-auto. HANDLES PHI не является полной ложной информацией, поэтому существующие арендаторы явно находятся за пределами отеля. Граница соответствия. PHI EFFECTIVE DATE – это TIMESTAMP, а не TIMESTAMPTZ. Потому что поле сущности является LocalDateTime. Тест представляет собой защиту от дрейфа, а не пересчет: он считывает названия колонок. сущность путем отражения и не справляется с построением, когда поле сущности не имеет ПОЛЬЗОВАНИЯ. Это Это фактический режим неудачи этого соглашения — кто-то добавляет поле, забывает миграция, и с ddl-auto: никто в услугах он всплыл как производство Ошибка запроса, а не в момент сборки. Проверено мутацией: удаление PHI EFFECTIVE DATE ALTER делает его несостоятельным, восстанавливая его. Зеленый. Обе колонки уже присутствуют в кластере; эти АЛТЕРЫ идемпотентны.
· kamo-shared-library
Persist handlesPhi и phiEffectiveDate на сайте
Назад PhiTenantGuard с реальной колонкой. HANDLES PHI не является полной ложью ******************************************************************************************************************************************************************* Он находится явно за пределами границы соответствия, а не молча внутри нее. PHI EFFECTIVE DATE охватывает оценку нарушения и обязательство по удержанию Правое окно — «с каких пор» — первый вопрос, заданный после «что было выставлено». Преобразование в политику с **************** Охранник сам остается свободным от сущности, поэтому абонент держит только кэшированный сессией флаг Не нужно загружать ряд. Безопасно толкать: оба столбца уже существуют в кластере и каждый из 12 Организации читают ложь. Проверено по информационной схеме перед посадкой — Рукоятки phi boolean НЕ НУЛЬНО DEFAULT ложные, phi effective date временная метка NULL. См. миграцию KamoInitializer для воспроизводимой формы.
· kamo-shared-library
PhiTenantGuard и граница соответствия модуля
Краеугольный камень границы соответствия HIPAA: какие модули могут достигать арендатора учитывая, обрабатывает ли этот арендатор защищенную информацию о здоровье. Все ключи вниз по течению от него — какие интеграции отключены, вынуждена ли MFA; какие сеансы получают более строгий тайм-аут, что должен покрывать ревизионный позвоночник. Не имеет схемной зависимости. Предыдущая попытка повесила это на новый Organization.handlesPhi столбец и пришлось вернуть: сервис CI клонов основной библиотекой во время сборки, в то время как KamoInitializer управляется вручную, поэтому столбец сущности на главном прерывает следующее развертывание службы, пока кто-то не мигрирует. Отделение политики от организации означает, что она приземляется и может быть исполнена сейчас. Колонка становится самостоятельным шагом. Три свойства, вокруг которых построен дизайн: - Неудачи закрыты. Конструктор PhiModule требует PhiDisposition и причины. Таким образом, модуль, добавленный позже, не может наследовать разрешение по упущению - он не будет Составляйте без решения, а все, что не подтверждено, отрицает. Это Режим неисправности границы соответствия должен выжить: модуль корабля, никто Подумайте о PHI, клиника начинает его использовать. Неурегулированный арендатор не является безопасным арендатором. Охранник берет три государства ФитенантСтатус, а не булеан, поэтому «не смог загрузить организацию» Не может быть записано так же, как «эта организация не является PHI». С ним Путь ошибки - это разрешенный (ложный, ...) - легко добраться, Невозможно заметить в обзоре. Неизвестный отрицает все. - Нечего поднимать. Решение является чистой функцией (статус, модуль) без Абонент, сеанс, правый или богорежимный параметр. "Наш вспомогательный персонал может повернуть его "выключено" - это ответ, который не дает обзора охватываемой организации. Диспозиции следуют скопинг-анализу в камо-внутреннем **************************** Разрешено: CRM, контакты, хранилище документов, саморазмещенная электронная почта, электронная подпись, заметки, задачи, календарь, внутренний чат, в кластере Стек транскрипции/перевода, MLOS. Заблокированный без доступа к BAA: социальный Обмен сообщениями, Canva, синхронизация электронной коммерции, шлюз MCP. Заблокированное ожидание названной работы: роутер AI, поиск RAG, записи встреч, записи VOIP, маркетинговые кампании, Аналитический экспорт. Пока ничего не вызывает охрану, так что это инертно, пока не подключено.
· InitializerService
Создание HANDLES PHI и PHI EFFECTIVE DATE на оргах
Зеркала двух новых полей на объекте Организации в библиотеке, разделенной Камо. В соответствии с Конвенцией OrgBrandingFieldsMigration. HANDLES PHI не является полноценным Неправильная ложь, поэтому каждый существующий арендатор явно выходит за рамки соответствия. Вместо того, чтобы молчать внутри него. PHI EFFECTIVE DATE - это TIMESTAMP. TIMESTAMPTZ, потому что поле сущности является LocalDateTime. SEQUENCING — это должно работать до развертывания следующей службы. Услуги PIN kamo-shared-библиотека в фиксированной версии, которая переиздается на месте, поэтому первый сервис для восстановления после этого изменения библиотеки начинает выбирать эти столбцы; ddl-auto: нет в сервисах, которые терпят неудачу, пока этот бегун не выполнит. Сопровождающее испытание представляет собой дрифтерную защиту, а не повторение: оно гласит колонку. именует объект путем отражения и не выполняет сборку, если поле объекта не имеет АЛТЕР. Это режим неудачи, который есть во всей конвенции — кто-то добавляет поле, Забывает миграцию, и она появляется как ошибка производственного запроса, а не на Построить время. Проверено мутацией: удаление PHI EFFECTIVE DATE ALTER делает это Потерпите неудачу с действенным сообщением, восстановив его.
· ESigService
Выйти из сегмента Signer Locale в подписывающих ссылках
Извлекает байт за байтом дублированную сборкуSignLink в SignLinkBuilder и добавляет локальный сегмент, полученный из Organization.language, падение Возвращаясь к английскому языку для четырех языковых значений без перевода. Требуется, чтобы сначала была развернута маршрутизация /{locale}/.
· kamo-shared-library
Добавить ручки Phi флаг арендатора и PhiTenantGuard
Краеугольный камень границы соответствия HIPAA: флаг организации плюс Охраняйте решение о том, к каким модулям может прийти организация. Все ниже по течению ключи от него — какая интеграция отключена, вынуждена ли MFA сеансы получают более строгий тайм-аут, который должен покрывать ревизионный позвоночник. Организация получает HANDLES PHI (BOOL DEFAULT FALSE) и PHI EFFECTIVE DATE. Следуйте существующей схеме *******************. ложные пороки, Таким образом, это инертно для существующей ипотечной и CRM-книги. Три свойства, вокруг которых построен дизайн: - Неудачи закрыты. Конструктор PhiModule требует PhiDisposition и причины. Таким образом, модуль, добавленный позже, не может наследовать разрешение по упущению - он не будет Составляйте без решения, а все, что не подтверждено, отрицает. Это Конкретный режим неисправности должен выживать граница соответствия: модуль корабля, Никто не думает о ПИ, клиника начинает его использовать. - Невозможно обойти. Решение является чистой функцией (организации, модуля) Нет абонента, сеанса, правильного или божественного параметра, поэтому нет ничего, что могло бы помочь. Поднять. «Наш вспомогательный персонал может выключить его» - это ответ, который не дает покрытия. Обзор предприятия. Отрицания несут причину, потому что голый «отрицаемый» не может быть использован поддержкой. Инженер или клиент. Диспозиции следуют скопинг-анализу в камо-внутреннем **************************** Разрешено сегодня: CRM, контакты, документ хранилище, электронная почта, электронная подпись, заметки, задачи, календарь, внутренний чат, Стек транскрипции/перевода и MLOS. Блокировка без возможности получения BAA: социальные сообщения, Canva, синхронизация электронной коммерции, шлюз MCP. Заблокированный ожидающий имени работа: роутер AI, поиск RAG, записи встреч, записи VOIP, маркетинг кампании, экспорт аналитики. Примечание к схеме: HANDLES PHI и PHI EFFECTIVE DATE материализуются на следующем KamoInitializer работает после того, как CI публикует эту сборку совместной библиотеки. Ничего не читается Флаг, так что заказ в безопасности.
· kamo-signer-monorepo
Локальное форматирование даты и метаданные на локальную

Ваша вселенная ждет.
Начните бесплатно через несколько минут. Добавляйте продукты, когда будете готовы — без миграции, без новых логинов.
Бесплатно навсегда — не требуется кредитная карта