Documentation
¶
Overview ¶
Package clients — peer-gRPC clients for kaname.
kaname is the leaf-owner of Account/Project/User and does not initiate peer-domain calls itself. The clients here are dependencies on adjacent systems (OIDC providers, HSM/PKCS#11, S3 report-store, etc.) — see individual files for each client's scope. A client that pushed cache invalidation to the edge used to be listed here; that edge was retired (the edge now reads the subject-change journal itself), so nothing dials it any more. A client to an external relations engine used to stand here too; the verdict is now computed in this service's own database, so there is nothing to dial.
Peer-client template — `internal/clients/builder.go`-style: TTL+LRU cache + retries/dialTimeout/keepalive + TLS + optional dns:///+round_robin.
invite_mail.go — НАШ отправитель писем: приглашение, восстановление доступа и подтверждение адреса.
Почему отправитель здесь, а не у поставщика личности ¶
Писем в продукте три вида. Приглашение отправляем МЫ (приёмка ID-MAIL-1, Р23): его предмет — наша строка в нашей базе, и о ней поставщик не знает ничего. Восстановление доступа с фазы Ф5 (`kacho#1271`, приёмка `recovery-of-access.md`, Р3, Д5) — тоже наше: код чеканим мы, поток поставщика истёк вместе с поставщиком, и у письма не осталось бы ни одного отправителя. Подтверждение адреса — тоже наше (kaname#456, приёмка `access-beyond-login-needs-a-verified-address.md`, Р8): код чеканим мы, и без подтверждения человеку не открывается ничего дальше экрана подтверждения.
Второй вид живёт в ТОЙ ЖЕ полосе, а не во второй (Р3): отправитель, настройка, закрытый набор клеток исхода, ограниченный повтор и предел времени на попытку — общие; различается только тело письма, выбираемое по виду события. Две стороны словаря видов — ограничение схемы и ветви применителя — сверяет гейт `TestEveryMailKindHasExactlyOneSender`. Имя полосы, очереди и клеток осталось от приглашения: переименование не меняет ни одного предмета, а разошлось бы с историей.
Что здесь лежит — три части одной цепочки ¶
- InviteMailSender — транспорт: один разговор с почтовым узлом, СВОИМ пределом времени ограниченный; тело письма выбирает по виду события;
- DecodeMailEvent — Decoder[T] для общего дренажа;
- NewInviteMailApplier — Applier[T]: ветвится на ВИДЕ события, зовёт транспорт и раскладывает исход по ЗАКРЫТОМУ набору клеток счётчика.
Две величины, и они РАЗНЫЕ ¶
Предел времени на ПОПЫТКУ (`MailRelay.AttemptTimeout`) и число ПОВТОРОВ (`drainer.Config.MaxAttempts`, композиционный корень) — разные величины с разными предметами. «Ограниченный повтор» без первой ограничивает ЧИСЛО попыток, каждая из которых вправе висеть вечно, — а это §«Per-call deadline на КАЖДОМ внешнем вызове» в чистом виде. Наблюдаема первая только на узле, который ПРИНИМАЕТ соединение и молчит: на отказе в соединении обрыв даёт ядро, а не наша величина.
Настройка отделена от сбоя — собственной клеткой ¶
Недоступность узла лечится временем; ответ не по протоколу почты по объявленному адресу не лечится никогда. Схлопнуть их в один ряд значит сделать постоянную неверную настройку штатным режимом — §Hardening п. 8. Поэтому клеток три, набор ЗАКРЫТ и приходит из констант, а не из ответа узла: иначе кардинальность росла бы с трафиком.
provider_compensation_outbox.go — ОЧЕРЕДЬ компенсаций саги «зарегистрировать клиента у внешнего поставщика → закоммитить свою строку», которой больше некого компенсировать.
Писатель намерения, дренаж и его исполнитель сняты вместе с административной дорогой к поставщику (kaname#363): у службы одна посадка, клиента у поставщика она не заводит ни на одном пути, и намерению «снять его там» неоткуда взяться. Таблица же в схеме осталась — применённые миграции не правятся, — и строки, записанные прежней посадкой до её снятия, в ней могли остаться. Поэтому остаются ДВЕ вещи, которые смотрят на таблицу, а не пишут в неё: перепись очереди (глубина, возраст, отравленные — `runProviderCompensationMetrics`) и уборщик доставленных строк (`kanamepg.ProviderCompensationSweeper`). Без них остаток стал бы невидимым. Снятие самой таблицы — отдельный предмет миграции, а не этого файла.
Index ¶
- Constants
- Variables
- func ClassifyInviteMailOutcome(err error) string
- func NewInviteMailApplier(transport InviteMailTransport, obs InviteMailObserver, logger *slog.Logger) drainer.Applier[MailEvent]
- func RenderEmailChangeMail(relay MailRelay, ev MailEvent) []byte
- func RenderEmailChangedMail(relay MailRelay, ev MailEvent) []byte
- func RenderInviteMail(relay MailRelay, ev MailEvent) []byte
- func RenderMail(relay MailRelay, ev MailEvent) []byte
- func RenderRecoveryMail(relay MailRelay, ev MailEvent) []byte
- func RenderVerificationMail(relay MailRelay, ev MailEvent) []byte
- type ConditionalTuple
- type InviteMailObserver
- type InviteMailSender
- type InviteMailTransport
- type MailEvent
- type MailRelay
- type MailTLSMode
- type RelationQueries
- type RelationStore
- type RelationTuple
- type TupleConditionRef
Constants ¶
const ( // InviteMailTable — полное имя очереди писем приглашения. ШЕСТАЯ очередь // сервиса; форма не изобретается — она в дереве пятикратна. InviteMailTable = "kaname.invite_mail_outbox" // InviteMailChannel — LISTEN-канал (триггер миграции). InviteMailChannel = "kaname_invite_mail_outbox" // EventInviteMailSend — вид события приглашения. Словарь закрыт CHECK'ом // миграции: расширение требует и кода, и миграции. EventInviteMailSend = "mail.invite.send" // EventRecoveryMailSend — вид события письма восстановления доступа (Ф5 Р3); // заведён миграцией `20260917015400_recovery_code_is_our_record`. EventRecoveryMailSend = "mail.recovery.send" // EventVerificationMailSend — вид события письма подтверждения адреса // (kaname#456, Р8); заведён миграцией // `20260927190000_address_verification_is_our_verb`. EventVerificationMailSend = "mail.verification.send" // EventEmailChangeMailSend — вид события письма с кодом смены адреса на // НОВЫЙ адрес (kaname#635, Р7); заведён миграцией // `20261007150000_email_change_is_confirmed_from_the_new_address`. EventEmailChangeMailSend = "mail.email-change.send" // EventEmailChangedMailSend — вид события уведомления о смене адреса на // ПРЕЖНИЙ адрес (kaname#635, Р7); заведён той же миграцией. EventEmailChangedMailSend = "mail.email-changed.send" )
const ( // InviteMailOutcomeSent — письмо СДАНО почтовому узлу. // // КЛЕТКА НАЗЫВАЕТСЯ «sent», А НЕ «delivered», И ЭТО НЕ ПРИДИРКА К СЛОВУ. // Дальше ретранслятора наш вердикт не идёт (Р15): продукт видит сдачу, а не // получение адресатом. Ряд с именем «delivered» читался бы дежурным в три // часа ночи как «письма доходят» — то есть утверждал бы ровно то, чего // продукт не знает, и делал бы это на поверхности, где комментария нет. // Имя ряда и есть утверждение; комментарий, поясняющий, что имя означает не // то, что говорит, — второе место об одном предмете, и верным было бы одно. InviteMailOutcomeSent = "sent" // InviteMailOutcomeTransient — узел не принял письмо по причине, которая // лечится временем: не поднят, не ответил, ответил временным отказом. InviteMailOutcomeTransient = "transient" // InviteMailOutcomeMisconfigured — по объявленному адресу не почтовый узел, // величина не задана либо задана вырожденно, удостоверение отвергнуто. // Временем НЕ лечится. InviteMailOutcomeMisconfigured = "misconfigured" )
Клетки счётчика исходов отправки. Набор ЗАКРЫТ (Р25): форма взята у зеркала набора ключей вместе с обоснованием — успехи считаются НАРАВНЕ с отказами, иначе ноль отказов неотличим от «сюда никто не приходил», а настройка стоит СВОЕЙ клеткой, потому что временем не лечится.
const ProviderCompensationTable = "kaname.provider_compensation_outbox"
ProviderCompensationTable — полное имя очереди компенсаций.
const SettingsScreenPath = "/settings"
SettingsScreenPath — путь экрана параметров учётной записи в консоли: письмо с кодом смены адреса называет его (kaname#635, Р7). Адрес экрана — происхождение консоли, объявленное той же настройкой, что адрес входа, и этот путь, без параметров и фрагмента.
const VerificationScreenPath = "/verification"
VerificationScreenPath — путь экрана подтверждения адреса в консоли. Адрес экрана — происхождение консоли, объявленное той же настройкой, что адрес входа в письмах службы, и этот путь, без параметров и фрагмента.
Variables ¶
var ( // ErrMailMisconfigured — отказ, который повтор не вылечит. ErrMailMisconfigured = errors.New("invite mail: relay is misconfigured") // ErrMailTransient — отказ, который лечится временем. ErrMailTransient = errors.New("invite mail: relay is temporarily unavailable") )
Сигнальные ошибки классификации. КАЖДАЯ ветка возврата транспорта заворачивает свой отказ ровно в одну из них — корзины «прочее» у отправителя нет by construction.
var InviteMailOutcomes = []string{ InviteMailOutcomeSent, InviteMailOutcomeTransient, InviteMailOutcomeMisconfigured, }
InviteMailOutcomes — закрытый набор клеток семейства.
var MailEventTypes = []string{ EventInviteMailSend, EventRecoveryMailSend, EventVerificationMailSend, EventEmailChangeMailSend, EventEmailChangedMailSend, }
MailEventTypes — ЗАКРЫТЫЙ перечень видов события очереди писем, по одному на ветвь применителя. Им засевается метка `kind` ряда исходов по виду письма (kaname#733): неудача письма восстановления видна своей клеткой, а не растворяется в общем счёте полосы. Сверку с ветвями применителя и словарём схемы держит `TestEveryMailKindHasExactlyOneSender`.
Functions ¶
func ClassifyInviteMailOutcome ¶
ClassifyInviteMailOutcome раскладывает исход одной попытки по ЗАКРЫТОМУ набору клеток.
nil — сдано. Прочее приходит завёрнутым в одну из двух сигнальных ошибок: каждая ветка возврата транспорта заворачивает свой отказ явно, поэтому неклассифицированного отказа НАШ отправитель не производит. Неназванный отказ (он мог бы прийти от чужого транспорта в пробе) считается ВРЕМЕННЫМ осознанно: повтор безопаснее отравления.
func NewInviteMailApplier ¶
func NewInviteMailApplier( transport InviteMailTransport, obs InviteMailObserver, logger *slog.Logger, ) drainer.Applier[MailEvent]
NewInviteMailApplier — drainer.Applier: сдаёт письмо узлу и раскладывает исход по закрытому набору клеток.
ОТКАЗ ПО НАСТРОЙКЕ ВОЗВРАЩАЕТСЯ ПОСТОЯННЫМ (MAIL-33: «без бесконечных повторов»): строка отравляется, остаётся в очереди видимой и повторно исполнимой, когда настройку починят, — а не крутится вечно, изображая работу. Временный отказ ретраится: он лечится временем.
Ни один исход не остаётся тихим: клетка ставится на КАЖДОЙ ветке возврата, и отказ по настройке пишется в журнал уровнем ошибки, а не предупреждения (§Hardening п. 8: настройка — громко, никогда тихим Warn).
func RenderEmailChangeMail ¶ added in v0.5.0
RenderEmailChangeMail собирает тело письма с кодом смены адреса на НОВЫЙ адрес (kaname#635, Р7).
Письмо несёт КОД, его срок в минутах и адрес экрана параметров — и ничего сверх. Кода в адресе нет: код вводится руками на экране, где смена начата.
Штамп — как у RenderMail; адрес отправителя без домена — nil.
func RenderEmailChangedMail ¶ added in v0.5.0
RenderEmailChangedMail собирает тело уведомления о смене адреса на ПРЕЖНИЙ адрес (kaname#635, Р7).
Уведомление несёт момент смены и строку «если это были не вы — обратитесь к администратору аккаунта» — и ничего сверх: нового адреса нет (прежний ящик мог оказаться в чужих руках), кода и ссылки нет.
Штамп — как у RenderMail; адрес отправителя без домена — nil.
func RenderInviteMail ¶
RenderInviteMail собирает тело письма.
Письмо говорит об ОТПРАВКЕ и НИГДЕ не говорит «доставлено»: продукт видит сдачу ретранслятору, а не получение адресатом, и утверждать второе значило бы обещать то, чего он не знает (Р15).
Предъявителя письмо НЕ несёт (Р24): в нём призыв и адрес страницы входа, а доступ даёт владение почтовым ящиком, доказанное подтверждением адреса.
Штамп — как у RenderMail; адрес отправителя без домена — nil.
func RenderMail ¶ added in v0.5.0
RenderMail — письмо целиком по виду события, со штампом отправки, который поставил бы отправитель с часами по умолчанию (NewInviteMailSender). Вид неизвестный применитель до транспорта не доводит (постоянный отказ), поэтому здесь исходов два.
Адрес отправителя без домена — nil: письма без штампа не бывает, и отдавать его нельзя даже для осмотра. Отправка судит тот же отказ сама (Send).
func RenderRecoveryMail ¶ added in v0.5.0
RenderRecoveryMail собирает тело письма восстановления доступа (Ф5 Р3).
Письмо несёт КОД и его срок — и ничего сверх: ни ссылки-предъявителя (полоса кодовая, Ф5 §1.2), ни утверждения «доставлено» (Р15 ID-MAIL-1 — продукт видит сдачу узлу, а не получение). Адрес консоли, если объявлен, стоит отдельной строкой и кода не несёт.
Штамп — как у RenderMail; адрес отправителя без домена — nil.
func RenderVerificationMail ¶ added in v0.5.0
RenderVerificationMail собирает тело письма подтверждения адреса (kaname#456, Р8).
Письмо несёт КОД, его срок в минутах и адрес экрана подтверждения — и ничего сверх. Кода в адресе нет: адрес ведёт на экран, код вводится руками, — так устроены и письма восстановления и приглашения, и письмо, действующее одним нажатием, приучало бы нажимать на ссылки о своей учётной записи.
Штамп — как у RenderMail; адрес отправителя без домена — nil.
Types ¶
type ConditionalTuple ¶
type ConditionalTuple = authztypes.ConditionalTuple
ConditionalTuple — псевдоним authztypes.ConditionalTuple.
type InviteMailObserver ¶
type InviteMailObserver interface {
// IncInviteMailOutcome — исход одной попытки отправки; kind — вид события
// из MailEventTypes, outcome — клетка из InviteMailOutcomes.
IncInviteMailOutcome(kind, outcome string)
}
InviteMailObserver — писатель счётчика исходов отправки.
Нужен, чтобы «ноль писем за всю жизнь очереди» было ЗАМЕТНО: без него мёртвый отправитель и здоровое облако, куда никто не приходил, выглядят одинаково — тихо (класс мёртвой очереди регистраций).
type InviteMailSender ¶
type InviteMailSender struct {
// contains filtered or unexported fields
}
InviteMailSender — транспорт поверх SMTP.
func NewInviteMailSender ¶
func NewInviteMailSender(relay MailRelay) *InviteMailSender
NewInviteMailSender конструирует транспорт. Величины НЕ проверяются здесь: вырожденная настройка обязана дать наблюдаемый исход `misconfigured` на попытке, а не тихий отказ конструирования, который никто не считает.
Часы по умолчанию — `time.Now`, тем же правилом, что у остальных обработчиков службы (`Deps.Now == nil → time.Now`); подменяются WithClock.
func (*InviteMailSender) Send ¶
func (s *InviteMailSender) Send(ctx context.Context, ev MailEvent) error
Send — один разговор с почтовым узлом, ограниченный СВОИМ пределом времени.
Предел ставится дважды и намеренно: контекстом (он обрывает установление соединения) и абсолютным сроком на самом соединении (он обрывает узел, который СОЕДИНЕНИЕ ПРИНЯЛ и молчит). Одного контекста мало: после того как соединение установлено, чтения и записи по нему контекст уже не сторожит.
func (*InviteMailSender) WithClock ¶ added in v0.5.0
func (s *InviteMailSender) WithClock(now func() time.Time) *InviteMailSender
WithClock ставит часы отправителя — ими датируется каждое письмо. nil оставляет часы по умолчанию: «часов нет» у отправителя непредставимо.
type InviteMailTransport ¶
InviteMailTransport — порт: то, что умеет сдать письмо почтовому узлу. Реализуется InviteMailSender; в пробах — подставным транспортом, который снисходительнее настоящего быть не вправе. Вид письма приходит в `ev.Kind`.
type MailEvent ¶ added in v0.5.0
type MailEvent struct {
// To — адрес получателя. Единственная координата, без которой письмо
// отправить некому.
To string `json:"to"`
// AccountID — аккаунт. Атрибуция и тело письма приглашения.
AccountID string `json:"account_id"`
// UserID — строка человека. Атрибуция; отправка от неё не зависит.
UserID string `json:"user_id"`
// LoginURL — адрес страницы входа. Пусто → берётся из настройки установки.
LoginURL string `json:"login_url,omitempty"`
// Code — код в форме для человека; у видов восстановления и подтверждения.
// Строка этих видов без кода нерастолковываема.
Code string `json:"code,omitempty"`
// CodeValidMinutes — срок кода в минутах, как его называет письмо (Ф1-25:
// «код с объявленным сроком»). У видов восстановления и подтверждения.
CodeValidMinutes int `json:"code_valid_minutes,omitempty"`
// ChangedAt — момент смены адреса (RFC 3339, до секунды); у уведомления о
// смене адреса. Нового адреса уведомление не несёт.
ChangedAt string `json:"changed_at,omitempty"`
// Kind — вид события строки; проставляется применителем, в нагрузке не
// хранится.
Kind string `json:"-"`
}
MailEvent — расшифрованная нагрузка одной строки очереди: письмо любого из двух видов. Вид в нагрузке НЕ хранится — он в колонке события; применитель проставляет его в `Kind` перед сдачей транспорту.
Приглашение предъявителя НЕ несёт (Р24): письмо несёт призыв и адрес страницы входа, а доступ даёт владение почтовым ящиком. Письмо восстановления предъявителя НЕСЁТ — код — потому что предъявитель и есть его предмет (Ф5 Р1); в строке очереди он лежит открытым до сдачи письма узлу, и сданную строку снимает уборка.
func DecodeMailEvent ¶ added in v0.5.0
DecodeMailEvent — drainer.Decoder, общий для обоих видов.
Строка, не назвавшая адресата, нерастолковываема и повтором таковой не станет — это ПОСТОЯННЫЙ отказ, а не вечный ретрай. Условие закрыто и ограничением миграции, поэтому записать такую строку НЕЛЬЗЯ; проверка остаётся вторым рубежом, а не единственным. Требования ВИДА (код у письма восстановления) судит применитель: декодер вида не знает.
type MailRelay ¶
type MailRelay struct {
// Addr — `host:port` почтового узла.
Addr string
// From — адрес отправителя, ОДИН на установку (Р16).
From string
// FromName — отображаемое имя отправителя; необязательно.
FromName string
// Username/Password — удостоверение. Приезжает из СЕКРЕТА, не из карты
// настроек (Р6). ПАРА: половина настройки хуже отсутствия обеих, потому что
// выглядит настроенной (Р4).
Username string
Password string
// AttemptTimeout — СОБСТВЕННЫЙ предел времени на ОДНУ попытку: весь разговор
// с узлом, от соединения до принятого письма. Отдельная величина от числа
// повторов, и наблюдаема она на узле, который принимает соединение и молчит.
AttemptTimeout time.Duration
// TLSMode — посадка полосы. Р5: шифрование обязательно и на стенде тоже.
TLSMode MailTLSMode
// RootCAs — якорь доверия для проверки сертификата узла. nil означает
// системный набор, а НЕ отключённую проверку: проверка не отключается ничем.
RootCAs *x509.CertPool
// ServerName — имя, по которому сверяется сертификат узла. Пусто → берётся
// из Addr.
ServerName string
// LoginURL — адрес страницы входа, который несёт письмо. Ссылки-предъявителя
// приглашение НЕ несёт (Р24): обладание письмом доступа не даёт.
LoginURL string
}
MailRelay — величины НАШЕГО исходящего соединения к почтовому узлу.
Встроенных умолчаний у узла, отправителя и удостоверения НЕТ (Р3): пустое значение означает «не задано» и даёт отказ по настройке, а не «разумное значение». Величина, которую построение подставляет молча, предметом стража быть не может — он зелен при любом входе.
type MailTLSMode ¶
type MailTLSMode int
MailTLSMode — посадка полосы до почтового узла.
const ( // MailTLSStartTLS — открытая полоса, поднимаемая до шифрованной командой // STARTTLS, с проверкой сертификата по объявленному якорю. Посадка по // умолчанию: Р5 требует шифрования И на стенде тоже. MailTLSStartTLS MailTLSMode = iota // MailTLSImplicit — шифрование с первого байта (submissions, 465). MailTLSImplicit // MailTLSDisabledForTest — НЕЗАЩИЩЁННАЯ полоса, допустимая ТОЛЬКО в // in-process фикстурах (ban #16: dev-insecure posture запрещена на любом // поднятом стенде). // // РАЗБОР КОНФИГУРАЦИИ ЭТО ЗНАЧЕНИЕ НЕ ПРОИЗВОДИТ НИ ПРИ КАКОМ ВХОДЕ, и это // не соглашение, а построение: `ParseMailTLSMode` принимает два имени и // отвергает всё прочее, поэтому оператор не может выбрать незащищённую // полосу, как бы он ни написал значение. Свойство закреплено пробой // `Test_ParseMailTLSMode_NeverYieldsThePlaintextMode`. MailTLSDisabledForTest )
func ParseMailTLSMode ¶
func ParseMailTLSMode(s string) (MailTLSMode, error)
ParseMailTLSMode переводит объявление профиля в посадку полосы.
Принимаются РОВНО два имени. Незащищённой полосы среди них нет: значение, которого разбор не производит, оператор выбрать не может (ban #16).
type RelationQueries ¶
type RelationQueries interface {
// CheckWithContext — вердикт с условным контекстом запроса.
CheckWithContext(ctx context.Context, subject, relation, object string, condCtx map[string]any) (allowed bool, err error)
// BatchCheckWithContext — ОДИН вопрос о МНОГИХ объектах одного типа; по
// вердикту на объект, позиционно.
//
// Объявлен здесь, а не отдельной способностью, НАМЕРЕННО: этот порт — тип
// поля в каждом читающем use-case, а фильтр страницы (`internal/authzfilter`)
// выбирает батчевый путь. Провязка, потерявшая способность, продолжала бы
// возвращать верные строки, платя по обращению за строку, и заметить это было
// бы нечем. Здесь потеря — ошибка компиляции.
//
// Стоимость страницы принадлежит ЗАПРОСУ: ответ собирается одной читающей
// транзакцией, поэтому все объекты страницы видят один снимок базы.
BatchCheckWithContext(ctx context.Context, subject, relation string, objects []string,
condCtx map[string]any) (allowed []bool, err error)
// ListSubjects — кто держит это отношение на объекте, СТРАНИЦЕЙ С КУРСОРОМ.
//
// Курсор обязателен и это не оформление: перечисление без продолжения
// оставляет остаток недостижимым при живых правах — ровно то, ради чего снято
// перечисление объектов (решение Р1 приёмки R7-3).
ListSubjects(ctx context.Context, objectType, objectID, relation string,
pageSize int, pageToken string) (subjects []string, nextPageToken string, err error)
}
RelationQueries — вопрос С условным контекстом и страничные формы.
type RelationStore ¶
type RelationStore interface {
// Check — держит ли субъект это отношение на этом объекте.
//
// Ошибка означает «ответа нет», а не «доступа нет»: вызывающий обязан
// различать их, иначе недоступность базы читалась бы как законный отказ.
Check(ctx context.Context, subject, relation, object string) (allowed bool, err error)
}
RelationStore — вопрос о доступе БЕЗ условного контекста.
type RelationTuple ¶
RelationTuple — тройка «субъект, отношение, объект».
ОСТАЁТСЯ ПОСЛЕ СНЯТИЯ ДВИЖКА: это форма полезной нагрузки СТРОКИ ЖУРНАЛА намерений (`kaname.fga_outbox`), из которой триггер складывает прямой факт. Журнал и триггер — решение Р7 приёмки R7-3: снимается потребитель журнала, а не журнал.
type TupleConditionRef ¶
type TupleConditionRef = authztypes.TupleConditionRef
TupleConditionRef — псевдоним authztypes.TupleConditionRef.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package breachcheck — адаптер авторитета утечек паролей по протоколу k-анонимности (фаза Ф3, задача PRO-Robotech/kacho#1269; Р11, Ф3-34).
|
Package breachcheck — адаптер авторитета утечек паролей по протоколу k-анонимности (фаза Ф3, задача PRO-Robotech/kacho#1269; Р11, Ф3-34). |