clients

package
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: AGPL-3.0 Imports: 18 Imported by: 0

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

View Source
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"
)
View Source
const (
	// InviteMailOutcomeSent — письмо СДАНО почтовому узлу.
	//
	// КЛЕТКА НАЗЫВАЕТСЯ «sent», А НЕ «delivered», И ЭТО НЕ ПРИДИРКА К СЛОВУ.
	// Дальше ретранслятора наш вердикт не идёт (Р15): продукт видит сдачу, а не
	// получение адресатом. Ряд с именем «delivered» читался бы дежурным в три
	// часа ночи как «письма доходят» — то есть утверждал бы ровно то, чего
	// продукт не знает, и делал бы это на поверхности, где комментария нет.
	// Имя ряда и есть утверждение; комментарий, поясняющий, что имя означает не
	// то, что говорит, — второе место об одном предмете, и верным было бы одно.
	InviteMailOutcomeSent = "sent"
	// InviteMailOutcomeTransient — узел не принял письмо по причине, которая
	// лечится временем: не поднят, не ответил, ответил временным отказом.
	InviteMailOutcomeTransient = "transient"
	// InviteMailOutcomeMisconfigured — по объявленному адресу не почтовый узел,
	// величина не задана либо задана вырожденно, удостоверение отвергнуто.
	// Временем НЕ лечится.
	InviteMailOutcomeMisconfigured = "misconfigured"
)

Клетки счётчика исходов отправки. Набор ЗАКРЫТ (Р25): форма взята у зеркала набора ключей вместе с обоснованием — успехи считаются НАРАВНЕ с отказами, иначе ноль отказов неотличим от «сюда никто не приходил», а настройка стоит СВОЕЙ клеткой, потому что временем не лечится.

View Source
const ProviderCompensationTable = "kaname.provider_compensation_outbox"

ProviderCompensationTable — полное имя очереди компенсаций.

View Source
const SettingsScreenPath = "/settings"

SettingsScreenPath — путь экрана параметров учётной записи в консоли: письмо с кодом смены адреса называет его (kaname#635, Р7). Адрес экрана — происхождение консоли, объявленное той же настройкой, что адрес входа, и этот путь, без параметров и фрагмента.

View Source
const VerificationScreenPath = "/verification"

VerificationScreenPath — путь экрана подтверждения адреса в консоли. Адрес экрана — происхождение консоли, объявленное той же настройкой, что адрес входа в письмах службы, и этот путь, без параметров и фрагмента.

Variables

View Source
var (
	// ErrMailMisconfigured — отказ, который повтор не вылечит.
	ErrMailMisconfigured = errors.New("invite mail: relay is misconfigured")
	// ErrMailTransient — отказ, который лечится временем.
	ErrMailTransient = errors.New("invite mail: relay is temporarily unavailable")
)

Сигнальные ошибки классификации. КАЖДАЯ ветка возврата транспорта заворачивает свой отказ ровно в одну из них — корзины «прочее» у отправителя нет by construction.

InviteMailOutcomes — закрытый набор клеток семейства.

MailEventTypes — ЗАКРЫТЫЙ перечень видов события очереди писем, по одному на ветвь применителя. Им засевается метка `kind` ряда исходов по виду письма (kaname#733): неудача письма восстановления видна своей клеткой, а не растворяется в общем счёте полосы. Сверку с ветвями применителя и словарём схемы держит `TestEveryMailKindHasExactlyOneSender`.

Functions

func ClassifyInviteMailOutcome

func ClassifyInviteMailOutcome(err error) string

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

func RenderEmailChangeMail(relay MailRelay, ev MailEvent) []byte

RenderEmailChangeMail собирает тело письма с кодом смены адреса на НОВЫЙ адрес (kaname#635, Р7).

Письмо несёт КОД, его срок в минутах и адрес экрана параметров — и ничего сверх. Кода в адресе нет: код вводится руками на экране, где смена начата.

Штамп — как у RenderMail; адрес отправителя без домена — nil.

func RenderEmailChangedMail added in v0.5.0

func RenderEmailChangedMail(relay MailRelay, ev MailEvent) []byte

RenderEmailChangedMail собирает тело уведомления о смене адреса на ПРЕЖНИЙ адрес (kaname#635, Р7).

Уведомление несёт момент смены и строку «если это были не вы — обратитесь к администратору аккаунта» — и ничего сверх: нового адреса нет (прежний ящик мог оказаться в чужих руках), кода и ссылки нет.

Штамп — как у RenderMail; адрес отправителя без домена — nil.

func RenderInviteMail

func RenderInviteMail(relay MailRelay, ev MailEvent) []byte

RenderInviteMail собирает тело письма.

Письмо говорит об ОТПРАВКЕ и НИГДЕ не говорит «доставлено»: продукт видит сдачу ретранслятору, а не получение адресатом, и утверждать второе значило бы обещать то, чего он не знает (Р15).

Предъявителя письмо НЕ несёт (Р24): в нём призыв и адрес страницы входа, а доступ даёт владение почтовым ящиком, доказанное подтверждением адреса.

Штамп — как у RenderMail; адрес отправителя без домена — nil.

func RenderMail added in v0.5.0

func RenderMail(relay MailRelay, ev MailEvent) []byte

RenderMail — письмо целиком по виду события, со штампом отправки, который поставил бы отправитель с часами по умолчанию (NewInviteMailSender). Вид неизвестный применитель до транспорта не доводит (постоянный отказ), поэтому здесь исходов два.

Адрес отправителя без домена — nil: письма без штампа не бывает, и отдавать его нельзя даже для осмотра. Отправка судит тот же отказ сама (Send).

func RenderRecoveryMail added in v0.5.0

func RenderRecoveryMail(relay MailRelay, ev MailEvent) []byte

RenderRecoveryMail собирает тело письма восстановления доступа (Ф5 Р3).

Письмо несёт КОД и его срок — и ничего сверх: ни ссылки-предъявителя (полоса кодовая, Ф5 §1.2), ни утверждения «доставлено» (Р15 ID-MAIL-1 — продукт видит сдачу узлу, а не получение). Адрес консоли, если объявлен, стоит отдельной строкой и кода не несёт.

Штамп — как у RenderMail; адрес отправителя без домена — nil.

func RenderVerificationMail added in v0.5.0

func RenderVerificationMail(relay MailRelay, ev MailEvent) []byte

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

type InviteMailTransport interface {
	Send(ctx context.Context, ev MailEvent) error
}

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

func DecodeMailEvent(payload []byte) (MailEvent, error)

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

type RelationTuple struct {
	User     string
	Relation string
	Object   string
}

RelationTuple — тройка «субъект, отношение, объект».

ОСТАЁТСЯ ПОСЛЕ СНЯТИЯ ДВИЖКА: это форма полезной нагрузки СТРОКИ ЖУРНАЛА намерений (`kaname.fga_outbox`), из которой триггер складывает прямой факт. Журнал и триггер — решение Р7 приёмки R7-3: снимается потребитель журнала, а не журнал.

type TupleConditionRef

type TupleConditionRef = authztypes.TupleConditionRef

TupleConditionRef — псевдоним authztypes.TupleConditionRef.

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL