Aller au contenu

Changelog

All notable changes to private-gerege-nexus (Gerege Nexus) will be documented in this file.

Тэр журмаас үүдэн зарим бичлэг нь хожим устсан баримтыг нэрлэдэг (docs/RELEASING.md, docs/MONITORING.md, docs/CONTROL_PLANE.md, docs/URTUU.md гэх мэт). Нэрийг нь үлдээж, ХОЛБООСЫГ нь авсан: бичлэг тухайн өдрөө юуг заасныг хэлсээр байх ёстой ч GitHub дээр 404 өгөх ёсгүй. Кодын зам руу заасан хуучин холбоосууд ҮЛДСЭН — тэдгээрийг цэвэрлэх нь тусдаа шийдвэр.

Entries below the rebrand keep the names that were true when they shipped — the open-gerege-mn-erp repository, the ERP framing, and the openerp.gerege.mn deployment, which has since moved to nexus.gerege.mn. A changelog edited to match the present tense stops being a record.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Гаргаагүй шинэ бичлэг 2026-09-25-наас changelog.d/-д файл тус бүрээр бичигдэж, release-ийн өмнө ## [Unreleased] доор нэгтгэгдэнэ.


[Unreleased]

[1.49.13] - 2026-10-25

Added — External аппын OAuth2 client-ийг каталог бүртгэнэ (2026-10-25)

  • Юу: external аппын манифест external.redirect_uris (ба post_logout_redirect_uris)-ээ зарлана. Каталог sync бүр sso_client_id-г тэр хаягтай public (PKCE) client болгон үүсгэж/шинэчилж, апп каталогоос гарахад устгана. Migration 00161: oauth2_clients.app_id, ийм мөрийн tenant_id NULL.
  • Яагаад: өмнө нь апп хөгжүүлэгч (жишээ нь contract.mn-ийн Гэрээ) суулгасан платформ бүр дээр client-ээ гараар бүртгүүлэх ёстой байв — store-ын зам биш.
  • Дүрэм: хаяг бүр https, launch_url-ийн host дээр. Ижил id-тай байгууллагын гараар бүртгэсэн client-д хүрэхгүй (log бичнэ). Хоосон каталог устгалт хийхгүй. Бүртгэгдсэн нь нэвтрэлт биш: install gate хэвээр.
  • Хаана: pkg/catalog/manifest.go, internal/workspace/appinstall/external_clients.go, sync.go, ssoprovider/store_clients.go, docs/MINI_APPS.md, docs/IDENTITY.md.
  • Шалгасан: манифестын шалгалтын тест; бодит Postgres дээр үүсгэх → шинэчлэх → гараар бүртгэснийг хөндөхгүй → устгах тест; migrate up/down/up; go test ./....

[1.49.12] - 2026-10-25

Added — Нэмэлт OIDC provider: SSO_PEERS (2026-10-25)

  • Юу: суулгац SSO_CLIENT_ISSUER-ээс гадна өөр OpenID Connect provider-уудыг (ихэвчлэн өөр Gerege Nexus) нэвтрэх товч болгон авч болно: SSO_PEERS=<key>, SSO_PEER_<KEY>_ISSUER, _CLIENT_ID, _CLIENT_SECRET (заавал биш), _NAME, _SCOPES. Маршрут нь /api/v1/auth/<key>/start|link|callback.
  • Яагаад: SSO_CLIENT_ISSUER нэг л issuer авдаг. contract.mn e-property.mn-ээр нэвтэрдэг бөгөөд мөн nexus.gerege.net дээр external апп болж суусан — нэг issuer-тэй бол аль нэг нь л ажиллана.
  • Дүрэм: Apple/GitHub-ийн рэйл (ssoclient.Socials) дээр суусан тул тэдгээрийн дүрмийг өвлөнө: анхны нэвтрэлт eID binding-ийг хүлээнэ, identity (issuer, subject)-ээр, холбох нь зөвхөн асуусан session-д. Нэвтрэх дэлгэц peer-ийн товчийг серверийн өгсөн нэрээр («Gerege Nexus-ээр нэвтэрнэ») харуулна.
  • Хаана: backend/internal/workspace/ssoclient/peer.go (+ тест), social.go, identity/social.go, identity/routes.go, frontend/app/login/page.tsx, docs/IDENTITY.md.
  • Шалгасан: peersFromEnv-ийн нэгж тест (буруу түлхүүр, давхардсан/нөөцлөгдсөн нэр, issuer-гүй, HTTP issuer, public клиент, callback); go test ./...; frontend tsc · eslint · vitest.

[1.49.11] - 2026-10-25

Removed — API гарц цөмөөс хасагдав (2026-10-25)

  • Юу: io.gerege.nexus.api_gateway — цөмийн цорын ганц апп — бүхэлдээ хасагдав: модуль (backend/internal/apps/apigateway), нийтийн гарц /api/v1/gw/* ба удирдлагын API /api/v1/api-gateway/*, дэлгэцүүд (frontend/app/module/api-gateway), API клиент, толь, каталогийн бичлэг, манифест, chronicle. catalog/apps.json дахин []; demo seed апп суулгахгүй.
  • Яагаад: эзэн нь хасахаар шийдэв. Өөр distribution руу нүүгээгүй — авч явах газар байхгүй.
  • Өгөгдөл: migration 00160_the_api_gateway_leaves.sql дөрвөн workspace.api_gateway_* хүснэгт, public.goose_db_version_api_gateway түүх, суулгалтын мөрийг устгана. 2026-10-25-нд nexus.gerege.net, contract.mn, enotary.mn, powergrid.mn дээр API ба түлхүүрийн хүснэгт 0 мөртэй байв.
  • Шалгасан: хуучин main-аар migrate хийсэн DB дээр 00160 хүснэгт, түүхийг устгаж, дахин ажиллахад алдаагүй; routes.txt, route_chains.txt дахин бичигдэв (зөвхөн гарцын мөрүүд хасагдсан); backend go test ./... (DB, Redis-тэй), frontend tsc · eslint · vitest.

[1.49.10] - 2026-10-25

Removed — nexus.gerege.mn хаагдлаа, цаашид nexus.gerege.net (2026-10-25)

  • Юу: nexus.gerege.mn production суулгац байхгүй болсон тул түүний deploy, хостын тохиргоо бүгд устав: .github/workflows/deploy.yml, docker-compose.prod.yml, deploy/nginx, deploy/scripts, deploy/monitoring, deploy/dwh-landing, DWH / нөөцлөлт / мониторинг / appstore / GlitchTip-ийн compose ба .env.*.example. CI-аас тэдгээрийг шалгадаг «Monitoring config» ажил, deploy туслахын тест хасагдав. deploy/Dockerfile (CI-ийн image) үлдэнэ.
  • Цаашид: цорын ганц суулгац nexus.gerege.net — deploy-dev.yml main-ийн CI ногоон болмогц гаргана; хостын compose, нууц, нөөц нь proxmox-gerege-net репод. Консолын «Release»-ийн deploy товч (GITHUB_DEPLOY_WORKFLOW анхдагч), GHCR цэвэрлэгээ, eID smoke (scripts/eid-smoke.sh) бүгд deploy-dev / nexus.gerege.net руу заана.
  • Хаягууд: код, тест, native апп (macOS, Windows, iOS, Android), баримт дахь nexus.gerege.mn ба *.nexus.gerege.mn бүгд nexus.gerege.net болов (CHANGELOG ба migration-ий түүхэн тайлбар хэвээр).

[1.49.9] - 2026-10-25

Changed — Мэдэгдлийн цонх ба AI туслахын самбар шинэ загвараар (2026-10-25)

  • Мэдэгдэл: хонхны цонх «Мэдэгдэл — хонхны цонх» загвараар — 400px карт, уншаагүйн тоо, «Бүгд / Уншаагүй» шүүлтүүр, мэдэгдэл бүр апп, төрлөөрөө өнгөт дүрстэй, «апп · хугацаа» мөртэй. Хонхны тэмдэг загварын улаан тоо.
  • AI туслах: баруунаас гарах самбар «AI Туслах — баруунаас гарч ирэх самбар» загвараар — нил ягаан тэмдэг, «Шинэ яриа» товч, «AI чат / Орчуулга» таб, бэлэн асуултууд, дугуй оруулах талбар; орчуулга эх, орчуулсан хоёр талбар, «Хуулах», «Сонсох»-той. Толгойн товч нэргүй дүрс боллоо. Самбар хуудсыг бүдэгхэн бүрхэх ч хуудас дарагдсаар байна.
  • Хаана: frontend/components/{NotificationBell,AICopilot,Layout}.tsx, frontend/lib/i18n/{web.ts,addons/ai.ts}.

[1.49.8] - 2026-10-25

Changed — Нэвтрэх дэлгэцийн зураг жинхэнэ компонент (2026-10-25)

  • Нэвтрэх дэлгэцийн 3D цөмийн зураг HTML мөр (dangerouslySetInnerHTML) байхаа больж, React компонент болов (components/SignInScene.tsx). Хэсгүүдийн шошго орчуулгаас ирж, EN хэл дээр англиар гарна.

Changed — Аппын цэс аппын өөрийн нэрлэсэн бүлгээр (2026-10-25)

  • Юу: платформ апп бүрт «Модуль», «Тохиргоо» гэсэн тогтмол хоёр бүлэг өгөхөө больсон. Апп цэсний бүлгээ өөрөө зарлана: path, external_url, parent_id-гүй мөр нь бүлэг, дэлгэц бүр parent_id-ээр өөрийн аппын бүлгийг нэрлэнэ; монгол нэр labels-ээр (manifest-д одоо уншигдана). Бүлэг нэрлээгүй, эсвэл өөр аппын бүлэг рүү заасан дэлгэц аппын нэртэй бүлэгт орно — хуучин manifest эвдрэхгүй. Консолын ConsoleMenus аль хэдийн ийм.
  • Дүрэм: ValidateManifest нэргүй бүлэг, «Модуль/Modules» нэртэй бүлэг, өөр аппын бүлэг рүү заасан parent_id-ийг татгалзана; docs/MODULES.md — Цэсний бүлэг. MenuDefinition.Group, nexus.MenuGroupSettings хуучирсан (үл тоомсорлоно), аппууд бүлгээ зарласны дараа устна.
  • Бүрхүүл: .settings. id-аар дахин эцэглэх ба байгууллагын аппын дэлгэцийг Платформын тогтмол бүлэгт хийх дүрэм хасагдав — chrome апп бүлгээ өөрийн нэрээр Платформын самбарт авчирна.
  • API Gateway: «Нийтлэл» (API-ууд, гарцын түлхүүр), «Хяналт» (хэрэглээ).
  • Хаана: backend/internal/workspace/menu/menu.go, backend/pkg/nexus/module.go, backend/pkg/catalog/manifest.go, backend/internal/apps/apigateway/module.go, catalog/manifests/*.json, frontend/components/Layout.tsx, docs/MODULES.md; тест backend/internal/workspace/menu/groups_test.go, backend/pkg/catalog/menu_groups_test.go.

Fixed — Утсан дээр картын баруун тал тасарч байсан (2026-10-25)

  • Юу: 375px өргөнтэй утсан дээр хувийн гэрийн «Профайл» табын дансны карт, статистикийн хавтан, «Харьяалагдах байгууллага»-ын толгой дэлгэцээс өргөн болж баруун тал нь таслагдаж байв (хуудас хажуу тийш гүйлгэгддэггүй тул харагдахгүй алга болж байсан). Шалтгаан: grid lg:grid-cols-2 нь утсан дээр нэг auto баганатай — тэр багана хамгийн өргөн хүүхдийнхээ хэмжээнд хүртэл тэлдэг. Ийм бүх сүлжээнд утсан дээр grid-cols-1 (minmax(0,1fr)) өгөв; картын толгойн товч мөр шилжинэ; «Харагдах байдал»-ын урьдчилан харах ширээний өргөнөө хадгалж утсан дээр өөрөө хажуу тийш гүйлгэгдэнэ; бүртгэл устгах тайлбар товчны дээр орно.
  • Хамгаалалт: e2e-ийн утасны тест одоо таслагдсан элементийг ч барина (зөвхөн scrollWidth биш).
  • Хаана: frontend/components/home/{cards,PersonHome,WalletHome}.tsx, frontend/components/profile/{StatCards,PersonApps,OrganisationsSection,LeaveSection}.tsx, frontend/app/{my/dashboard,organisation/dashboard,settings/access,settings/appearance,settings/integrations}/page.tsx; тест frontend/tests/e2e/dashboard.spec.ts.

[1.49.7] - 2026-10-25

Fixed — Хувийн гэрийн табууд самбарт, тохиргоо самбарын доор, цэвэр толгой хэсэг (2026-10-25)

  • Хувийн гэрийн табууд: тойм ба профайл нэг «Профайл» таб болов (дээр нь тойм, доор нь нэвтрэлт, таних тэмдэг, eID, байгууллагууд). Хажуугийн самбарт «Хянах самбар»-ын доор Профайл / Аппууд / Үйл явдал харагдаж, нээлттэй нь тэмдэглэгдэнэ — таб хаягт (?tab=) байна; хуучин ?tab=profile, ?tab=eid «Профайл»-ыг нээнэ. Байгууллагын самбарын профайл таб «Профайл ба eID» биш «Профайл».
  • Тохиргоо самбарын доор: гэрт «Тохиргоо» (харагдац) «Миний гэр» бүлэгт «Хянах самбар»-ын дараа орж, тусдаа «Тохиргоо» бүлэг байхгүй болов. Байгууллагад «Хандалтын тохиргоо», «Холболтууд» «Байгууллага» бүлэгт орж, «Тохиргоо» аппад App Store л үлдэв (харах эрх өмнөх шигээ).
  • Толгой хэсэг: тохиргооны (араа) ба хэлний («МН») товчийг авав — хэл хэрэглэгчийн цэс ба «Харагдах байдал»-д.
  • Тэнцүү багана: «Профайл» табын дээд мөр (данс ба статистик) доорх картуудтай ижил хоёр тэнцүү багана, ижил зайтай; профайлын картууд мөн хоёр багана зэрэгцэнэ.
  • «Таны таних тэмдгүүд» хэсгийг авав: eID-ийг дансны картын «eID-ээ холбох» товч тэр дор нь холбоно; Google ба бусад провайдерыг холбох, салгах товч энэ хуудаснаас алга болов.
  • Хувийн дэлгэц гэр рүү шилжүүлнэ: байгууллага дотроос «Миний профайл» (эсвэл /my/... холбоос) нээхэд session нь хувийн гэр рүү шилжиж, тэр хуудсандаа үлдэнэ — өмнө нь гэрийн дэлгэц байгууллагын самбар, сэлгэгчтэй хамт гарч, гэр солигддоггүй байв (HomeOnly, OrganisationOnly-ийн толин тусгал).
  • Шууд самбар: rail-ийн лого сэлгэгч нээхээ больж самбарын эхлэл рүү хөтөлнө; нэвтэрсний дараах /profile хаяг самбарын эхний таб дээр буулгана. «Харагдах байдал» ч мөн хувийн гэр рүү шилжүүлнэ.
  • Хатуу цэнхэр хавтан бүдэг болов: eID тэмдэг, апп ба алхмын тэмдэг, сэлгэгчийн чек, аппын дүрс, .tone-chip.
  • Эхний үсгийн тэмдэг нэг хэв: зураггүй хүн, байгууллагын эхний үсэг бүх газарт (самбарын карт, хураасан самбар, байгууллагын том тэмдэг, гишүүдийн нүүр, үйлдлийн хүн, хэрэглэгчийн цэс ба аватар, солигч, эрхийн жагсаалт, профайл, eID бүртгэл) бүдэг цэнхэр бөөрөнхий дөрвөлжин, нимгэн цэнхэр хүрээтэй (.initials); бүдэг цэнхэр карт дээр цагаан.
  • Хаана: frontend/components/Layout.tsx, frontend/components/home/PersonHome.tsx, frontend/app/organisation/dashboard/page.tsx, frontend/lib/permissions.tsx, frontend/app/my/layout.tsx, frontend/app/settings/appearance/layout.tsx, frontend/app/profile/page.tsx, frontend/components/profile/IdentitiesSection.tsx, frontend/components/miniapp/person/AppSheet.tsx, backend/internal/workspace/menu/menu.go, frontend/app/globals.css, frontend/components/UserMenu.tsx, frontend/components/TenantChoices.tsx, frontend/app/settings/access/page.tsx, frontend/app/cp/profile/page.tsx, frontend/components/profile/Header.tsx, frontend/components/eid/PersonRecord.tsx; тест backend/internal/workspace/menu/fallback_test.go, frontend/tests/me-dashboard.test.tsx, frontend/tests/home-only.test.tsx.

[1.49.6] - 2026-10-24

Changed — Нэвтрэх дэлгэц «Нэвтрэх (landing + 3D)» загвараар (2026-10-24)

  • Юу: /login (мөн /login/bind, нээлтийн мэдэгдэл) — самбарын өмнөх нэвтрэх дэлгэц, загварын яг дарааллаар хоёр баганатай. Зүүн тал: брэнд ба хост ↗, Nexus цөм ба түүн дээр ажилладаг хэсгүүдийн 3D зураг, «Нэгдсэн нэвтрэлт», «Нэг удаа нэвтэр. Бүх системд хүр.», гурван баталгаа (eID Mongolia · нууц үггүй, нэг данс, аудит), ©. Бичвэр дээд хэсэгт, загвараас том (гарчиг 44–60px, тайлбар 18px); 3D зураг том хэмжээгээр бичвэрийн ард, дээд хэсэг нь зөөлөн бүдэгрүүлсэн. Нэг дэлгэц өндөр. Баруун тал: лого ба «Тусламж», «{brand} · Нэгдсэн нэвтрэлт», «Нэвтрэх», «Өөрийн бүх байгууллага, апп руу нэг данснаас.», 48px бөөрөнхий талбарууд, «эсвэл», eID Mongolia ба Passkey нэг мөрөнд, урилга/сэргээх кодын шар карт (код нь tab-ийн sessionStorage-аар /login/set-password руу — хаягт биш, lib/redeemCode.ts), «Шинэ хэрэглэгч үү?», «Шифрлэгдсэн холболт · хост». lg-ээс доош зөвхөн нэвтрэх тал. Тохируулгын wizard (wide) хуучин нэг картаараа. «Тусламж» ба хэлний товч нэг өндөр (38px).
  • Хаана: frontend/components/SignInFrame.tsx, frontend/components/SignInScene.tsx (загварын SVG, тогтмол), frontend/app/login/page.tsx, frontend/app/login/set-password/page.tsx, frontend/lib/redeemCode.ts, frontend/components/LanguageSwitcher.tsx, frontend/app/globals.css, frontend/lib/i18n/addons/auth.ts.

Changed — Миний гэр шинэ загвараар (2026-10-24)

  • Юу: /my/dashboard нь «Миний гэр (нэгтгэсэн)» загвараар. Таб дөрөв: Тойм, Аппууд (тоотой), Үйл явдал (7 хоногийн тоотой), «Профайл ба eID» — профайл ба «Миний eID» нэг таб болсон (?tab=eid түүнийг нээнэ; eID холбосон бол eID-ийн бүртгэл профайл табын доод хэсэгт). Тойм: «Миний данс» — имэйл, бүртгүүлсэн огноо, eID-ийн төлөв, «Дансны хамгаалалт» 4 хэсэгтэй хэмжүүр (eID, нэвтрэх нэр, нууц үг, passkey), «eID-ээ холбох», «Бүх тохиргоо →»; дөрвөн тоо (байгууллага, апп, нээлттэй session, 7 хоногийн үйлдэл) — тус бүр өөрийн таб руу; «Миний аппууд» ба сүүлийн 3 үйлдэл. Үйл явдал: бүх үйлдэл өдрөөр, мэдэгдэл хажууд. Профайл: карт бүр дүрстэй толгойтой; eID холбоогүй бол цэнхэр самбар — гурван давуу тал ба «eID-ээ холбох». «Шууд шинэчлэгдэнэ» нь хүрээтэй pill.
  • Өөрчлөгдсөн байрлал: мэдэгдэл тоймоос «Үйл явдал» таб руу, нууц үг солих «Профайл ба eID» таб руу; тоймын eID/нууц үгийн карт, профайлын статистик картууд хасагдсан (тоймын дөрвөн тоо ба хамгаалалтын хэмжүүр тэдгээрийг орлоно).
  • Орхисон: «Нээлттэй session» жагсаалт ба «Бусдыг хаах» (session-ий жагсаалтын API алга — тоо нь тоймд бий); «App Store-оос нэмэх» зөвхөн админд (/apps нь админых).
  • Хаана: frontend/components/home/PersonHome.tsx, frontend/components/home/cards.tsx, frontend/components/profile/*Section.tsx, frontend/components/profile/PersonApps.tsx, frontend/lib/datetime.ts (weekdayOf), frontend/lib/i18n/addons/home.ts; тест frontend/tests/me-dashboard.test.tsx.

Changed — Байгууллагын хянах самбар шинэ загвараар (2026-10-24)

  • Юу: /organisation/dashboard-ийг 2026-10-ын «Байгууллагын хянах самбар (сайжруулсан)» загвараар дахин зурав: мэндчилгээ ба нэр, «Гишүүн урих» / «App Store» товч; админд «Эхлэл» — дөрвөн алхам (байгууллага үүссэн, профайл бүрэн, хоёр дахь гишүүн, эхний апп) ба явц; байгууллагын том карт (лого/эхний үсэг, slug · цагийн бүс · валют, зургаан баримт, «Нэмээгүй»); Гишүүд, Идэвхтэй апп, Профайлын бүрэн байдал (9 талбараас) өнгөт картаар; сүүлийн 7 хоног accent дээр; сүүлийн үйлдлүүд (үйлдлийн өнгөт цэг, хэн нь эхний үсэгтэй) эсвэл админ бусдад яагаад хаалттайг; шуурхай холбоос.
  • Санаатай: гишүүн, апп-ийн картад явцын зураас байхгүй — тэдэнд «бүтэн» гэх тоо байхгүй, зохиосон зорилт болно. Загварт байгаа ч тэр хоёрыг зураагүй. Профайлын 9 талбар (lib/orgProfile.ts) нэг жагсаалтаас — самбар, алхам, Профайл табын жагсаалт санал нийлэхгүй байх боломжгүй.
  • Дараа нь (шинэчилсэн загвар): хавтгай карт, загварын яг өнгө (#2F4FE0, таван өнгө hex-ээр); бүрэн тохируулсан байгууллагад гишүүдийн нүүр, суусан аппын дүрс, 9 хэсэгтэй зураас, «≈ N / өдөр»; «Үйл явдал» таб — төрлөөр шүүх chip (тоотой), хайлт, өдрөөр бүлэглэх, мөр дарахад дэлгэрэнгүй; «Профайл» таб — хоосон талбар шараар, бүрэн байдлын цагираг ба жагсаалт, хадгалах карт; ачаалал картын хэлбэртэй, алдаанд «Дахин оролдох». Ажлын талбар #F5F7FC.
  • Бүрхүүл ба хэрэглэгчийн цэс: толгой 68px — зам (Платформ › бүлэг › дэлгэц), голд хайлт ⌘K-тэй (⌘K / Ctrl+K дарахад хайлт руу), «AI туслах» ягаан товч, хүрээтэй хонх ба хэл (МН), дөрвөлжин аватар нэр ба үүрэгтэй. Rail 62px, саарал дүрс, идэвхтэй нь өөрийн өнгөөр, админд «Апп нэмэх». Хажуугийн самбарын байгууллагын карт (эхний үсэг, «Админ»), бүлгийн гарчиг том үсгээр. Хэрэглэгчийн цэс: толгой (аватар, имэйл, байгууллага ба үүрэг), хэл ба загвар сегментээр, «Гарах» нэг л газар — сүүлийн улаан мөр (толгой дахь давхар товчийг авав).
  • Гурав дахь хувилбар: бичгийн хэмжээ нэг шат томорсон (13→14px, гарчиг 28px); идэвхтэй таб, chip, сегмент navy биш зөөлөн цэнхэр (#EEF3FF); «Үйл явдал» мөр нягт (40px дүрс, 14px гарчиг); тойм дахь «Сүүлийн үйлдлүүд» ба «Шуурхай холбоос» дүрстэй толгой, мөр бүр дүрс ба tag-тай; «Тохиргоо» rail-ийн ёроолоос толгойн мөр рүү (араа товч); хажуугийн самбарын мөр 28px дүрсний хайрцагтай; байгууллага солигч — «Байгууллагууд N», мөр бүр өнгөт тэмдэг ба slug, одоогийнх цэнхэр хүрээтэй; ганц байгууллагын тайлбар, шилжилт амжилтгүйн улаан хайрцаг, 5-аас олон бол хайлт.
  • Дөрөв дэх хувилбар: хажуугийн самбарыг хураахад алга болохгүй, 72px дүрсний багана болно — байгууллагын эхний үсэг, цэсний мөр бүр 44px дүрс (нэр нь tooltip-д), бүлгийн хооронд зураас, одоогийнх цэнхэр; хураасан үед товч нь цэнхэр. Rail-ийн идэвхгүй апп нь rail-ийн өнгийг бараантуулсан өнгөөр (бүдэг шар дээр хүрэн). Хэрэглэгчийн товч бүдэг цэнхэр хүрээтэй.
  • Орхисон: «Тусламж» tile, «Бүх систем хэвийн · хувилбар» мөр (шалгах зүйл, хувилбарын тогтмол алга), хажуугийн самбарын тохируулгын карт, «Аудитын бүрэн бүртгэл →» (байгууллагын аудит хуудас алга), хэрэглэгчийн цэсний «Солих» (байгууллага солих нь самбарын картанд), солигч дахь байгууллага бүрийн үүрэг (/tenants үүрэг буцаадаггүй), rail-ийн «Тусламж» ба хураасан самбарын тохируулгын цагираг, хажуугийн самбар дахь табын дэд мөр (Тойм / Үйл явдал / Профайл).
  • Хаана: frontend/app/organisation/dashboard/page.tsx, frontend/lib/orgProfile.ts, frontend/components/organisation/OrganisationProfile.tsx, frontend/components/Layout.tsx, frontend/components/UserMenu.tsx, frontend/components/AICopilot.tsx, frontend/components/NotificationBell.tsx, frontend/lib/i18n/addons/core.ts, frontend/lib/i18n/web.ts, frontend/app/globals.css; тест frontend/tests/organisation-dashboard.test.tsx, frontend/tests/user-menu-height.test.tsx, frontend/tests/tenant-choices.test.tsx; frontend/components/TenantChoices.tsx.

Changed — Интеграц ба Webhook шинэ загвараар (2026-10-24)

  • Юу: /settings/integrations-ийг «Интеграц ба Webhook (сайжруулсан)» загвараар: дүрстэй гарчиг; INTEGRATION_ENCRYPTION_KEY алга бол шар карт (түлхүүрийн нэр код хэлбэрээр, «Админ консол →»); гурван тоо — холбогдсон, холбоход бэлэн, платформд тохируулаагүй; «Холбох боломжтой үйлчилгээ» нэг картад, Бүгд / API ба Webhook / Файл ба уулзалт шүүлтүүртэй, тохируулаагүй үйлчилгээг хэн хаана тохируулдаг тухай тайлбар; үйлчилгээ бүр өөрийн өнгө, дүрс, тайлбартай, «Консол →» эсвэл «Нэмэх →»; «Бүртгэсэн интеграц» тоотой, хоосон бол тайлбар ба «Эхний интеграцаа нэмэх».
  • Хаана: frontend/app/settings/integrations/page.tsx, frontend/lib/i18n/addons/integrations.ts.

Changed — Консолын eID таб QR-ээ шууд харуулна (2026-10-24)

  • Юу: консолын нэвтрэх картын «eID Mongolia» таб товч дарж тусдаа цонх нээдэг байсныг болиулж, QR, алхмууд, баталгаажуулах кодыг табын дотор шууд зурна. eID widget-д inline: true сонголт нэмэв (товч ч, dialog ч үгүй; нээгдмэгц QR эхэлнэ).
  • Хэвээр: нийтийн сайтын eID товч, /login хуудас widget-ийн dialog-оор хэвээр.
  • Хаана: frontend/public/eid-login.js, frontend/components/cp/ConsoleLanding.tsx, frontend/components/cp/Console.tsx, frontend/app/landing.css.

Added — «Харагдах байдал»: харагдац ба форматыг өөрөө тохируулах (2026-10-24)

  • Юу: /settings/appearance нь «Харагдах байдал — өөрийн загвар» загвараар: бэлэн 4 загвар (Nexus, ContractMN, Ногоон, Ягаан); аппын багана, цэс, дээд мөр, ажлын талбайн дэвсгэр; 6 үндсэн өнгө; үсгийн хэмжээ ба хэлбэр (Manrope, IBM Plex, систем), товчны хэлбэр ба загвар, картын булан ба хүрээ, нягтрал. Баруун талд ажлын орчны жижиг урьдчилсан харагдац ноорогоор шууд өөрчлөгдөнө; «Хадгалах» дарахад бүх апп-д (энэ хөтөч дээр) үйлчилнэ, «Анхны байдал» анхныхыг сэргээнэ. Өнгөнүүд гэгээн горимд. Гэгээн/харанхуй горим, хэлний жагсаалт хэвээр.
  • Шинэчилсэн загвар: дэлгэц 8 ангилалтай (Загвар, Өнгө, Текст, Товч ба талбар, Карт ба таб, Цэс ба багана, Хүртээмж, Бүс ба формат) — ангилал, тохиргоо, урьдчилан харах гурван багана. Нэмэгдсэн нь: горим ноорогт, картын өнгө, гарчгийн жин, мөр хоорондын зай, товчны хэмжээ, талбарын загвар (хүрээтэй / дүүрэн / доогуур зураас) ба булан, картын зөөлөн сүүдэр, таб (капсул / зураас / хайрцаг), цэсний өргөн ба дүрс, аппын нэрийг нуух, дүрсний зураасны зузаан, өндөр ялгарал, холбоосын доогуур зураас, тод focus хүрээ, хөдөлгөөн бага/унтраах; огноо (2026.10.24 / 24.10.2026 / 10 сарын 24), цаг (24/12), тоо (1 234 567 / 1,234,567) — lib/datetime-ээр бүх дэлгэцэд. Бэлэн загвар 6 (Албан, Шөнө нэмэгдсэн).
  • Анхаар: анхны формат өөрчлөгдсөн — огноо 2026-10-24 биш 2026.10.24, тоо 1,250,000 биш 1 250 000 (no-break зай). Өдрөөр бүлэглэх, харьцуулах код dayKey()-г ашиглана.
  • Орхисон: «Байгууллага даяар» (байгууллагын түвшний хадгалалт backend-д алга), «7 хоногийн эхлэл» (ашигладаг дэлгэц алга).
  • Бүрхүүл: rail-ийн tile бүр доороо нэртэй (Платформ, Дэлгүүр), идэвхтэй нь цайвар хайрцаг ба ирмэгийн зураастай; rail анхнаасаа Nexus-ийн бүдэг шар. Хажуугийн самбарт «Хянах самбар»-ын доор Тойм / Үйл явдал / Профайл (таб нь хаягт ?tab=); байгууллагын картанд «Админ · N гишүүн».
  • Хаана: frontend/app/settings/appearance/page.tsx, frontend/lib/theme.tsx (Look, applyLook), frontend/public/theme-init.js, frontend/lib/landingFonts.ts (IBM Plex Sans, preload-гүй), frontend/app/globals.css, frontend/components/Layout.tsx, frontend/app/organisation/dashboard/page.tsx, frontend/lib/i18n/addons/appearance.ts, frontend/lib/i18n/web.ts; тест frontend/tests/setup.ts, frontend/tests/organisation-dashboard.test.tsx, frontend/tests/cp/ui.test.tsx, frontend/tests/bank-wallet.test.tsx, frontend/tests/me-dashboard.test.tsx, frontend/tests/miniapp-sandbox.test.tsx; frontend/lib/datetime.ts.

Changed — Нүүр хуудасны харагдац бүх дэлгэцэд (2026-10-24)

  • Юу: нүүр хуудасны 2026-10-ын харагдацыг нэвтэрсний дараах бүх дэлгэцэд (ажлын орчин, дистрибьюшний аппууд, консол, нэвтрэх хуудас) шилжүүлэв: Manrope ба IBM Plex Mono үсэг, гүн цэнхэр бичвэр, цэнхэр саарал дэвсгэр дээр цагаан карт, цэнхэр өнгөтэй зураас, илүү дугуй булан (12 / 18 / 24px), бүх товч pill хэлбэртэй, үндсэн товч accent-ийн градиент ба туяатай, сүүдэр нь хар биш бичвэрийн өнгөтэй, дэлгэцийн гарчиг тод (800). Харанхуй горим нүүрнийхээ шиг гүн navy. eID-ийн товч pill.
  • Хэрхэн: UI сангийн өөрийн token-ууд дээр (frontend/app/globals.css) — дэлгэц нэгбүрчлэн өөрчлөөгүй. Үсгийг frontend/app/layout.tsx бүх хуудсанд ачаална (lib/landingFonts.ts; Geist хасагдсан).
  • Хэвээр: deployment-ийн өнгө (BRAND_THEME_COLOR / accent), хэрэглэгчийн сонгосон accent, BRAND_THEME_CSS_FILE дарсаар байна.
  • Өнгө: нүүр хуудасны зургаан өнгө утгаараа бүх дэлгэцэд (frontend/lib/tones.ts): цэнхэр — хүн, байгууллага (accent); ногоовтор — эрүүл, дата, нөөц, аюулгүй байдал; шаргал — оператор, консол, квот; ягаан — апп, AI, хөгжүүлэгч; тэнгэрийн цэнхэр — хяналт, үйлдэл, session; улаан — алдаа, сэрэмжлүүлэг, устгал. Тоон картууд мөрийн дарааллаар (цэнхэр → ногоовтор → ягаан → шаргал → тэнгэрийн) өнгө авна; апп бүрийн rail-ийн дүрс өөрийн өнгөтэй; карт толгойн дүрс accent-аар.

Changed — Эрхийн тохиргоо шинэ загвараар (2026-10-24)

  • Юу: /settings/access-ийг «Эрхийн тохиргоо (сайжруулсан)» загвараар: толгой (дүрстэй гарчиг), табууд тоотой (хүсэлт байвал улаан). Role жагсаалт — role бүр өнгөт тэмдэгтэй (Administrator ягаан, Manager ногоон, User цэнхэр), «N эрх · Идэвхтэй»; «Шинэ role» маягт гарчигтай талбартай; эрх нэмэгддэг тухай тайлбар. Засварлагч: role-ийн толгой, «N / M эрх» зураас, «Болих» ба тоотой «Хадгалах»; эрх хайх, Бүгд / Сонгосон / Сонгоогүй шүүлтүүр, хадгалаагүй өөрчлөлтийн тоо; эрхүүд аппаар бүлэглэгдэж хоёр баганаар, бүлэг бүр дүрс, «n / m», бүгдийг асаах/унтраах switch-тэй; өөрчлөгдсөн мөр шаргал, «Өөрчлөгдсөн» тэмдэгтэй. Хэрэглэгчид ба Хүсэлт табууд ижил карт, аватартай.
  • Хаана: frontend/app/settings/access/page.tsx, frontend/lib/i18n/addons/access.ts.

[1.49.5] - 2026-10-24

Changed — Nexus-ийн шинэ 3D тэмдэг (2026-10-24)

  • Юу: платформын тэмдгийг 2026-10-ын «3D тэмдэг» загвараар солив: өнгөний шилжилттэй дугуй, сийлсэн цагираг, төв ба гурван зангилаа — сүүдэр, гялбаагүй. Брэндийн лого (/brand.webp), PWA дүрс (192, 512, maskable 512), Apple touch icon, favicon (16/32/48) ба нүүр хуудсанд зурагддаг тэмдэг (kit.tsx Mark) бүгд нэг зургаас.
  • Хэвээр: BRAND_LOGO_URL, BRAND_ICON_URL-ээр өөрийн лого өгсөн суулгац өөрийнхөө логог харуулсаар байна.
  • Хаана: frontend/public/brand.webp, frontend/public/favicon.ico, frontend/public/icons/*, frontend/components/landing/kit.tsx.
  • Үлдсэн: native апп (iOS, Android, macOS, Windows) ба баримт бичгийн (MkDocs) лого хуучнаараа — тусдаа солих.

Fixed — Нүүрний «Платформ руу» товч хугацаа дууссан session-д гарахаа болив (2026-10-24)

  • Юу: нүүр хуудасны header ба хаалтын хэсэг session_token cookie байгаа эсэхээр л «Платформ руу» гаргадаг байв. Session дууссан (90 мин идэвхгүй, 12 цаг) ч cookie үлддэг тул нэвтрээгүй хүн eID-ийн товчны оронд «Платформ руу» хардаг байв. Одоо hero шиг API-аас асууна (useAccess): хүчинтэй session байвал л «Платформ руу».
  • Хаана: frontend/components/landing/SiteHeader.tsx, CallToAction.tsx; тест frontend/tests/landing-header-session.test.tsx.
  • Мөн: хаалтын хэсгийн гарчгийн хоёр мөр өнгөө солив — «Систем бүрээ дахин бүтээхээ боль.» цайвар цэнхэр, «Nexus дээр босго.» цагаан.

Changed — Консолын нүүр шинэ загвараар (2026-10-24)

  • Юу: консолын (admin.*) нэвтрээгүй хүнд харагдах нүүрийг 2026-10-ын «Консол (admin)» загвараар дахин зурав: hero (консолын зураг), эхлүүлэх таван алхам, консол юу хийдэг (16 боломж), өөрчлөлт ба аудит нэг commit, дөрвөн үүрэг, нэвтрэх хэсэг. Нэвтрэх хэсэг нь гүн цэнхэр зурвас дээр операторын нэвтрэлтийн карт: «Нууц үг + TOTP» ба «eID Mongolia» гэсэн хоёр таб (eID-гүй серверт таб байхгүй, маягт л), баталгаажуулагчийн код зургаан нүдээр (дараагийн нүд рүү шилжинэ, paste бүгдийг дүүргэнэ), eID таб нь eID widget-ийг шууд нээнэ. Үсэг нь нүүр хуудастай ижил (Manrope, IBM Plex Mono — frontend/lib/landingFonts.ts).
  • Хэвээр: нэвтрэх логик (eID-ийн консолын замууд, QR л, нууц үг + TOTP, алдааны мессеж), брэнд, гэрэл/харанхуй горим, нэвтэрсний дараах консол.
  • Хаана: frontend/components/cp/ConsoleLanding.tsx, frontend/components/cp/Console.tsx (SignIn), frontend/app/landing.css.
  • Шалгасан: vitest 502/502, e2e (console, landing) давсан; 1440 ба 375px, гэрэл/харанхуй screenshot, хажуу тийш гүйлгээ 0.
  • Үлдсэн: консолын зурган дахь тоонуудын оронд хоосон байр (бодит тоо биш); үг mn/en-ээр.

[1.49.4] - 2026-10-24

Changed — Нүүр хуудас дэлгэрэнгүй загвараар (2026-10-24)

  • Юу: нийтийн нүүр хуудсыг 2026-10-ын «Дэлгэрэнгүй (кодоос)» загвараар дахин зурав. Нэг урт хуудас, 16 хэсэгтэй: hero (платформын зураг: middleware → гурван урсгал → kernel → PostgreSQL), Nexus-ийг тойрсон үйлчилгээ, хэнд зориулсан, танилт ба эрх (IdP), өгөгдлийн тусгаарлалт, аюулгүй байдал (6 давхарга × 6 хяналт, эмзэг байдал мэдээлэх security@gerege.mn), App Store, цөмд бэлэн зургаан чадвар, гарын үсэг, Mini app ба native, операторын консол, найдвартай ажиллагаа, хөгжүүлэгчид, кодоос тоолсон тоо, түгээмэл асуулт, хаалтын урилга. Цэс нь /#services, /#identity, /#security, /#applications, /#operator, /#developers руу үсэрнэ. Үсэг нь Manrope ба IBM Plex Mono. Hero-гийн «Платформд зочлох» нь /login руу; header ба хаалтын «eID Mongolia-аар нэвтрэх» нь eID-ийн widget-ийг шууд нээнэ (eID Mongolia-ийн логотой).
  • Хэвээр: брэнд (нэр, лого, өнгө), гэрэл/харанхуй горим, LANDING_SECTIONS, SERVICE_URL_* (тохируулаагүй үйлчилгээ гарахгүй), нэвтэрсэн хүний товч ба ирсэн гэрээ, дэлгүүр, /architecture, /platform, /trust хуудсууд. Суулгац өөрийн hero зураг өгсөн бол (LANDING_MEDIA_FILE) зураг нь платформын зургийн оронд гарна.
  • Хаана: frontend/components/landing/* (шинэ хэсгүүд ба kit.tsx), frontend/app/landing.css, frontend/lib/landing.ts (DEFAULT_LANDING_SECTIONS, HOME_MENU).
  • Шалгасан: vitest 501/501, landing e2e 5/5; 1440 ба 375px, гэрэл/харанхуй, mn/en дээр screenshot, хажуу тийш гүйлгээ 0.
  • Үлдсэн: хуудасны үг mn/en-ээр, ar/es/fr/ru/zh англи руу fallback хийнэ. «Кодоос тоолсон» тоог 2026-10-24-нд гараар тоолсон. Загварын имэйл ба утасны дугаар хоосон байсан тул холбоо барих хэсгийг оруулаагүй.

[1.49.3] - 2026-10-24

Changed — eID Mongolia widget шинэ загвартай (2026-10-24)

  • Юу: frontend/public/eid-login.js-ийн цонх шинэ загвараар зурагдана:
  • толгойд eID Mongolia лого, гарчиг, хаах товч;
  • QR кодын доор дугаарласан гурван алхам;
  • баталгаажуулах кодыг орон тус бүр тусдаа нүдэнд, өнгөт хэсэгт харуулна; хугацаа нь бүтэж буй зураас ба «Хүсэлт 2:44-ийн дараа хүчингүй болно» гэж тоологдоно;
  • «эсвэл» зааглагчийн доор нөгөө арга;
  • хөлд PIN-ий сануулга ба «eID Mongolia татах» холбоос (options.downloadUrl). Регистрээр илгээсэн үед утасны тэмдэг, амжилттай нэвтэрсэн үед ногоон тэмдэгтэй «Амжилттай нэвтэрлээ» харагдана.
  • Бараан горим: <html> дээр dark class эсвэл data-theme="dark" байвал (Nexus-ийн theme ингэж тавьдаг), мөн options.theme-ээр (light / dark / auto) асаана. Өнгө бүр --eid-* хувьсагч, бараан хувилбар нь -dark төгсгөлтэй.
  • API, poll, QR шинэчлэлт, утсан дээр апп руу шилжих логик өөрчлөгдөөгүй. Код дэлгэц уншигчид бүтнээрээ уншигдана (.eidw-sr).
  • Шалгасан: stub backend-тэй локал хуудсанд таван төлөв (QR, бараан, регистр, илгээсэн, амжилттай)-ийг дэлгэцийн зургаар шалгасан. tests/login-password.test.tsx тэнцсэн; e2e (citizen-eid.spec.ts) нь CI дээр.

[1.49.2] - 2026-10-24

Fixed — «Буцах» товч нүүр хуудсан дээр бүдгэрч далдлагдахаа больсон (2026-10-24)

  • Юу: external аппаас ирсэн платформ руугаа буцах мөр урсгал дотор биш, toast давхаргад хөвөгч том товч болсон (доод талд, lg-ээс доош мобайл таб мөрийн дээр).
  • Яагаад: олон нийтийн нүүр хуудсын толгой хэсэг дээд талд fixed тул мөрийг давхарлаж, бүдэг, хагас далд харагдуулж байв (enotary.mn, 1.49.1).
  • Хаана: frontend/components/Layout.tsx (ReturnBar).
  • Шалгасан: tsc --noEmit, tests/return-to.test.ts; production дээр нүдээр.

[1.49.1] - 2026-10-24

Added — External апп-аас ирсэн платформ руугаа буцах (2026-10-24)

  • Юу: external аппын холбоос платформын origin-ийг nexus_return параметрээр дагуулна. Тийм параметртэй ирсэн Nexus бүрхүүл үүнийг tab-д хадгалж (eID-ийн тойрогт алдагдахгүй), дээд талд өргөн, тод «← руу буцах» мөр зурна; хааж болно. Зөвхөн origin (зам биш — холбоос noreferrer хэвээр), зөвхөн https, өөрийн origin биш.
  • Яагаад: store-оос суулгасан external апп (enotary.mn, tasker.mn …) шинэ tab-д нээгдээд хэрэглэгч ирсэн газраа буцах замгүй үлддэг байв.
  • Хаана: frontend/lib/returnTo.ts, frontend/components/Layout.tsx (ExternalAnchor, ReturnBar), web.action.return_to, PR #287.
  • Шалгасан: frontend/tests/return-to.test.ts, vitest 80/80 файл, tsc --noEmit.
  • Үлдсэн: origin-ийн зөвшөөрлийн жагсаалт байхгүй — мөр хаашаа очихоо host-оор нь бичдэг.

Fixed — Registry-ийн каталог шахмал модулийн хувилбар зөрөхөд бүхэлдээ хаягддаггүй (2026-10-24)

  • Юу: registry (эсвэл түүний disk cache)-ээс ирсэн гарын үсэгтэй каталог дээр bundled catalog/apps.json-ийг давхарлана: энэ binary-д байгаа апп бүрийг өөрийн файлаар нь, registry зөвхөн binary-д БАЙХГҮЙ аппуудыг (external апп, бусад deployment-ийн модуль) нэмнэ. Bundled slug-ийг өөр id-аар эзэлсэн registry бичлэгийг хаяна.
  • Яагаад: нэг store бүх deployment-д нэг каталог өгдөг, харин нэг модуль (жишээ нь contract) deployment бүрт өөр хувилбараар шахагдсан байдаг. VerifyCatalogVersions хувилбар зөрөхөд каталогийг бүхэлд нь буцааж bundled файл руу унадаг байсан тул external аппууд алга болдог байв. Шахмал модулийг каталог шинэчилж чадахгүй — зөвхөн шинэ binary.
  • Хаана: backend/internal/kernel/appcatalog/verify.go (overBundled, parseSigned), тестүүд source_test.go, replay_test.go, golden_test.go.
  • Шалгасан: go test ./internal/kernel/appcatalog ./internal/workspace/appinstall, шинэ TestABundledAppIsDescribedByItsFileNotTheRegistry.

Fixed — Хуучин /cp холбоос админ консолын хост руу шилжинэ (2026-10-23)

  • Юу: CONTROL_PLANE_HOST тохируулсан үед өөр хост дээрх /cp, /cp/… хуудас 404 биш, консолын хост руу 308-аар шилжинэ (зам, query хэвээр; https, порт хасна — localhost, *.localhost дээр scheme, порт хэвээр). Консолын API (/api/platform/v1/…) өөр хост дээр хаалттай хэвээр (HostGate), /cpx мэт зам ч хөндөгдөхгүй.
  • Яагаад: консол өөрийн хост руу (admin.nexus.gerege.mn) нүүсний дараа nexus.gerege.mn/cp хавчуурга, холбоосоор орсон админ 404 харж, нэвтэрч чадахгүй байв.
  • Хаана: frontend/lib/controlPlaneHost.mjs ("elsewhere" шийдвэр, consoleAddress), frontend/proxy.ts, frontend/tests/control-plane-host.test.mjs, frontend/scripts/check-control-plane-host.mjs.
  • Шалгасан: Node 22 дээр control-plane-host.test.mjs 5/5, npm run host:check, tsc --noEmit, eslint цэвэр; build хийсэн Next дээр npm run host:smoke (nexus.localhost/cp/tenants?page=2 → 308 http://admin.localhost:3137/cp/tenants?page=2, /cpx → 404).

[1.49.0] - 2026-10-23

Added — Мэдэгдэл утас руу: APNs илгээгч (2026-10-23)

  • Юу: nexus.Notify-ээр бичсэн мэдэгдэл бүр хонх, мэйлээс гадна хүний бүртгүүлсэн iPhone руу push болж очно. APNS_KEY_PATH, APNS_KEY_ID, APNS_TEAM_ID (гурвуулаа эсвэл юу ч биш) тохируулаагүй бол унтраалттай — лог «APNs is not configured». 10 секунд тутам 50 хүртэлх мөрийг push_state='claimed' болгож аваад (мэйлийн хэлбэр) хүний бүх APNs токен руу илгээнэ: ES256 JWT (40 мин дахин ашиглана), HTTP/2, production эхлээд, BadDeviceToken бол sandbox нэг удаа (Xcode-ийн build). 410/Unregistered/ DeviceTokenNotForTopic, эсвэл хоёр хост хоёулаа BadDeviceToken → токеныг устгана. Payload: aps.alert (хүний locale, эс бол mn), sound, thread-id = kind, мөн kind, link; 4 KB-аас урт body таслагдана. Нэг цагаас хуучин мэдэгдэл push болохгүй; бүртгэл устгахаар товлогдсон хүнд илгээхгүй. DELETE /api/v1/push-tokens {token} — гарахдаа утас өөрийн токеныг устгана (зөвхөн өөрийн user_id-тай мөр, үргэлж 204, push_token.removed audit).
  • Яагаад: токенууд 00040-өөс бүртгэгдэж байсан ч илгээгч байгаагүй. Gerege App-ийн хэтэвчид «мөнгө орлоо» мэдэгдэл хэрэгтэй; утас өөр хүнд шилжихэд өмнөх эзний мэдээ гарах ёсгүй.
  • Хаана: migration 00159_a_phone_hears_it_too.sql (push_state, push_sent_at; хуучин мөрүүд none, шинэ нь pending), internal/workspace/pushtokens/apns.go, deliver.go, pushtokens.go (DELETE), pkg/host/server.go (StartDelivery), testdata/routes.txt, route_chains.txt, .env.example.
  • Шалгасан: apns_test.go — HTTP/2 хуурамч APNs JWT-ийн гарын үсгийг нийтийн түлхүүрээр шалгана; илгээх үр дүн (production, sandbox fallback, 410, хоёр хост BadDeviceToken, 503 токеныг үлдээнэ); payload 4 KB; токен шифр. deliver_db_test.go — мэдэгдэл хүний хэлээр хоёр токен руу, Unregistered токен устгагдана, дахин илгээгдэхгүй, 2 цагийн өмнөх нь үлдэнэ, утасгүй → none, устгагдаж буй бүртгэл → none; DELETE нь зөвхөн эзний токеныг.
  • Үлдсэн: FCM (Android) илгээгч; хүн push-ийг төрлөөр нь хаах (мэйлийн mute-тэй адил).

[1.48.0] - 2026-10-23

Added — Утас нэвтэрсэн хэвээр үлдэнэ: native клиентийн device grant (2026-10-23)

  • Юу: native клиент шинэхэн (≤10 мин, хүчтэй аргаар) нэвтэрсний дараа POST /api/v1/profile/device-grants {device_name?} → {refresh_token, expires_at} авна; POST /api/v1/auth/device-grants/refresh {refresh_token} → шинэ session (cookie + expires_at) ба дараагийн refresh_token; POST /api/v1/auth/device-grants/revoke {refresh_token} → гэр бүлийг нь хаана (үргэлж 204). Token бүр нэг удаа: ашиглагдсан token дахин ирвэл бүхэл гэр бүл хүчингүй (auth.device_grant_reused audit). Ашиглаагүй 90 хоногт, анхны нэвтрэлтээс хойш 365 хоногт дуусна. Сэргээсэн session нь auth_method = 'device' — StrongSignInMethods-д ороогүй тул passkey нэмэх, шинэ grant авах зэрэг step-up-д хүрэхгүй. «Бүх газраас гарах», нууц үг солих, сэргээх нь grant-уудыг session-тэй хамт хаана (revokeIn). Бүртгэлээ устгахаар товлоход (1.47.6) хүний бүх grant хаагдах ба refresh нь товлогдсон хүнд татгалзана — чимээгүй refresh-ийн session нь sign_in_keeps_the_account-аар устгалыг цуцлахгүй.
  • Яагаад: session 12 цаг (тогтмол) / 90 мин idle-д дуусдаг тул Gerege App-ийн iOS клиент өдөрт хэд хэдэн удаа eID-ээр бүтэн дахин нэвтрүүлдэг байв. Face ID-ийн түгжээ нь зөвхөн дэлгэцийг нээдэг, серверийн session-д хүрдэггүй.
  • Хаана: migration 00158_a_phone_stays_signed_in.sql (registry.device_grants, grant-гүй, 00138-ийн адил), backend/internal/workspace/auth/device_grant.go, session_revoke.go, session_housekeeping.go (хугацаа дууссаныг 7 хоногийн дараа, ашиглагдсаныг хугацаа нь дуусах хүртэл — дахин ашиглалтын нотолгоо), identity/routes.go, telemetry.LoginDevice, pkg/host/route_policy_test.go (хоёр public route), testdata/routes.txt, route_chains.txt.
  • Шалгасан: device_grant_db_test.go — олгох → сэргээх (session device) → дахин → ашиглагдсан token гэр бүлийг хаана; хүчингүй болгох; device болон 1 цагийн өмнөх session grant авч чадахгүй; хугацаа дууссан; RevokeAllForUser. Schema-ийн тоо, эзэмшил, tenant role-д хаалттай байх тестүүд.
  • Үлдсэн: Secure Enclave түлхүүрээр төхөөрөмжид уях (proof-of-possession) — одоогоор token нь Keychain-ийн ThisDeviceOnly-д хадгалагдах ба rotation + reuse detection-оор хамгаалагдана. Профайлын «нээлттэй session»-д grant-уудыг жагсааж тус бүрийг хаах дэлгэц. Клиент нэг зэрэг хоёр refresh илгээвэл хоёр дахь нь дахин ашиглалт гэж уншигдана — клиент refresh-ээ нэг урсгалд (single-flight) хийх ёстой.

[1.47.6] - 2026-10-23

Added — Үүсгэгч байгууллагаа, хүн өөрийн бүртгэлээ устгана (2026-10-23)

  • Юу: Профайл → «Устгах» хэсэг. Өөрийн үүсгэсэн байгууллагыг (зөвхөн үүсгэгч, одоо ч админ) устгахад шууд түдгэлзэж, session-ууд хаагдан, 30 хоногийн дараа операторын sweep устгана; тэр хүртэл «Сэргээх». Операторын товлосон устгалыг үүсгэгч буцаахгүй. Бүртгэлээ устгахад бүх session хаагдаж, 30 хоногийн дараа хэрэглэгч ба хувийн гэр устна; тэр хооронд дахин нэвтэрвэл цуцлагдана. Ганц админ нь байгаа байгууллага байвал татгалзана (sole_admin).
  • Хаана: migration 00157_a_founder_and_a_person_can_leave.sql (registry.tenants.founded_by, registry.users.deletion_scheduled_at, schedule_/cancel_organisation_deletion, schedule_account_deletion, founded_organisations, sessions-ийн trigger; access_change_events.actor_user_id → ON DELETE SET NULL), backend/internal/person/leave.go, internal/operator/tenants/deletion.go, frontend/components/profile/LeaveSection.tsx. GET /api/v1/me/organisations/founded, POST|DELETE /api/v1/me/organisations/{id}/deletion, POST /api/v1/me/deletion.
  • Шалгасан: leave_db_test.go, tests/leave-section.test.tsx, migration tests, vitest 496/496.
  • Үлдсэн: 1.47.0–1.47.2-д нээгдсэн байгууллагын үүсгэгчийг нэг transaction-д үүссэн ганц админ гишүүнээр нөхсөн.

Fixed — Модулийн консолын API файл хүлээж авна (2026-10-23)

  • Юу: nexus.UploadRoutes модулийн консолын API (/api/platform/v1/apps/{id}/) POST-д файлын хязгаар (files.MaxBytes() + 1 MB) үйлчилнэ.
  • Яагаад: contract-mn-ий операторын сан 3 MB PDF-ийг /apps/mn.contract.contracts/ocr руу илгээхэд routing-оос өмнө 413 «request body is too large» авдаг байв (e-property.mn, contract.mn) — хязгаар зөвхөн UploadPrefix()-д.
  • Хаана: backend/pkg/host/server.go (requestBodyLimit).
  • Шалгасан: pkg/host/bodylimit_test.go (өөр модулийн консол, GET хэвээр 1 MB).

[1.47.5] - 2026-10-23

Fixed — Байгууллагын дэлгэц хувийн гэрээс нээгдэхэд байгууллагаа асууна (2026-10-23)

  • Юу: SSO клиентүүд, API gateway, App Publishing-ийн дэлгэц хувийн гэрт уясан session-оор нээгдвэл 403/«зөвхөн харах эрх» биш, байгууллага сонгуулна; ганц байгууллагатай бол өөрөө шилжиж, тэр хуудсандаа үлдэнэ.
  • Яагаад: нэвтрэлт бүр хувийн гэрт ордог (FirstTenantFor) тул шууд холбоос эсвэл BRAND_HOME_PATH-аар нэвтэрсний дараа «Танд энэ үйлдлийг хийх эрх алга» гарч, апп эвдэрсэн мэт харагддаг байв.
  • Хаана: frontend/lib/permissions.tsx (OrganisationOnly), components/TenantChoices.tsx (switchTo(id, stay)), app/sso-clients, app/module/{sso-clients,api-gateway,appstore}/layout.tsx.
  • Шалгасан: tests/organisation-only.test.tsx, vitest 494/494, tsc.

Added — Нэвтэрсний дараах хуудсыг deployment сонгоно (2026-10-23)

  • Юу: BRAND_HOME_PATH — нэвтэрсний дараа (login, hero-гийн eID/нууц үг, eID callback) болон landing-ийн «Платформ руу» товчнуудаас очих хуудас. Зөвхөн энэ хостын зам (safeReturnPath); хоосон бол өмнөх шигээ /profile, /my/dashboard, /apps.
  • Яагаад: нэг backend-ийг хоёр домэйн (appstore.gerege.mn, sso.gerege.mn) хуваалцаж, домэйн бүр өөрийн аппын дэлгэц рүү оруулна.
  • Хаана: frontend/lib/brand.ts, lib/brandEnv.ts, components/landing/{SiteHeader,Hero,CallToAction,HeroSignIn}.tsx, app/login/page.tsx, app/auth/eid/callback/EIDCallback.tsx, .env.example, docker-compose.prod.yml.
  • Шалгасан: tests/landing-docs-link.test.tsx (open redirect-ийг татгалзана), vitest 489/489, tsc.

Added — Апп нийтлэлтийн native дэлгэцүүд (2026-10-23)

  • Юу: io.gerege.nexus.appstore_registry (appstore-gerege-nexus)-ийн гурван дэлгэц энэ модонд буцаж ирэв: /module/appstore/publisher (профайл, апп бүртгэх/засах, хувилбар илгээх), /module/appstore/review (дараалал, manifest, түүх, нийтлэх/татгалзах, нийтлэгч баталгаажуулах), /module/appstore/registry (revision, түлхүүр, каталог, платформ ба хаалттай аппын эрх). sso_clients, api_gateway-тэй ижил: модуль нь distribution-д, дэлгэц нь энд.
  • Яагаад: 2026-08-21-нд (06118571) цөм эдгээр замыг мөнтөддөггүй байсан тул устсан; одоо модуль appstore-gerege-nexus-д амьд бөгөөд iframe-ээр биш shell-ийн өөрийн дэлгэцээр харагдах ёстой. Хувийн гэрт 403 ирвэл байгууллагаа сонгохыг хэлнэ.
  • Хаана: frontend/app/module/appstore/*, lib/api/appstore.ts, lib/i18n/{addons,apps}/appstore.ts, scripts/generate-icon-map.mjs (upload, list-checks).
  • Шалгасан: tests/appstore-screens.test.tsx, vitest 491/491, tsc, eslint.

[1.47.4] - 2026-10-23

Added — Стор нүүр хуудсандаа платформын hero-г хадгалж болно (2026-10-23)

  • Юу: LANDING_STOREFRONT=below тавьсан стор deployment нүүр хуудсандаа платформын hero (eID-ээр нэвтрэх) -г үлдээж, каталогийг түүний яг доор (Storefront-ийн өөрийн hero-гүйгээр) харуулна. Хоосон бол өмнөх шигээ каталог дангаараа нүүр хуудас.
  • Яагаад: appstore.gerege.mn-ийн зочид каталог үзэхээс гадна нэвтрэхээр ирдэг.
  • Хаана: frontend/lib/landing.ts (storefrontPlacementFromEnv), frontend/app/page.tsx, frontend/components/landing/Storefront.tsx, .env.example, docker-compose.prod.yml.
  • Шалгасан: tests/landing.test.ts, tsc, eslint.

Fixed — Хасагдсан core аппын суулгалтын мөр цэвэрлэгдэв (2026-10-23)

  • Юу: nexus.integrations ба nexus.devices-ийн байгууллага болон хүний суулгалтын мөрийг устгав.
  • Яагаад: хоёр апп 41085f05-д цөмөөс хасагдсан ч суулгалтын мөр нь үлдэж, backend startup бүрт «organisations have apps installed that this binary does not carry» ERROR бичдэг байв. Холбогчид Тохиргоо → Холболтууд-д, төхөөрөмжийн шугам бүрэн хасагдсан (00152).
  • Хаана: migration 00156_departed_core_apps_leave_their_installs.sql.
  • Шалгасан: локал migrate, go test ./db/migrations/.

[1.47.3] - 2026-10-23

Fixed — Бүртгэлтэй байгууллага «Нээх» гэж харагдана (2026-10-23)

  • Юу: нүүрний «eID-тэй холбоотой байгууллага» карт платформд аль хэдийн бүртгэлтэй байгууллагыг таньдаг болсон: гишүүн бол «Нээх» (тийш нь шилжинэ), гишүүн биш бол «Платформд бүртгэлтэй — админаас нь урилга хүснэ үү». «Бүртгүүлэх» товч зөвхөн бүртгэлгүй, ADMIN эрхтэйд. Татгалзал бүр код (register_taken, slug_taken, not_representative, no_eid, slug_invalid)-той, дэлгэц монголоор тайлбарлана. Бүртгүүлсний дараа карт шинэчлэгдэнэ.
  • Яагаад: бүртгэлтэй байгууллагад «Бүртгүүлэх» дарахад 409 ирж, «Энэ хооронд өөрчлөлт орсон байна» гэсэн буруу мессеж гардаг байв. Мөн tenant.self_founded audit мөр RLS-д татгалзагдаж бичигддэггүй байв (session хүний гэрт уясан, мөр шинэ байгууллагынх).
  • Хаана: migration 00155_a_representative_sees_what_is_registered.sql (registry.registered_organisations; found_organisation audit мөрөө өөрөө бичнэ), backend/internal/person/found.go, frontend/components/profile/OrganisationsSection.tsx, frontend/lib/apiError.ts.
  • Шалгасан: found_organisation_test.go (tenant role-оор audit мөр бичигдэнэ, гишүүн биш хүнд workspace id өгөхгүй), found_db_test.go (бүртгэлтэй/гишүүн тэмдэг, хоёр дахь төлөөлөгч дахин нээж чадахгүй), me-dashboard.test.tsx.
  • Үлдсэн: 1.47.0–1.47.2-д нээгдсэн байгууллагын tenant.self_founded мөр алга (log-д AUDIT_EVENT мөр байгаа).

[1.47.2] - 2026-10-23

Fixed — Федерацлагдсан SSO eID иргэнийг Gerege дугаараар нь таньна (2026-10-23)

  • Юу: SSO client (SSO_CLIENT_ISSUER) (issuer, subject)-оор олдоогүй бол registry.users.ge_id-ийг provider-ийн ge_id claim-тай тулгаж, тохирсон бүртгэлийг холбож нэвтрүүлнэ. Тухайн issuer өөр subject-ээр аль хэдийн холбогдсон бүртгэлийг авахгүй.
  • Яагаад: eID бүртгэлийн хаяг хоёр талд <ge_id>@gemail.com, email_verified=false тул хаягаар тулгах алхам олдоггүй байв — private горимд no_account, public-д давхар @identity.invalid бүртгэл (e-property.mn → contract.mn SSO).
  • Хаана: backend/internal/workspace/identity/sso_accounts.go.
  • Шалгасан: sso_geid_db_test.go — private горимд eID бүртгэл ge_id-ээр олдож холбогдоно; өөр subject авахгүй; засваргүйгээр тест унадаг нь шалгагдсан.

[1.47.1] - 2026-10-23

Fixed — eID-тэй холбоотой байгууллагууд нүүрний тойм дээр харагдана (2026-10-23)

  • Юу: /my/dashboard-ийн тойм дээр «eID-тэй холбоотой байгууллага» карт — eID-д таныг төлөөлөгч гэж бүртгэсэн байгууллагууд, ADMIN эрхтэй нь бүрд «Бүртгүүлэх» товч. Төлөөлөлгүй эсвэл eID холбоогүй бол карт гарахгүй. Бүртгүүлэх цонхны хаяг (богино нэр) анхдагчаар регистрийн дугаар (өөрчилж болно).
  • Яагаад: 1.47.0-д товч «Бүртгэл» табын хамгийн доор байсан, eID-ээр нэвтэрсэн хүн тойм дээр буудаг тул байгууллагуудаа хардаггүй байв.
  • Хаана: frontend/components/profile/OrganisationsSection.tsx (RepresentationsCard), frontend/components/home/PersonHome.tsx.
  • Шалгасан: frontend/tests/me-dashboard.test.tsx — хоёр байгууллага гарч, зөвхөн ADMIN-д товч, дарахад тэр байгууллага сонгогдсон цонх нээгдэнэ.

[1.47.0] - 2026-10-23

Added — Байгууллагын төлөөлөгч байгууллагаа өөрөө бүртгүүлнэ (2026-10-23)

  • Юу: eID-ээр нэвтэрсэн хүн гэрийн дэлгэцийн «Байгууллага бүртгүүлэх» товчоор eID-д төлөөлөгч гэж бүртгэгдсэн байгууллагуудаа харж, ADMIN эрхтэй, хүчинтэй байгаа байгууллагаа хаяг (slug) сонгоод нээнэ — тэр байгууллагын анхны админ болно. API: GET /api/v1/me/representations, POST /api/v1/me/organisations. Шинэ байгууллагын audit-д tenant.self_founded мөр бичигдэнэ.
  • Яагаад: байгууллага зөвхөн консолоос (оператор) үүсдэг байсан. eID нь иргэн аль хуулийн этгээдийг төлөөлөхийг аль хэдийн хэлдэг (байгууллагын нэрийн өмнөөс гарын үсэг зурах зам үүнийг ашигладаг) — операторын гараар хийх шалгалтыг улсын бүртгэл хийчихсэн.
  • Хаана: migration 00154_a_representative_opens_their_organisation.sql (registry.found_organisation, SECURITY DEFINER, session-ий өөрийн хүнээр хязгаарлагдана; байгууллага, профайл, гишүүнчлэл, admin үүрэг нэг transaction-д), backend/internal/person/found.go, backend/pkg/host/server.go (eID-ийн төлөөллийг port-оор дамжуулна), frontend/components/profile/OrganisationsSection.tsx.
  • Хамгаалалт: нотолгоог сервер нээх бүрд eID-ээс дахин асууна, клиент зөвхөн аль байгууллагыг сонгосныг илгээнэ. MANAGER эрх, хугацаа дууссан эрх хүрэхгүй. Нэг регистр өөрөө нээх замаар нэг л удаа нээгдэнэ (advisory lock + шалгалт; консолоор үүссэн байгууллагууд регистр давхардсан байж болох тул unique index тавиагүй).
  • Шалгасан: db/migrations/found_organisation_test.go (өөр хүний нэрээр нээж болохгүй, үүсгэгч admin, давхар регистр), internal/person/found_db_test.go (хуурамч eID-тэй: ADMIN нээнэ, MANAGER, танихгүй регистр, давхар, буруу slug татгалзана), found_test.go (эрх, хугацаа, регистрийн тааралт).
  • Үлдсэн: private access mode-д бүртгэлгүй хүн нэвтэрч чадахгүй тул энэ дэлгэцэнд хүрэхгүй; апп автоматаар суулгахгүй (админ store-оос суулгана); операторын унтраах тохиргоо алга.

[1.46.0] - 2026-10-23

Added — Mini app: хөгжүүлэгчийн sandbox, аналитик, операторын хязгаар ба нүүрний layout (2026-10-22)

  • Юу: хөгжүүлэгчийн консолд (/module/miniapp/apps/{id}) Webhook илгээлтүүдийн жагсаалт (шүүлт, payload, дахин илгээх, тест event), «Тест хэрэглэгчид» (байгууллагын гишүүнийг имэйлээр), «Тест төлбөр» (OMAS тест дүн …01/…02), «Аналитик» (7/30/90 хоног, live/тест, өдөр бүрийн график, хүснэгт) табууд; байгууллагын нийт аналитик /module/miniapp/apps/analytics. Операторт нэг төлбөрийн дээд дүн («Хязгаар») ба /module/miniapp/review/layout — хувилбартай нүүрний layout (featured, grid, category, categories), нийтлэх, буцаах, суваг бүрээр урьдчилан харах. Хүний launcher нийтлэгдсэн layout-той бол «Нүүр» табаар нээгдэнэ; layout биш хариу (404, алдаа, өөр хэлбэр) ирвэл хүлээлгүй хуучин жагсаалтаа харуулна.
  • Яагаад: distribution-ий mn.gerege.miniapp 1.2.0 (app-gerege-nexus) эдгээрийн API-г нэмсэн; дэлгэц нь бүрхүүлд байх ёстой. Хуучин модуль шинэ route-д 404 өгвөл «Энэ боломж одоогоор алга» гэж харуулна.
  • Хаана: frontend/lib/api/miniappConsole.ts, frontend/components/miniapp/{analytics,sandbox,layoutEditor}.ts, components/miniapp/dev/*, components/miniapp/review/*, components/miniapp/person/MiniApps.tsx, app/module/miniapp/apps/analytics, app/module/miniapp/review/layout, lib/i18n/addons/miniapp.ts (mn, en). PR #267.
  • Шалгасан: tsc, lint, vitest (487), e2e (launcher-ийн layout «Нүүр» шинэ тест), next build.
  • Үлдсэн: layout-ыг чирж зөөх, хадгалаагүй ноорог урьдчилан харах, payout тохиргооны дэлгэц, шинэ дэлгэцүүдийн e2e.

Changed — Нүүр хуудасны гол товч нь eID-ээр нэвтрэх (2026-10-22)

  • Юу: landing-ийн hero дээр «Эхлэх»-ийн оронд eID-ээр нэвтрэх товч шууд байна; нууц үг идэвхтэй суулгацад доор нь «Нууц үгээр нэвтрэх» (цонхонд маягт нээгдэнэ). Толгойн «Эхлэх» товч хасагдсан, «Нэвтрэх» үлдсэн.

Changed — main руу merge хийсний дараах CI, deploy хурдан (2026-10-22)

  • Юу: main-д merge хийгдсэн код PR дээр CI давсан commit-тэй яг ижил (tree) бол Lint, Test, Frontend Build дахин ажиллахгүй, зөвхөн image build хийгдэнэ (ci.yml, «Already tested»). Deploy-ийн preflight нь өөрийг нь эхлүүлсэн CI-ийн мэдээлэлд итгэнэ — API хоцорсноос «no successful main CI run» гэж буруу унах (#314) алга.

[1.45.1] - 2026-10-22

Fixed — Хуучин /me хаяг HTTP-ээр шилжинэ (2026-10-22)

  • Юу: /me, /me/dashboard, /me/profile нь хуудас ачаалахаас өмнө 307-оор /my/... руу шилжинэ (query хадгалагдана). Өмнө нь хөтөч доторх шилжүүлэлт байсан тул нэвтрээгүй хүн хуучин хаягаар ороход нэвтрэх хуудас биш «Нэвтрэлт дууссан» алдаа харагддаг байв.

[1.45.0] - 2026-10-22

Changed — Гарын үсгийн нэршил: цахим ба тоон (2026-10-22)

  • Юу: гараар зурсан / зурган гарын үсгийг «Цахим гарын үсэг», eID (PIN2)-ээр зурсныг «Тоон гарын үсэг» гэж бүх текстэд нэгтгэв (eSign, Баримт бичиг, Ортоо, Хөгжүүлэгч, тайлан).

Removed — «Апп болгон суулгах» цонх (2026-10-22)

  • Юу: платформын өөрийн суулгах санал (баруун доод булангийн карт) хасагдсан; хөтөч өөрийн стандарт суулгах товч, санал болголтыг харуулна.

Changed — Хувийн гэр /my; консолын нүүр landing-ийн загвартай (2026-10-22)

  • Юу: хүний хувийн гэрийн хуудсууд /me/...-ээс /my/... руу шилжсэн (/my/dashboard); хуучин /me/... хаяг зам, query-тайгаа /my/... руу шилжинэ. API (/api/v1/me/...) өөрчлөгдөөгүй.
  • Консол: нэвтрэх нүүр хуудас нийтийн landing-ийн загвартай — голлосон hero, гол товч нь «eID-ээр нэвтрэх», «Нууц үгээр нэвтрэх» дарахад маягт нээгдэнэ; хэсгүүд landing-ийн картаар; текстийг цэвэр монгол хэлээр засав.

Added — Байгууллагын элсэх QR ба нэмэлт мэдээллийн маягт (2026-10-22)

  • Юу: байгууллага бүр нэг тогтмол QR-тэй (/join/<slug>): Байгууллага → Профайл дээр QR харагдана, холбоосыг хуулах, PNG татах боломжтой. Мөн тэнд eID өгч чадахгүй мэдээллийн маягт тохируулна — 20 хүртэлх асуулт, төрөл нь текст / урт текст / утас / и-мэйл / огноо / тоо / сонголт, заавал эсэх. QR уншуулсан хүн нийтийн хуудсанд байгууллагын нэр, асуултыг харж, eID-ээр нэвтэрч (next буцаана), маягтаа бөглөөд элсэх хүсэлт илгээнэ. Хариулт хүсэлтийн хамт Тохиргоо → Хандалт → Хүсэлт дээр админд асуултын шошготой харагдана. Элсэх хүсэлтийг админ батлах эсвэл автоматаар батлагдах (join_policy: on_request / open) сонголт QR, маягттай нэг картад шилжсэн (өмнөх тусдаа «Хаалга» карт хасагдсан); автоматаар бол хүн шууд гишүүн болж, хариулт тэр мөрөнд хадгалагдана.
  • Яагаад: 00089/00104-ийн элсэх хүсэлт хүн slug-ийг мэддэг гэж үзсэн; байгууллага ажилтан, гишүүдээ бүртгэхэд хананд наах хаяг болон албан тушаал, хэлтэс зэрэг eID-д байхгүй мэдээлэл дутуу байв.
  • Хаана: migration 00153_a_door_with_a_form.sql (registry.tenants.join_fields, workspace.join_requests.answers, registry.answer_join_request, gerege_nexus_public-д the_door_is_public — зөвхөн байгууллагын мөр, зөвхөн хаалганы баганууд); internal/kernel/joinform (маягт шалгах, хоёр plane ашиглана); internal/person (GET /api/v1/join/{slug} нийтийн, POST /api/v1/me/join-requests-д answers); internal/workspace/profile (join_fields унших/хадгалах); internal/workspace/access (дараалалд хариулт); frontend/app/join/[slug], components/organisation/JoinDoor.tsx, app/settings/access, lib/i18n/addons/join.ts.
  • Шалгасан: joinform unit тест; the_door_is_public нийтийн role-оор (байгууллага уншигдана, хувийн орон зай, бусад багана, бичилт хаалттай); person DB тест (хаалга маягтаа харуулна, хариулт хүсэлттэй хамт, дахин асуухад солигдоно); route policy + golden; backend бүх тест (DB-тэй), frontend tsc, lint, vitest.
  • Үлдсэн: join.* түлхүүрүүд mn/en л (бусад хэл англи руу унана). Хариулт засах, хүсэлтээ буцаах дэлгэц алга.

[1.44.0] - 2026-10-22

Changed — Консол: Платформын эрүүл мэнд ба тохиргооны картууд хянах самбарын загвартай (2026-10-22)

  • Юу: «Платформын эрүүл мэнд» тоон картууд, хэсгийн толгойтой; тохиргоо ба түлхүүрийн картууд байгууллагын Холболтуудын адил (төлөв баруун дээд буланд, доор нь бүтэн өргөнтэй товч).

Fixed — eID-ээр PDF-д гарын үсэг дахин ажиллана: client-side PAdES (2026-10-22)

  • Юу: nexus.Signer.SignDocument → PollSignature → SignedDocument, nexus.OrganisationSigner (PDF), /esign модуль бүгд internal/kernel/eidsign-ээр явдаг; PIN2-ын дараа гарын үсэгтэй PDF буцаахаа больсон байв. Одоо PDF eID-д очихгүй: (1) SignDocument иргэний утсанд гарын үсгийн гэрчилгээ сонгуулна (certificate choice, PIN-гүй); (2) сонголт ирэхэд pades.Prepare, DTBS-ийг ЯГ тэр төхөөрөмж рүү PIN2-оор (/signature/notification/linked/{documentNumber}); (3) гарын үсгийг өмнөх шигээ eidrp.VerifySignedDigest-ээр (нэвтрэлттэй ижил trust store, DTBS-ийн эсрэг) + гэрчилгээ нь сонгосонтой ижил байх; (4) SignedDocument / StampSigned дээр pades.Complete (TSA + OCSP/CRL → PAdES-BASELINE-LT) ба eID-д бүртгэл (POST /pdf/signatures). Дуудагчид нэг session — PollSignature хоёр алхмын турш running; pkg/nexus-ийн гэрээ өөрчлөгдөөгүй. Байгууллагын нэрийн өмнөөс бол eID-ийн claimedRole гарын үсэгт, onBehalfOf PIN2-ын session-д.
  • Яагаад: eID Mongolia POST /signature/stamp/{id}-ийг хассан (rp.eidmongolia.mn болон бусад хост 404, RP-ийн mTLS + bearer-ээр шалгасан). Орлуулга embedPAdES production-д Document-Signer-гүй тул унаж байв: eidsign: eID could not stamp the document; embedding our own signature as a stand-in ... stamp answered 404.
  • Хассан: stamp-ийн зам (stampV3), Document-Signer-ийн орлуулга (embedPAdES, signer.go, DownloadResult.Fallback, Config.SignerCertPEM/SignerKeyPEM/IsProduction), EID_SIGN_SIGNER_CERT_PEM/_KEY_PEM (.env.example, docker-compose.prod.yml) — тохируулсан утга нөлөөгүй. Шууд digitorus/pdfsign хамаарал.
  • Хугацаа: eID state-ийн TTL eID-ийн session-ий expiresAt + 30 мин (тогтмол 30 мин биш). /esign-ийн хүлээлтийн sweep 20 → 70 мин (хоёр session, CA-ийн дээд 1800с тус бүр).
  • Хэмжээ: maxPDFBytes = pades.MaxInputBytes (10 223 616); frontend-ийн хязгаар мөн.
  • Distribution-д: цөм eid-mongolia-sdk-go v0.15.2-г шууд хэрэглэх болсон тул цөмийг ашигладаг distribution бүрийн go.mod-д replace github.com/digitorus/pdf => github.com/craftzbay/pdf v0.1.3-eid.2 заавал (байхгүй бол rdr.Resolve undefined). distribution-ci.yml одоо зөвхөн цөмийн replace-ийг татгалзана. e-notary-mn ба contract-mn-ийг энэ branch-аар (replace нэмж) build хийж шалгасан.

Хаана: backend/internal/kernel/eidsign/{eidsign,pades,ca,state,pdf}.go (+pades_test.go, verify_test.go, eidsign_test.go, bounded_test.go), signer.go устсан, backend/internal/workspace/identity/eidmongolia/eidmongolia.go, backend/internal/workspace/signing/{domain,stamping}.go, backend/go.mod, .github/workflows/distribution-ci.yml, frontend/components/esign/EidSignView.tsx, docs/SIGNING.md, docs/OPERATIONS.md, docs/DISTRIBUTION.md.

Шалгасан: go build ./..., go vet ./..., go test ./..., go test -race (eidsign, identity, signing), GOOS=linux GOARCH=amd64 go build ./.... Шинэ тестүүд (хуурамч eID + pades.Prepare/Complete-ийн орлуулга): гэрчилгээ сонголт → тэр төхөөрөмж рүү linked PIN2 → completed → PDF; өөр гэрчилгээний гарын үсэг → failed; TSA унавал ErrStampUnavailable, дараа нь PIN2-гүйгээр ижил гарын үсэг; бүртгэл татгалзвал PDF өгөгдөж дараагийн татахад дахин бүртгэнэ; claimedRole + onBehalfOf; угсрагдахгүй PDF PIN2-оос өмнө failed; TTL нь eID-ийн expiresAt-ийг дагана.

Үлдсэн: бодит PIN2-оор (production eID) шалгаагүй. PIN2-ын алхмын шинэ verification code PollSignature-аар харагдахгүй (утсан дээр л). Deploy-ийн өмнө эхэлж stamp хүлээж байсан ёслол (eID гарын үсэг нь баримтын SHA-256 дээр) PAdES болж чадахгүй — failed. eID-ийн бүртгэл амжилтгүй бол зөвхөн дахин татахад л дахин оролдоно.

Added — Холбогч бүрийг байгууллагад харуулах эсэхийг консолоос (2026-10-22)

  • Юу: Админ консол → Тохиргоо → Холболтууд дээр холбогч бүрд «байгууллагад харуулах» унтраалга (connector.<provider>_shown, анхдагч асаалттай, env CONNECTOR_<PROVIDER>_SHOWN). Унтраасан холбогч байгууллагын Холболтууд дээр харагдахгүй, шинээр бүртгэх API ч татгалзана.

[1.43.0] - 2026-10-22

Changed — Webhook, Төрийн гарц, Төлбөрийн гарц, Захиалгат REST платформын холболт болсон (2026-10-22)

  • Юу: эдгээр дөрвийн хаяг, түлхүүрийг оператор Админ консол → Тохиргоо → Холболтууд дээр нэг удаа оруулна (connector.* түлхүүрүүд, env CONNECTOR_*); бүх байгууллага тэрийг ашиглана. Байгууллагын Холболтууд дээр «Платформоос» гэж төлөв, хаягтайгаа харагдана; байгууллага өөрийн хувилбарыг нэмэхгүй (POST /integrations татгалзана). Хуучин бүртгэсэн мөрүүд хэвээр.

Changed — Байгууллагын хянах самбар табтай; «Профайл» цэснээс хасагдсан (2026-10-22)

  • Юу: /organisation/dashboard хүний хянах самбарын загвартай — Тойм (байгууллагын том карт, гишүүд, идэвхтэй апп, регистр, 7 хоногийн үйлдэл), Үйл явдал, Профайл. Байгууллагын бүртгэлийг засах дэлгэц Профайл таб болж, цэснээс хасагдсан; /organisation → /organisation/dashboard?tab=profile.

Added — Байгууллагын эхний админыг eID QR уншуулж нэмэх (2026-10-22)

  • Юу: «Шинэ байгууллага» цонхонд «eID-ээр уншуулах»: консол eID QR код гаргана, админ болох хүн өөрийн утасны eID аппаар уншуулж баталгаажуулмагц платформын хүн болж бүртгэгдэн (eID нь холбогдоно) админаар сонгогдоно. Консолд eID холбогдсон үед л харагдана.
  • API: POST /api/platform/v1/people/eid (tenant.create эрхтэй), консолын eID хаалгаар эхэлсэн session-ийг зөвхөн эхлүүлсэн хөтөч нэг удаа дуусгана (operator.ProveEID); audit person.create.eid.

Changed — Хянах самбар: Профайл ба Миний eID таб болж, цэснээс хасагдсан (2026-10-22)

  • Юу: хүний хянах самбарын «Бүртгэл» таб «Профайл» болж, профайлын бүх хэсэг (таних тэмдэг, нэвтрэх нэр ба нууц үг, passkey, 2FA, байгууллага) тэнд орсон; ард нь «Миний eID» таб (eID CA-ийн бүртгэл). Цэснээс «Профайл», «Миний eID» хасагдсан; /me/profile → /me/dashboard?tab=profile. Самбарын саарал дэвсгэр, лого хасагдаж ажлын талбайг бүтэн дүүргэнэ.
  • Хаана: frontend/components/home/PersonHome.tsx, cards.tsx, app/me/dashboard, app/me/profile; backend/internal/workspace/menu/menu.go.

Changed — Консолын хянах самбар табтай; ажлын талбайн дэвсгэр тохиргоонд (2026-10-22)

  • Консол: /cp/profile хүний хянах самбарын загвартай болсон — Тойм (дансны том карт, сүүлийн нэвтрэлт, eID, эрх, 7 хоногийн үйлдэл), Үйл явдал, Профайл табууд. «Профайл» (/cp/profile/account) цэснээс хасагдаж Профайл таб болсон; хуучин хаяг таб руу шилжинэ.
  • Харагдац: «Ажлын талбайн дэвсгэр» — Цагаан / Саарал. Тохиргоо → Харагдац дээр, консолд хэрэглэгчийн цэсэнд. Анхдагч нь саарал; хөтөч тус бүрд хадгалагдана (gerege_theme).

Changed — Холболтууд: үйлчилгээ бүр карт, тохируулаагүй шалтгаан ил (2026-10-22)

  • Юу: Тохиргоо → Холболтууд дээр холбох боломжтой үйлчилгээ бүр карт болж («Бэлэн» / «Тохируулаагүй», тохируулсан тоо, «Нэмэх» нь тэр үйлчилгээгээр урьдчилан сонгогдсон маягт нээнэ). Google Drive, Google Meet, Dropbox нь платформын OAuth client байхгүй бол яагаад, хаана (Админ консол → Тохиргоо → Холболтууд) тохируулахыг холбоостой хэлнэ. Консолын тайлбарт буцах хаяг жинхэнэ PUBLIC_ORIGIN-оор бичигдэнэ.

[1.42.0] - 2026-10-22

Removed — «Төхөөрөмж» апп ба төхөөрөмжийн шугам; «Холболтууд» Тохиргоонд (2026-10-22)

  • Юу: байгууллагын «Төхөөрөмж» апп бүрмөсөн хасагдсан: киоск/терминал бүртгэх, ажилтны PIN нэвтрэлт, телеметр, /settings/devices, DEVICES_ENABLED, SDK-ийн nexus.StaffCredential. Хүний утасны push token (POST /api/v1/push-tokens) хэвээр — internal/workspace/pushtokens.
  • Холболтууд: «Холболтууд» апп хасагдаж, холбогчдын дэлгэц (Google Drive, Dropbox, Google Meet, webhook) байгууллагын Тохиргоо → Холболтууд (/settings/integrations) болсон; зөвхөн админд харагдана. Цөмийн суулгадаг апп (coreapps.Core) үлдээгүй тул тэр механизм хасагдсан; хуучин суулгалтын мөр юу ч харуулахгүй.
  • Migration: 00152_the_device_line_leaves.sql — devices, device_enrollment_codes, staff_pin_credentials, device_telemetry хүснэгт, push_tokens.device_id багана, staff-pin сешнүүдийг устгана. Down нь хүснэгтүүдийг сэргээхгүй.

[1.41.0] - 2026-10-22

Changed — Нүүр хуудсыг шинээр зурж, зураг, бичвэрийг суулгацад өгөв (2026-10-22)

  • Юу. Нийтийн нүүр хуудас (frontend/components/landing/) шинэ дүр төрхтэй: дулаан цайвар canvas, төвд байрлах цэс («Нэвтрэх» + «Эхлэх» pill), hero-д хоёр мөр том гарчиг, нэг товч, гарт барьсан утас; доор нь bento карт, showcase (аппын дэлгэцүүд), гурван алхам, хаалтын урилга (cta), шинэ footer. Бүхэлдээ BrandAccent дотор — accent нь BRAND_THEME_COLOR. Хоёр горимд (app/landing.css, токеноос гаргасан canvas/ink, accent дээрх бичвэр ≥4.5:1).
  • Суулгацын зураг. LANDING_MEDIA_FILE (compose: /brand/landing/media.json) — hero, showcase, картын зураг, cta дэвсгэр; lib/landingMedia.ts хаяг, srcset, хэмжээг шалгана, муу slot нь анхдагч руу буцна. brand/ доторх зураг, фонт /brand/… хаягаар үйлчлэгдэнэ (app/brand/[...path]/route.ts, lib/brandAssets.ts: зөвхөн зураг/фонт, нуугдмал файл, symlink-ээр гадагш гарах, copy.json/media.json — бүгд 404; SVG sandbox CSP-тэй). Цөмийн анхдагч hero нь цөмийн өөрийн вэб хэтэвчийн бодит дэлгэц (public/landing/).
  • Бичвэр. Hero, showcase, алхам, cta, зургийн alt бүгд website.* түлхүүр — copy.json-оор дарна. Шинэ: website.hero.*, website.showcase.*, website.cta.*, website.media.*, website.tech.{eyebrow,title,lede,*_title}, website.action.get_started. Хасагдсан: website.view.hero_{eyebrow,title_*,lede}, website.stat.*, website.action.see_features (overlay-оос мөн).
  • Нэвтрэх карт. Hero-гийн байнгын нэвтрэх карт «Эхлэх» товчоор dialog болж нээгдэнэ (header-ийн «Эхлэх» мөн — START_EVENT); JavaScript-гүй бол /login холбоос. Federated суулгацад карт байхгүй, нэвтэрсэн хүнд «Платформ руу» / ирсэн гэрээ — өмнөх шигээ.
  • Баримт бичгийн холбоос. BRAND_DOCS_URL хоосон бол header, утасны цэс, footer-т «Баримт бичиг» огт гарахгүй — өмнө нь платформын хаалттай репо руу (олон нийтэд 404) унадаг байв. DEFAULT_BRAND.docsUrl хоосон болов; тохируулсан суулгацад өмнөх шигээ (tests/landing-docs-link.test.tsx).
  • Шинэ хэсгүүд. LANDING_SECTIONS-д showcase (зураггүй бол зурагдахгүй) ба cta нэмэгдэв; анхдагч жагсаалт … capabilities showcase technology cta.
  • Гүйцэтгэл. Hero зураг <picture> (AVIF → WebP), preload + fetchpriority=high, width/height-тай (CLS 0); зөвхөн transform-оор хөдөлнө (opacity биш — LCP хойшлохгүй). Доорх картууд animation-timeline: view()-оор; prefers-reduced-motion үед хөдөлгөөнгүй.
  • Шалгасан. tests/landing-media.test.ts (media parse, /brand хамгаалалт), tests/hero-sign-in.test.tsx (dialog, header event, federated), tests/e2e/landing.spec.ts (LCP зураг preload/ачаалалт, dialog, 375px хэвтээ халилтгүй, reduced motion, /brand 404).
  • Суулгацад. Шинэ image-ийг pin хийсний дараа brand/landing/ + media.json, copy.json-ийн түлхүүрүүд, LANDING_SECTIONS=hero capabilities showcase technology platform cta.

[1.40.0] - 2026-10-22

Changed — Анхны тохиргоо удирдлагын консолоос (2026-10-22)

  • Юу: анхны тохиргооны шидтэн (системийн шалгалт → эхний оператор → authenticator) одоо удирдлагын консолд (/cp) нэвтрэх маягтын оронд гарна; дуусмагц консолын нэвтрэлт рүү шилжинэ. Нийтийн сайт тохируулаагүй ч, нээгээгүй ч /opening («Тохиргоо хийгдэж байна») харуулна, «Сайтын админ бол Админ консол руугаа орж тохиргоогоо хийнэ үү» гэсэн сануулгатай. /setup хуудас хасагдсан.
  • API: шидтэнгийн алхмууд /api/platform/v1/setup/{status,checks,enrol,complete} болж консолын host/хаягийн хаалганы (operator.HostGate) ард орсон; нийтийн сайтад /api/v1/setup/status л үлдэв (console_url нэмэгдсэн).
  • Хаана: backend/internal/operator/setup/setup.go, internal/operator/service.go, pkg/host/server.go; frontend/components/cp/SetupWizard.tsx, components/cp/Console.tsx, app/opening, lib/cp.ts.

Changed — Хүний гэр /me/dashboard санхүүгийн тоймын бүтэцтэй боллоо (2026-10-22)

  • Юу: нэвтэрсний дараах гэр шинэ байршилтай: брэндийн лого + мэндчилгээ + хугацаа сонгогч, pill таб (Тойм / Гүйлгээ / Аппууд / Үйл явдал / Бүртгэл) ба «Шууд шинэчлэгдэнэ» (харагдаж байх үед минут тутам, таб руу буцахад дахин уншина; дарвал одоо), саарал canvas дээрх цагаан картууд. Хүн Gerege Wallet (bank) барьж байвал дээд хэсэг нь хэтэвчийн өөрийн картууд — нийт үлдэгдэл (нуух, валют, өөрчлөлт, 14 хоногийн bar, Хүлээн авах / Илгээх / Төлбөр нэхэх stacked sheet), орлого / зарлага / цэвэр дүн, сарын тайлангийн дэлгэсэн карт — GET /bank/me/summary ба данснаас; гүйлгээний таб. Хэтэвчгүй бол ижил grid-д бүртгэлийн картууд (хүн, байгууллага / таних тэмдэг / session, аппууд брэндийн өнгөн дээрх дэлгэсэн карт). Доор нь «Миний аппууд», mini апп (/miniapp/apps, аппын sheet-ээр нээнэ), eID ба нууц үг, сүүлийн гүйлгээ, сүүлийн үйлдэл, мэдэгдэл (уншсан болгох, холбоос руу очих). Хуучин самбарын зүйл хасагдаагүй: статистик, таних тэмдэг, байгууллага «Бүртгэл» табад, audit хүснэгт «Үйл явдал» табад. app/me/template.tsx — хуудас шилжилт; tab шилжилт useTabDirection, staggered appear, reduced-motion хүндэтгэнэ.
  • Яагаад: гэр нь хүний ирдэг газар, харин хэтэвчийн тойм зөвхөн /module/bank дотор байв. Мөн distribution бүрт ажиллах ёстой: дээд хэсгийг GET /person/apps шийддэг — хэтэвчгүй суулгац /bank руу хүсэлт илгээхгүй, хэтэвчийн код, толийг ч татахгүй (next/dynamic). 503 бол алдаа биш тайван мөр, 403 бол «өөртөө идэвхжүүлэх».
  • Хаана: frontend/components/home/ (шинэ), frontend/components/overview/parts.tsx (IconChip, PillAction, StatCard — хэтэвч ба mini апп-ын давхардсан IconChip-ийг нэгтгэв), frontend/components/bank/ (useWallet, WalletSheets, MoneyCards/RecentMovements/PeriodSelect — Wallet.tsx эдгээрийг ашиглана, үйлдэл өөрчлөгдөөгүй), frontend/components/profile/PersonApps.tsx (жагсаалтаа prop-оор авна), frontend/lib/personScreens.ts (holds, withScreens), frontend/lib/i18n/addons/home.ts (цөмийн толь, mn/en; auth.ts-д биш — тэр native клиент рүү экспортлогддог), MODULES «Хүний гэр». Backend, migration өөрчлөлтгүй.
  • Шалгасан: vitest me-dashboard (хэтэвчгүй үед /bank, /miniapp хүсэлтгүй; хэтэвчийн картууд ба гүйлгээний таб; 503 тайван мөр; 403 → идэвхжүүлэх; дүн нуух хэтэвчтэй нэг тохиргоо; mini апп sheet; мэдэгдэл → уншсан + холбоос; нууц үг; бүртгэл ба үйл явдлын таб; минутын шинэчлэлтийн алдаа дуугүй), Playwright dashboard.spec.ts (хүсэлтгүй, бодит accent, sheet-ийн доор гэр ухрах, табын чиглэл ба reduced-motion, 375px-д хажуу тийш гүйлгэхгүй); бүх vitest, e2e, tsc, lint, build ногоон.
  • Үлдсэн: шинэ home.* түлхүүрүүд mn/en л (бусад таван хэл англи руу унана — backlog-д нэмэгдэв).

Added — Mini апп платформын дэлгэц: хөгжүүлэгчийн консол, операторын хяналт, хүний жагсаалт (2026-10-22)

  • Юу: /module/miniapp/apps — хөгжүүлэгчийн консол (miniapp.read / miniapp.manage): байгууллагын mini апп-ууд төлвөөр (pill таб), шинэ апп бүртгэх sheet (slug давхцвал slug дээрээ хэлнэ), апп бүрийн засварлагч /module/miniapp/apps/[id] зургаан табтай — тохиргоо (зөвхөн өөрчилсөн талбарыг version-той PATCH хийнэ; 409 version_conflict бол шинэ хувилбарыг санал болгоно, дарж бичихгүй; entry URL, origin, scope, суваг, гадаад холбоос зөвхөн ноорог/татгалзсан үед), manifest (татаж шалгах, алдааг JSON pointer-оор, татсан баримт, энэ аппын бүртгэлээр бэлдсэн загвар, буулгасан ноорог schema-гаар шалгах), түлхүүр (test/live client_id; secret нэг л удаа stacked sheet-д — хуулах, «хадгалсан» гэж баталгаажуултал Escape-ээр ч хаагдахгүй; шинэчлэх нь хуучныг шууд хүчингүй болгоно гэж асууна), webhook (URL тавих бүрд шинэ нууц, нэг удаа), түүх, хяналт (submitIssues-ийг checklist болгож засах таб руу заана; 400 not_ready-ийн дутуу бүрийг нэг дор; батлагдсан апп засварын горимоо асааж унтраана). /module/miniapp/review — операторын дараалал (хянагдаж буй / батлагдсан / татгалзсан / түдгэлзүүлсэн), апп-ын дэлгэрэнгүй sheet дээр батлах (тэмдэглэл заавал биш), татгалзах ба түдгэлзүүлэх (шалтгаан заавал, 500 хүртэл) давхарласан sheet-ээр; өөр байгууллагад 403 → «Хяналт операторынх». /module/miniapp — хүний mini апп-ууд: батлагдсан апп ба өөрийн ноорог, аппын sheet (юу мэдэх, хууль зүйн холбоос), имэйл зэрэг зөвшөөрөл хэрэгтэй бол бүрхүүлийн өөрийн sheet-д асууна, нээхэд шинэ цонхыг товшилт дотор нөөцөлж opener-ийг тасална, хөтөч хаавал холбоос үлдээнэ; холболт салгах; 403 → өөртөө идэвхжүүлэх. lib/personScreens.ts-д miniapp нэмэгдсэн.
  • Цөмд: lib/api/client.ts-ийн request() татгалзсан хариуны бүх биеийг error.body-д үлдээдэг боллоо (details, version, scopes зэрэг) — урьд нь мессежээс бусдыг хаядаг тул дэлгэц «юу засах»-ыг хэлж чаддаггүй байв. Бусад дуудагчид өөрчлөлтгүй.
  • Яагаад: app-gerege-nexus mn.gerege.miniapp-ийг (OMAS 1.0-ийн порт) гаргасан ч бүрхүүлд дэлгэц нь байгаагүй — /module/miniapp/* «удахгүй» хуудас байв. openpay devportal-ийн UX-ийг цөмийн бүрэлдэхүүн, BrandAccent, components/motion-оор шилжүүлэв.
  • Хаана: frontend/app/module/miniapp/, frontend/components/miniapp/, frontend/lib/api/miniapp.ts, frontend/lib/i18n/addons/miniapp.ts, frontend/lib/i18n/apps/miniapp.ts, frontend/lib/personScreens.ts, MODULES «Аппын дэлгэц цөмийн бүрхүүлд». Backend, migration өөрчлөлтгүй.
  • Шалгасан: vitest (miniapp-form — PATCH зөвхөн өөрчлөлттэй, хоёр талбарын дүрэм, checklist, manifest загвар, татгалзлын код; miniapp-console — version conflict, түгжээ, secret нэг удаа, not_ready, slug_taken, эрхгүй бол товчгүй; miniapp-review — 403, шалтгаангүй татгалзалт хориотой; miniapp-person — зөвшөөрөл → нээлт, opener null, хаагдсан цонхонд холбоос, 428, засвартай), Playwright miniapp.spec.ts (бодит accent, PATCH-ийн бие {name, version}, secret sheet Escape-д хаагдахгүй, оператор биш 403, зөвшөөрлийн давхарласан sheet → popup ?launch= ба window.opener === null).
  • Үлдсэн: аппын толь mn/en л. Хүн mini апп-ыг «Миний аппууд»-аас олохын тулд дистрибуц miniapp-ийг DefaultPersonApps-д нэмэх эсэхээ шийднэ (одоогоор хүн өөрөө идэвхжүүлнэ). Модуль дээр операторт нэг апп уншдаг route байхгүй тул дэлгэрэнгүй нь жагсаалтын мөрөөс; webhook хүргэлт, төлбөр модулийн дараагийн хувилбарт.

Added — webcredentials in apple-app-site-association (2026-10-22)

  • Юу: /.well-known/apple-app-site-association нь applinks-ийн хажууд ижил APPLE_APP_IDS-ийг webcredentials.apps-д зарлана.
  • Яагаад: native iOS апп энэ host-ийн passkey-г (WebAuthn RP ID = вэб origin-ий host) ашиглахын тулд AASA-д webcredentials заавал байх ёстой; үгүй бол iOS «application is not associated with domain» гэж бүх passkey хүсэлтийг татгалзана.

[1.39.0] - 2026-10-22

Added — Gerege Wallet-ийн дэлгэц, аппын дэлгэцийн өнгө ба хөдөлгөөний хэв маяг (2026-10-22)

  • Юу: /module/bank — хүний хэтэвч: санхүүгийн тойм (нийт үлдэгдэл нуух/харуулах, өмнөх сартай харьцуулсан өөрчлөлт, 14 хоногийн баганан график, Хүлээн авах / Илгээх / Төлбөр нэхэх), орлого / зарлага / цэвэр дүн, сарын тайлангийн сэнс хэлбэртэй картууд, next_before-оор хуудаслах гүйлгээ, данс нээх, мөнгө илгээх (эхлээд хүлээн авагчийг шалгана; Idempotency-Key оролдлого бүрд нэг удаа үүсч, давтахад хэвээр). 503 → тайван хоосон төлөв, 403 → хэтэвчийг өөртөө идэвхжүүлэх. /module/bank/overview, /customers (данс царцаах / гаргахад шалтгаан заавал), /aml — банкны арын алба. /me/dashboard-д «Миний аппууд». components/BrandAccent (BRAND_THEME_COLOR = accent), components/motion (appear, PageTransition, useTabDirection, StackedSheet, useSheetHost, motion-press; app/app-screens.css).
  • Яагаад: app-gerege-nexus mn.gerege.bank-ийг гаргасан ч бүрхүүлд түүний дэлгэц байгаагүй — /module/bank/* нь «удахгүй» хуудас байв. Хүний цэс MenuPermission-тэй модулийг алгасдаг тул хэтэвч рүү орох зам ч байгаагүй.
  • Хаана: frontend/app/module/bank/, frontend/components/bank/, frontend/components/motion/, frontend/lib/api/bank.ts, frontend/lib/personScreens.ts, frontend/lib/i18n/addons/bank.ts, MODULES «Аппын дэлгэц цөмийн бүрхүүлд». Backend, migration өөрчлөлтгүй.
  • Шалгасан: vitest (bank-send — давталт ижил түлхүүр, өөрчлөлт шинэ түлхүүр; bank-wallet — 503/403; bank-office — шалтгаангүй царцаалт хориотой, bank.manage-гүй бол товчгүй; brand-accent, person-screens, me-dashboard), Playwright wallet.spec.ts (бодит accent өнгө, давхарласан sheet-ийн transform, reduced-motion үед хөдөлгөөнгүй, Idempotency-Key header).
  • Үлдсэн: аппын толь mn/en л (бусад хэл англи руу унана). Төлбөр нэхэх нь модульд route байхгүй тул мессеж бичиж хуваалцдаг.

[1.38.1] - 2026-10-22

Fixed — release workflow 1.38.0-г нийтэлж чадаагүй (2026-10-22)

  • Юу: release.yml SDK-ийн тестийг (go test ./pkg/nexus/...) өгөгдлийн сангийн migration-аас өмнө ажиллуулдаг байсан тул nexus.PublicRead-ийн DB тест хоосон схем дээр унаж, v1.38.0-ийн GitHub release гараагүй. Migration-ыг түрүүлж ажиллуулдаг болгосон (ci.yml-тэй ижил). Кодын өөрчлөлт 1.38.0-тэй ижил.

[1.38.0] - 2026-10-22

Added — нэвтрээгүй route-ийн зөвхөн уншдаг роль (nexus.PublicRead, migration 00151) (2026-10-22)

  • Юу: gerege_nexus_public роль (NOLOGIN, ямар ч хүснэгт/схемийн эрхгүй, gerege_nexus_api-д гишүүнчлэлтэй) ба nexus.PublicRead(ctx, db, fn): READ ONLY транзакц нээж тэр роль руу SET LOCAL ROLE хийнэ, app.*-ийг хоосолно. Модуль нийтэд гаргах view-доо GRANT SELECT … TO gerege_nexus_public өгнө. MODULES.md-д хүнд хамаарах хүснэгтийн (app.current_user, FORCE RLS, platform_path) SQL жишээ нэмэгдсэн.
  • Яагаад: /verify/{ref} зэрэг session-гүй route platform path-аар login role-оор ажилладаг тул нэг мартсан WHERE бүх байгууллагын мөрийг ил гаргах боломжтой байв.
  • Хаана: backend/db/migrations/00151_a_role_for_the_public.sql, backend/pkg/nexus/publicread.go; Модуль бичих.
  • Шалгасан: publicread_db_test.go (pool ба nexustest savepoint хоёуланд): GRANT хийсэн view уншигдана, GRANT-гүй хүснэгт ба registry.users 42501, андуурч INSERT олгосон хүснэгтэд бичих 25006, SET TRANSACTION READ WRITE хаалттай; роль нь цөмийн хүснэгтэд эрхгүй. Migration up/down/up.

Added — Байгууллагын нэрийн өмнөөс гарын үсэг модульд (nexus.OrganisationSigner) (2026-10-22)

  • Юу: SignForOrganisation(ctx, OrgSignatureRequest{WorkspaceID, Permission, PDF|DigestHex, DisplayText, DocumentName}) → OrgSignatureSession; OrganisationSignature(ctx, owner, sessionID) → регистр, eID баталсан нэр, зурсан хүн, цаг, гэрчилгээ. Алдаа: ErrNotRepresentative, ErrNoOrganisationRegister, ErrNoSignerIdentity, эрхгүй/өөр workspace бол ErrForbidden.
  • Яагаад: e-property-mn зэрэг distribution-ий модуль байгууллагын нэрийн өмнөөс eID-ээр зурах нийтийн зам байгаагүй. Signer-т арга нэмээгүй тусдаа чадвар тул одоогийн хэрэгжүүлэлт эвдрэхгүй.
  • Хаана: backend/pkg/nexus/signing.go, backend/internal/workspace/signing/org_rail.go, internal/kernel/eidsign (digest-д onBehalfOf, Completed), SIGNING, MODULES. Migration байхгүй.
  • Шалгасан: org_rail_db_test.go (эрхгүй гишүүн, өөр workspace, impersonate, төлөөлөгч биш, регистргүй → алдаа; амжилттай үед onBehalfOf=NTRMN-<регистр>, audit мөр CA дуудлагаас өмнө), eidsign digest-ийн onBehalfOf/Completed тест, pkg/nexus golden.

Added — Модулиас хүнд мэдэгдэл: nexus.Notify, толгойн хонх (2026-10-22)

  • Юу: nexus.Notify(ctx, nexus.Notification{UserID, App, Key, Kind, Title, Body, Link}) — модуль хүнд мессеж үлдээнэ. Ажлын мужийн толгойд хонх (уншаагүйн тоо, жагсаалт, дарахад уншсан болж Link руу очно), суулгац SMTP relay-тэй, хүн баталгаажсан хаягтай, тухайн Kind-ийг дуугүй болгоогүй бол нэг удаа и-мэйл. Хүний API: /api/v1/me/notifications (жагсаалт, unread-count, {id}/read, read-all, email-mutes/{kind}).
  • Яагаад: e-property зэрэг distribution-д «гэрээ гарын үсэг хүлээж байна», «УБЕГ хариу өглөө» гэж хэлэх нийтийн зам байгаагүй (core-requests №4). PersonFeed нь төлөвийн проекц, мессеж биш.
  • Хаана: backend/pkg/nexus/notify.go, backend/internal/person/notify*.go, migration 00149_a_person_is_told.sql (registry.notifications, registry.notification_email_mutes, registry.notify SECURITY DEFINER, person_isolation + platform_path), frontend/components/NotificationBell.tsx. Мэйл нь тайлангийн relay-ээр (REPORT_SMTP_URL, REPORT_MAIL_FROM), холбоосыг PUBLIC_ORIGIN-оор бүтэн болгоно.
  • Шалгасан: ижил key-ээр хоёр дуудахад нэг мөр, нэг мэйл; А хүн Б-гийн мэдэгдлийг тенантын ролиор харж, уншсан болгож чадахгүй, шууд INSERT хийж чадахгүй; гадагш холбоос Go болон CHECK хоёулаа татгалзана (internal/person/notify_db_test.go, pkg/nexus/notify_test.go); хонхны vitest; routes golden.
  • Үлдсэн: push (APNs/FCM) — тусдаа ажил; token бүртгэгддэг ч илгээгч клиент алга. Мэйл нь 24 цагаас хуучин 'pending' мөрийг илгээхгүй, авсны (claim) дараа процесс унавал тэр нэг мэйл алдагдана — давхар илгээснээс дээр.

Added — модулийн тестийн хэрэгсэл pkg/nexustest (2026-10-22)

  • Юу: distribution-ий модулийг бодит Postgres дээр dbguard-тай ижлээр шалгах нийтийн багц: Open/Migrate/Tx/DB, AsTenant/AsPerson/AsPlatform, NewUser/NewWorkspace/AddMember, Permissions, модулийн RegisterRoutes/RegisterPersonRoutes-ийг gate-тэй нь mount хийдэг Handler, хуурамч Signer, EID, People ба Provide[T].
  • Яагаад: нийтийн тест хэрэгсэл байгаагүй (apigateway-ийн txDB/asTenant нь internal) тул power-grid-mn зэрэг distribution set_config('role', …)-ийг гараар давтдаг, app.allowed_tenants-ыг мартах, session-д роль үлдээх зэргээр production-оос зөрдөг байв.
  • Хаана: backend/pkg/nexustest/{nexustest,http,fakes}.go; Модуль бичих, Distribution.
  • Шалгасан: nexustest_test.go-ийн canary модуль: tenant A нь B-ийн мөрийг HTTP-ээр ч, store-оор ч харахгүй, B нь A руу бичиж чадахгүй, RoutePermissionPrefix 403, нэвтрээгүй 401, gate-гүй route platform path, person route зөвхөн өөрийн мөр; tenant роль none болговол тест унадаг нь шалгагдсан.

Added — Compiled модулийн өөрийн дэлгэц workspace дотор (/r/, nexus.PageServer) (2026-10-22)

  • Юу: compiled модуль дэлгэцээ өөрөө, shell-тэй ижил origin дээр HTML-ээр үйлчилнэ: nexus.PageServer (/r/<slug>/, workspace gate), PersonPageServer (/r/me/<slug>/, person gate), ConsolePageServer (/r/cp/<slug>/, консолын gate). MenuDefinition.Frame (ValidateFrame-ээр шалгагдана) мөрийг бүрхүүлийн /module/frame/… дэлгэц рүү заана; консол /cp/apps/<slug>/…-г /r/cp/<slug>/…-аар frame хийнэ. Хуудас ?tenant=&lang=&theme= ба nexus:context postMessage авна. nexus.AppSlug нэмэгдсэн.
  • Яагаад: distribution цөмийн Next.js-д хуудас нэмж чадахгүй тул /module/<app>/<feature> нь «тун удахгүй» карт л байв; iframe нь зөвхөн гадаад mini app-д байсан. e-property-ийн 10 модуль бүрхүүл дотроо дэлгэцтэй байх ёстой.
  • Хаана: backend/pkg/nexus/pages.go, internal/workspace/app/routes.go, internal/workspace/menu/menu.go, internal/operator/{service.go,apps/handlers.go}, internal/kernel/security/security.go (зөвхөн /r/ дор X-Frame-Options: SAMEORIGIN, frame-ancestors 'self'; бусад нь DENY хэвээр), frontend/components/ModuleFrame.tsx, frontend/app/module/frame/[...path], frontend/app/cp/apps/[slug]/[[...rest]], frontend/proxy.ts (frame-src 'self' https:), frontend/next.config.ts (хөгжүүлэлтэд /r/, /api/-г API руу), deploy/nginx/ (location /r/, консолд /r/cp/), MODULES, SHELL_CONTRACT §11.
  • Шалгасан: Postgres дээр TestAModulesPagesAreGatedFrameableHereAndWriteThroughCSRF (session-гүй 401, суугаагүй 403, SAMEORIGIN; same-origin cookie POST 204, cross-origin 403), TestMountPagesGatesAConsoleAppsPages; толгойн, цэсний rewrite, ValidateFrame-ийн unit тест; routes.txt өөрчлөгдөөгүй (цөмд хуудас үйлчлэх модуль байхгүй); vitest module-frame.test.tsx, tsc.

Added — модулийн цаг хуваарьт ажил: nexus.JobRunner (2026-10-22)

  • Юу: модуль Jobs() []nexus.Job{Name, Every, Timeout, Run} зарлавал цөм ажил бүрийг нэг goroutine-оор ажиллуулна: олон replica-д нэг үед нэг л удаа (pinned холболт дээр pg_try_advisory_lock + operator.module_jobs-ийн claim), platform path ctx, timeout, panic барих, slog, module_job_runs_total / module_job_duration_seconds метрик, module.<id>.disabled ба job.<id>.<name>.disabled kill switch. Сүүлийн төлөв /cp/ops/jobs-д.
  • Яагаад: модульд нийтийн scheduler байгаагүй; StartHousekeeping replica бүр дээр ажилладаг тул хадгалалт сулруулах, outbox илгээх зэрэг ажил давхардана.
  • Хаана: backend/pkg/nexus/jobs.go, backend/internal/kernel/modulejobs/, backend/internal/workspace/app/jobs.go, backend/internal/operator/observability/handlers.go, migration 00150_a_module_job_runs_once.sql, MODULES.md.
  • Шалгасан: modulejobs_test.go — хоёр pool (хоёр replica) нэг ажлыг зэрэг ба дараалан tick хийхэд үе бүрд нэг л удаа; kill switch алгасна; timeout цуцална; panic баригдана.

Added — модулийн файл, нотолгооны хадгалалт: nexus.Files() (2026-10-22)

  • Юу: nexus.Files() → FileStore{Put, Open, Stat}, FileRef{ID, WorkspaceID, Name, ContentType, Size, SHA256, CreatedBy, CreatedAt}. Байт нь шинэ workspace.files хүснэгтэд (BYTEA) — FORCE RLS, platform_path, зөвхөн нэмэгдэнэ (UPDATE/DELETE-ийг trigger татгалзана, байгууллага устахад л cascade). Хэмжээ FILES_MAX_MB (анхдагч 25, тааз 100), төрлийг байтаас таньна (PDF, PNG, JPEG, WebP, текст), Provide[FileScanner] hook, files.put audit мөр нэг transaction-д, storage quota-д тооцогдоно. Upload route-той модуль nexus.UploadRoutes-ээр биеийн хязгаараа өсгөнө; app gate нь nexus.AppID(ctx)-ийг тавина.
  • Яагаад: distribution-ий модульд (газрын гэрчилгээ, зөвшөөрөл, гэрээ, зураг) blob хадгалах нийтийн API байгаагүй; цөмд зөвхөн гарын үсгийн PDF хадгалагддаг байв.
  • Хаана: backend/pkg/nexus/files.go, backend/internal/workspace/files/, migration 00148_a_module_keeps_its_evidence.sql, pkg/host/server.go (requestBodyLimit), MODULES.md.
  • Шалгасан: internal/workspace/files/files_db_test.go — guarded pool дээр put/open sha256, өөр байгууллага нээж чадахгүй, UPDATE/DELETE татгалзсан, хэтэрсэн хэмжээ, төрөл таараагүй, scanner татгалзахад юу ч үлдээгүй.

Changed — операторын нэвтрэлтийн eID нь бусад нэвтрэлттэй ижил widget (2026-10-22)

  • Юу: /cp нэвтрэх картын «Нууц үг / eID Mongolia QR» tab хасагдсан. Форм хэвээр, доор нь «эсвэл» ба /login-той ижил eID Mongolia товч (public/eid-login.js) — дарахад QR + баталгаажуулах кодтой цонх.
  • Хаана: components/EIDWidget.tsx өөр API (консолын /session/eid/*) руу заах болсон; widget-д registration: false сонголт (оператор регистрээр нэвтрэхгүй); консолын /session/eid/start widget-ийн callbackUrl-ыг утасны same-device хүсэлт гэж уншина.
  • Шалгасан: vitest (бүгд), tsc, go test ./internal/operator/operator; локал /cp дээр mock API-аар товч → QR цонх.

[1.37.2] - 2026-10-22

Fixed — нэвтрэх хуудасны eID widget iPad, таблет дээр QR харуулна (2026-10-22)

  • Юу: 1.37.1-ийн засвар зөвхөн components/EIDLogin.tsx-д орсон тул /login болон нүүр хуудасны eID товч (public/eid-login.js widget) iPad дээр QR-ын оронд eID апп руу шилжүүлсээр байв. Widget-ийн isPhone() одоо зөвхөн iPhone болон UA-даа Mobile гэсэн Android-ыг утас гэж үзнэ. iPad simulator дээр QR гарч, iPhone simulator дээр апп руу шилжсээр байгааг шалгав.

[1.37.1] - 2026-10-22

Fixed — iPad, таблет дээр eID QR код харагдана (2026-10-22)

  • Юу: iPad (desktop горимтой Safari-г оролцуулаад) болон Android таблетын browser-ыг утас гэж үзэж, QR-ын оронд eID апп руу шилжүүлдэг байв. Одоо зөвхөн утас (iPhone, UA-даа Mobile гэсэн Android) апп руу шилжинэ; таблет desktop шиг QR зурна (components/EIDLogin.tsx).

[1.37.0] - 2026-10-21

Changed — нэг апп: байгууллагад admin/manager-ийн цэс, хувийн гэрт хэрэглэгчийн цэс (2026-10-21)

  • Юу: «Хувийн платформ» ба «Байгууллагын платформ» гэсэн хоёр built-in нэг «Платформ» апп болов (nexus.platform); агуулгыг workspace-ийн төрөл шийднэ.
  • Гэр (kind='personal'): Хянах самбар, Профайл, Миний eID (/module/eid, өмнө нь цэсгүй байсан), Тохиргоо → Харагдац. App Store байхгүй.
  • Байгууллага: Хянах самбар, Профайл, суусан аппууд; Тохиргоо → Хандалтын тохиргоо (admin, manager — join_requests.manage), App Store (зөвхөн admin — суулгах нь аль хэдийн RequireAdmin байсан, цэс өмнө нь гишүүн бүрд харуулдаг байв). Харагдац байгууллагын цэснээс гарсан (дансны цэсэнд хэвээр).
  • Хасагдсан: хувийн App Store (/me/apps), хоёр демо апп (nexus.demo_one/two, /me/demo/*). Хүнд санал болгох жинхэнэ апп байгаагүй — ганц каталогийн апп (API Gateway) байгууллагынх.
  • Хувийн tenant хэвээр: хүн бүрд гэр (117b547d-ийн бүтээгдэхүүний шийдвэр) зөв гэж үзэв; workspaceKind.mjs-ийн хуучирсан тайлбар (00094-ийн «гэргүй» загвар) засагдсан.
  • Бусад: tenant сэлгэхэд гэрт /me/dashboard, байгууллагад /organisation/dashboard руу орно (өмнө нь /profile, /apps). Байгууллагын самбарын «App Store» холбоос admin-д, «Хандалт» эрхтэйд л харагдана.
  • Хаана: backend/internal/workspace/{menu/menu.go,coreapps/coreapps.go,appinstall/store_menus.go}, frontend/components/{Layout,TenantChoices}.tsx, frontend/app/organisation/dashboard/page.tsx.
  • Хэвээр: хүний апп (nexus.PersonApp, person gate, host.Options.DefaultPersonApps, workspace.person_app_installations) — power-grid-mn-ийн PowerGrid App (/api/v1/me/powergrid/*, iOS/Android) түүн дээр ажилладаг. Ийм апп нь дистрибуцаас хүн бүрд суугдаж гэрийн цэсэнд гарна; хүн өөрөө суулгах дэлгэц л хасагдсан.

Added — native самбар: ширээн дээр зүүн цэс, утсан дээр доод таб (2026-10-21)

  • Юу: нэвтэрсний дараах эхний дэлгэц нь native самбар (нэр, РД, иргэний бүртгэлийн дугаар, KYC түвшин, платформ руу орох карт; утсан дээр хадгалсан хуудсууд). Хоёр дахь дэлгэц нь «Платформ» — одоогийн ажлын мужийн webview. macOS ба Windows дээр зүүн цэс, iOS ба Android дээр доод таб.
  • Хаана: native-apps/desktop/macos/Features/Main/{ShellView,DashboardPage}.swift, native-apps/mobile/ios/Features/{Main/MobileShellView,Dashboard/MobileDashboardPage}.swift, native-apps/mobile/android/.../ui/{ShellScreen,DashboardScreen,PlatformScreen}.kt.
  • Яаж: самбар нь Keychain/Keystore-ийн identity snapshot-оос л уншина — шинэ endpoint байхгүй, сүлжээгүй ч гарна. Webview нь самбар нээлттэй үед модноос хасагдахгүй тул платформ дээрх хуудас хэвээр үлдэнэ. Холбоос (Universal/App Link, хадгалсан мөр) ирэхэд Платформ таб руу шилжинэ.
  • Windows: цэс нь «Самбар + Платформ» болж, нэвтэрсний дараа самбар нээгдэнэ. Самбар нь хадгалсан session-оос уншина: өмнөх /api/dashboard, /api/devices, /api/activity дуудлагууд 089375cf-ээс хойш 404 өгдөг байсан. Бусад native хуудсууд (байгууллага, хүүхэд, токен, гарын үсэг, шалгах) цэснээс гарсан, код нь хэвээр.

Security — excelize-ийн засварласан commit, Go toolchain go1.26.10 (2026-10-21)

  • Юу: backend/go.mod-ийн github.com/xuri/excelize/v2 v2.11.0 → v2.11.1-0.20260915055537-22f76f9acb94 (дагалдан github.com/richardlehane/mscfb v1.0.7 → v1.0.8), toolchain go1.26.9 → go1.26.10.
  • Яагаад: govulncheck гурван дуудагдсан эмзэг байдал мэдээлж Vulnerability Check-ийг унагаж байсан: GO-2026-6688, GO-2026-6684 (excelize — internal/workspace/reporting-оор), GO-2026-6809 (crypto/x509 — eidrp.VerifySignedDigest → x509.Certificate.Verify). excelize-ийн хоёр засвар зөвхөн tag-гүй upstream commit-д байгаа; 22f76f9acb94 (2026-09-15) хоёуланг нь агуулна. Кодын өөрчлөлтөөс үүдээгүй; main ч мөн адил унана.
  • Хаана: зөвхөн backend/go.mod, backend/go.sum. Бүх workflow go-version-file: backend/go.mod уншдаг.
  • Шалгасан: go1.26.10 дээр govulncheck (дуудагдсан эмзэг байдал 0; өмнө нь 3), go build, go vet, go test ./... (78 багц ok). Digital Nomads distribution (nexus-app) excelize-ийн ижил commit-ийг 2026-10-21-нд авч, security шалгалтууд нь ногоон болсон.
  • Үлдсэн: excelize шинэ tag гаргавал pseudo-version-ийг tag руу шилжүүлнэ.

[1.36.0] - 2026-10-15

Changed — Native клиентүүд нимгэн бүрхүүл, нэг гарал (2026-10-15)

  • Бүрхүүл: macOS, iOS, Android нь нэвтрэлт → ажлын муж (webview) → тохиргоо гэсэн гурван дэлгэцтэй болов. Native самбар, ID, лог, байгууллага, хүүхэд, гарын үсэг, Токенууд/Токен уншигч, нуусан Home/Verify/Devices/Security хуудсууд хасагдсан — тэдгээр нь ажлын мужид. iOS/Android дээр табын зурвас алга: дээд мөрөнд буцах, хадгалсан, тохиргоо. Native чадварууд (eID, Keychain/Keystore, биометр, ESIGN гүүр, USB токен, баримт скан, хадгалсан хуудас, deep link, menu bar) хэвээр.
  • Нэг гарал: бүх клиент PUBLIC_ORIGIN (nexus.gerege.mn) дээр ажиллана. Шугамыг host биш webview-ийн User-Agent-ийн GeregeShell/<шугам> тэмдэгт хэлнэ (frontend/lib/deviceLine.ts, proxy.ts → Vary: User-Agent). desktop./mobile./kiosk./pos.nexus.gerege.mn, түүний nginx vhost, setup_device_lines.sh, DEVICE_LINE_ORIGINS устсан.
  • eID: macOS QR нь цөмийн device_link_url-ийг зурж, qr_refresh үед /api/v1/auth/eid/qr-аар секунд тутам шинэчилнэ. iOS/Android app-to-app нь цөмийн device_link-ийг эхэлж нээнэ.
  • Сервер дээр (deploy-ийн дараа гараар): /etc/nginx/sites-enabled/device-lines.nexus.gerege.mn.conf-ыг устгаж nginx -s reload; хүсвэл certbot delete --cert-name device-lines.nexus.gerege.mn болон GitHub-ийн DEVICE_LINE_ORIGINS хувьсагч.

Fixed — iOS: eID-ээс апп руу буцах (2026-10-15)

  • Universal Link: iOS апп eID-д gerege-nexus://auth биш https://nexus.gerege.mn/auth/eid/callback?to=app өгнө — eID апп өөр аппын схемийг нээж чаддаггүй тул «Боловсруулахад алдаа гарлаа» гэж зогсдог байв. Frontend /.well-known/apple-app-site-association (зөвхөн /auth/eid/callback, APPLE_APP_IDS) өгч, iOS тэр замыг аппд нээнэ.
  • Нөөц зам: холбоос Safari/Chrome-д нээгдвэл callback хуудас to=app-ийг аппын схем рүү (APP_URL_SCHEME, анхдагч gerege-nexus) дамжуулна.
  • Production-д EID_APP_CALLBACKS=gerege-nexus://auth нэмэгдсэн (PRODUCTION_ENV).
  • Android апп eID-д https://nexus.gerege.mn/auth/eid/callback?to=app өгнө (iOS-тэй ижил). Frontend /.well-known/assetlinks.json (ANDROID_APP_PACKAGE, ANDROID_CERT_SHA256) өгч, App Link нь зөвхөн /auth/eid/callback-ийг барина (appLinksPathPrefix анхдагч). Хөтөч нээвэл to=app нь gerege-nexus://auth руу дамжина.

[1.35.0] - 2026-10-15

Added — Distribution-ий анхдагч person app (host.Options.DefaultPersonApps) (2026-10-15)

  • Юу: DefaultApps-ийн хувь хүний хувилбар. Жагсаасан person-surface апп хүн бүрт цэс/store-ийг анх уншихад суугдана (цөмийн coreapps.PersonDefaultIDs-ийн хажууд); хүн устгавал мөр нь enabled = FALSE-ээр үлдэж, буцаж суугдахгүй. Каталогт байхгүй id-г алгасна.
  • Яагаад: person gate (nexus.PersonApp, 1.34.0) аппыг хүн өөртөө суулгасан байхыг шаарддаг. PowerGrid App-ийг иргэн бүр App Store-оос өөрөө суулгах шаардлагагүй байх ёстой.
  • Хаана: pkg/host/run.go, internal/workspace/app/wire.go (SetDefaultPersonApps), internal/workspace/appinstall/installer.go, person_store.go, MODULES.
  • Шалгасан: Postgres дээр TestDefaultPersonApps (анх уншихад суугдана, устгасныг буцааж суулгахгүй), go test -p 1 ./pkg/... ./internal/workspace/appinstall/... ./internal/workspace/app/....

[1.34.0] - 2026-10-15

Added — Хувь хүний аппын өөрийн route ба eID регистр (nexus.PersonApp, nexus.PersonOf) (2026-10-15)

  • Юу: person гадаргуутай compiled модуль RegisterPersonRoutes(r, personGate)-ийг хэрэгжүүлбэл платформ түүнийг mount хийнэ. Gate: session-гүй бол 401, kill switch асаалттай бол 503, impersonation бол зөвхөн унших, хүн аппыг өөртөө суулгаагүй бол 403. Хүсэлтийг тухайн хүнд л хамааруулна — session байгууллагатай байсан ч workspace, IsAdmin хасагдаж, dbguard tenant-гүй tenant ролиор холбоно. nexus.PersonOf(ctx) нь eID-ийн илгээсэн регистрийн дугаарыг (registry.own_eid_identity) буцаана, eID-гүй данс бол nexus.ErrNoEID.
  • Яагаад: GateMiddleware нь RequireWorkspace шаарддаг тул иргэн өөрийн өгөгдлийг (PowerGrid App — тоолуур, нэхэмжлэл, төлбөр) харах модулийн route руу орж чаддаггүй байв.
  • Хаана: pkg/nexus/person.go, internal/workspace/appinstall/gate.go (PersonGateMiddleware, personGate), appinstall/person_identity.go, workspace/app/routes.go, workspace/app/capabilities.go, MODULES. SDK нэмэлт л — api_changes.txt.
  • Шалгасан: TestPersonGate (401/403/impersonation/workspace хасагдах), Postgres дээр TestPersonOf (dbguard-тай, person scope), go test ./pkg/... ./internal/workspace/appinstall/... ./internal/workspace/app/....

[1.33.0] - 2026-10-15

Added — Apple, Facebook, X, GitHub-ээр нэвтрэх (2026-10-14)

  • Юу: /login дээр Google-ийн хажууд Apple, Facebook, X, GitHub товч. Тус бүр консолын auth.<provider>_login шилжүүлэгтэй (анхдагч унтраалттай) ба түлхүүрүүд нь бүгд (auth.apple_client_id, auth.apple_team_id, auth.apple_key_id, auth.apple_private_key; auth.{facebook,x,github}_client_id, …_client_secret) тохирсон үед л гарна — дахин эхлүүлэх шаардлагагүй.
  • Дүрэм нь Google-ийнх: анхны нэвтрэлт eID-ээр холбогдоно, и-мэйлээр данс тааруулахгүй; провайдер баталгаажуулсан и-мэйл (Apple, GitHub-ийн verified primary) л хадгалагдана. Facebook, X-ийнх хэзээ ч үгүй. Профайлаас холбож болно. Федерацитай суулгацад Google-ийн адил хаагдана.
  • Протокол: Apple нь OIDC (ES256 client secret JWT, form_post → GET болгож Lax cookie-г хадгална); Facebook, X, GitHub нь OAuth2 + PKCE + state (ssoclient.OAuth2).
  • Провайдер дээр бүртгэх callback: ${SSO_ISSUER}/api/v1/auth/{apple,facebook,x,github}/callback.
  • Шалгасан: go test ./internal/workspace/ssoclient/... ./internal/workspace/identity/... ./internal/kernel/..., ./pkg/host (routes.txt), npx tsc --noEmit, npx vitest run.

Changed — eID товч нь eID Mongolia-ийн товч (2026-10-14)

  • Юу: /login болон нүүр хуудасны hero-гийн eID товч одоо eID Mongolia-ийн товч — цагаан, хүрээтэй, eID Mongolia-ийн лого (public/eid-mongolia.png, developer.eidmongolia.mn-ийн logo-96) ба «eID Mongolia-аар нэвтрэх» / «Sign in with eID Mongolia». Дарахад нээгдэх QR цонх, backend хэвээр.
  • Widget (/eid-login.js): товч үргэлж энэ загвар (variant хасагдсан — цэнхэр дэвсгэр дээр лого харагдахгүй байв); лого нь скриптийн хажуунаас, эсвэл logo сонголтоор. Товчны фонтын хүчингүй shorthand засагдсан.
  • Шалгасан: npx tsc --noEmit, npx vitest run (360), Playwright (11), build-ийг нүдээр.

Added — eID login widget (/eid-login.js): нэг JS, дурын RP; Nexus өөрөө хэрэглэнэ; hero дээр нэвтрэх карт (2026-10-14)

  • Юу: frontend/public/eid-login.js — бие даасан «eID-ээр нэвтрэх» widget (React-гүй, QR-ийг Nayuki qrcodegen (MIT)-оор өөрөө зурна). Товч → <dialog>: QR баталгаажуулах кодтойгоо шууд (v3 бол секунд тутам шинэчилнэ), тоолуур, «Регистрийн дугаараар нэвтрэх»; утсан дээр eID апп руу шилжүүлнэ; mn/en. Secret-гүй — RP-ийн өөрийн backend-ийн POST {api}/auth/eid/start|start-id|poll|qr-ийг cookie-тэй нь дууддаг. <div data-eid-login data-api data-next> эсвэл EIDLogin.mount(el, {api, lang, variant, onStart, onSuccess, onError}); CSS хувьсагчаар theme.
  • Nexus хэрэглэгч сайт шиг: /login-ийн eID товч одоо widget (components/EIDWidget.tsx скриптийг ачаалж /auth/eid/*-ийг заана, next руу шилжүүлнэ). Нүүр хуудасны hero нэвтрээгүй зочинд нэр/нууц үгийн карт, доор нь eID товч (components/landing/HeroSignIn.tsx; нууц үг унтраалттай бол eID л). EIDLogin-ийн qrFirst горим хасагдсан.
  • Шалгасан: npx tsc --noEmit, npx vitest run (360), Playwright (11 — widget-ээр QR нэвтрэлт), локал mock eID.

Changed — eID нэвтрэлт нэг товч, QR нь popup-д; аудитын кодууд үгээр (2026-10-14)

  • eID нэвтрэх: /login дээр eID нь ганц «eID-ээр нэвтрэх» товч. Дарахад popup (Dialog) нээгдэж QR код баталгаажуулах кодтойгоо шууд зурагдана; «Регистрийн дугаараар нэвтрэх» нь доорх товч (буцаад «QR кодоор нэвтрэх»). Popup-ийг хаахад eID session зогсоно — тусдаа «Цуцлах» алга. Хуудас ачаалахад session эхлэхгүй. EIDLogin-ийн шинэ qrFirst горим — Google холболт, профайл, консолын eID карт өөрчлөгдөөгүй.
  • Хасагдсан тайлбар: «Энэ платформ хаалттай горимд байна…» ба регистрийн маягтын дээрх заавар.
  • Аудитын кодууд: /me ба байгууллагын самбар auth.eid_app_login_success, eid, session-ийг түүхийгээр харуулдаг байв — cp.audit_* шошго зөвхөн консолын bundle-д ачаалагддаг байсан. Шошгонууд core (lib/i18n/addons/audit.ts) руу шилжиж, хоёр самбар auditAction / auditTarget-ийг хэрэглэнэ; 28 шошгогүй код (ai., oauth2., access., reports., нууц үг, апп, и-мэйл баталгаажуулалт) ба зорилтын төрлүүд нэмэгдсэн. Баримтын лог SIGN, DOWNLOAD…-г esign.action_type.*-аар.
  • Dev: service worker dev-д бүртгэгдэхгүй (хуучин /_next/static-ийг cache-ээс өгч засварласан хуудсыг хуучин кодоор ажиллуулдаг байв). EID_MOCK_APPROVE_AFTER — mock eID-ийн батлах хугацаа (анхдагч 1.5s).
  • Шалгасан: npx tsc --noEmit, npx vitest run (361), Playwright (11), go test ./internal/workspace/identity/...; локалаар mock eID-ээр нэвтэрч /me самбарыг харсан.

Removed — operator.pending_approvals хүснэгт (2026-10-14)

  • Юу: Migration 00146 хоёр хүний дүрмийн хүсэлтийн хүснэгтийг хасна — #197-оос хойш уншигч, бичигчгүй. Хүсэлт, зөвшөөрөл бүр operator_audit-д хэвээр. Down нь хүснэгтийг (хоосон) сэргээнэ.
  • Шалгасан: хоосон DB дээр up → down → up, go test ./db/migrations/..., ./internal/operator/....

Changed — Байгууллага устгахад хоёр дахь хүн хэрэггүй болов (2026-10-14)

  • Юу: Хоёр хүний дүрэм (нэг superadmin хүсэж, өөр нэг нь зөвшөөрөх) хасагдав. POST /tenants/{id}/deletion одоо шууд товлоно: байгууллага түр зогсож, session-ууд нь дуусч, 30 хоногийн дараа sweep устгана; тэр хүртэл DELETE /tenants/{id}/deletion буцаана (internal/operator/tenants/deletion.go ScheduleDeletion).
  • Устсан: internal/operator/approvals багц, /approvals гурван route, approval.decide чадвар, «Хүлээгдэж буй зөвшөөрөл» дэлгэц. operator.pending_approvals хүснэгт DB-д хэвээр, ашиглагдахгүй.
  • Бичвэр: худал болсон мэдэгдлүүд засагдав — байгууллагын жагсаалтын «хоёр дахь superadmin» мөр, нэвтрэх хуудасны гурав дахь карт, Хэрэглэгч хуудасны «Дэмжлэгийн дэлгэц», консолын тайлбарын «үйлдлийн бүртгэл».
  • Шалгасан: Postgres дээр go test -p 1 ./internal/operator/... ./pkg/host/..., frontend vitest.

Changed — Консолын цэс цэгцлэгдэв (2026-10-14)

  • Юу: «Хянах самбар» Платформ бүлгийн эхэнд, «Миний данс» Тохиргооны «Данс» бүлэгт шилжив; «Операторууд» (Тохиргоо → Хандалт-аас) Хэрэглэгч бүлгийн эхэнд орж; «Платформын эрүүл мэнд» Платформ бүлгийн эхэнд орж «Ажиглалт» бүлэг алга болов; «Хандалт», «Мөрдлөг» бүлэг алга болов (backend/internal/operator/apps/builtin.go).
  • Эхлүүлэх: Тохиргооноос Платформын эхэнд «Эхлэл» бүлэг болж шилжив; сайт нээгдсэний дараа цэсэнд харагдахгүй (Console.tsx withoutStart).
  • Устсан: «Операторын бүртгэл» дэлгэц ба GET /audit (байгууллагын хуудас, аппын түүх аудитаа хэвээр уншина); «Дэмжлэг» дэлгэц ба GET /people, POST /people/{id}/unlock|sessions/revoke|credential-link (support багцын үйлчилгээ бусад дэлгэцэд хэвээр). «Эрх ба суулгац» бүлэг бүхэлдээ — /cp/quotas, /cp/installations хуудас, GET /api/platform/v1/tenant-quotas, GET /api/platform/v1/app-installations (tenants/roster.go). Байгууллагын квот, суулгацыг тухайн байгууллагын хуудаснаас хэвээр харна.
  • Тохиргоо: хүснэгтээс «Хаанаас» багана ба түүх/буцаах товч хасагдав (утга environment-ээс ирсэн бол тэмдэг үлдэнэ); platform.launched мөр Эхлүүлэх дэлгэцэд л үлдэв.
  • Шалгасан: go test (apps, tenants, pkg/host routes), frontend vitest, локал консол.

Added — Google Drive, Dropbox, Google Meet холбогч цөмд буцаж ирэв (2026-10-14)

  • Юу: 2026-08-27-нд (fcf953e5, 00088) client-gerege-nexus руу гарсан холбогчид буцав — Өртөө биш, зөвхөн холбогч. Байгууллагын админ «Холболтууд» апп-аас (/module/integrations/connectors, core app nexus.integrations, байгууллага бүрт анхдагчаар суусан) Drive, Dropbox, Meet дансаа OAuth-оор холбож, webhook/REST холбогч бүртгэнэ.
  • Хаана: backend/internal/workspace/integration (сэргээсэн), маршрут /api/v1/integrations/* (админ), session-гүй GET /api/v1/integrations/oauth/callback; цэвэрлэгээ (StartHousekeeping) internal/workspace/app/jobs.go-д. Migration 00147 гурван хүснэгтийг (workspace.integrations, integration_oauth_states, integration_deliveries) одоогийн дүрмээр — RLS FORCE, platform_path, tenant_isolation өргөн хэлбэр, (tenant_id, id) composite FK, урттай varchar — дахин үүсгэнэ.
  • Консол: OAuth client-ууд консолын итгэмжлэлд: google.oauth_client_id / _secret (Drive ба Meet хуваалцана, Google нэвтрэлтийн client-оос тусдаа), dropbox.oauth_client_id / _secret. Хүсэлт бүрт уншина — дахин эхлүүлэх шаардлагагүй. «Холболтууд» хуудсанд гарна (credentialSection). Орчны хувьсагч GOOGLE_OAUTH_CLIENT_ID/_SECRET, DROPBOX_OAUTH_CLIENT_ID/_SECRET, INTEGRATION_ALLOW_PRIVATE_TARGETS .env.example, docker-compose.prod.yml-д буцав.
  • Провайдер дээр бүртгэх буцах хаяг: $PUBLIC_ORIGIN/api/v1/integrations/oauth/callback.
  • Буцаагүй: гарын үсэгтэй PDF-ийг Drive/Dropbox руу автоматаар илгээх (/esign/.../export) ба nexus.MeetingBooker — дуудагчгүй тул. Холбогч холбогдож, Manager.ExportFile / CreateMeeting бэлэн боловч одоогоор тэднийг дууддаг дэлгэц алга.
  • Шалгасан: go build/vet ./..., go test ./internal/workspace/integration/... ./pkg/host/... ./db/migrations/... ./internal/workspace/... (Postgres-тэй), 00147 up/down/up, npx tsc --noEmit, npx vitest run.

Added — AI-г таван үйлчилгээ үзүүлэгчээс сонгоно (2026-10-14)

  • Юу: Copilot, орчуулга, бичвэрт буулгалт Gemini-ээс гадна OpenAI (GPT), Anthropic (Claude), DeepSeek, Qwen (Alibaba DashScope)-оор ажиллана. Оператор консолын шинэ ai.provider тохиргоогоор (анхдагч gemini, env AI_PROVIDER) сонгоно; ai.model нь сонгосон үйлчилгээ үзүүлэгчийн загварын нэр, хоосон бол анхдагч: gemini-2.5-flash, gpt-4o-mini, claude-sonnet-4-5, deepseek-chat, qwen-plus.
  • Түлхүүр: шинэ credential — ai.openai_api_key (OPENAI_API_KEY), ai.anthropic_api_key (ANTHROPIC_API_KEY), ai.deepseek_api_key (DEEPSEEK_API_KEY), ai.qwen_api_key (DASHSCOPE_API_KEY). Эхлүүлэх жагсаалтын «AI туслах» мөр сонгосон үйлчилгээ үзүүлэгчийн түлхүүрийг шалгана.
  • Хаана: backend/internal/kernel/llm (OpenAI Chat Completions — OpenAI, DeepSeek, Qwen; Anthropic Messages), workspace/ai/copilot.go (chatClient), kernel/settings/keys.go, kernel/credentials/keys.go, operator/launch/launch.go, frontend/lib/i18n/addons/cp.ts.
  • Хязгаар: дуу хоолой (TTS) болон дуут асуулт/бичвэрт буулгалт зөвхөн Gemini-д — бусад үйлчилгээ үзүүлэгчид дуу илгээгдэхгүй, Copilot «түр боломжгүй» гэж хариулна. Дуу хоолойд Gemini түлхүүр хэвээр хэрэгтэй. GEMINI_MODEL env-ээс ирсэн gemini… нэрийг бусад үйлчилгээ үзүүлэгчид үл тоож анхдагчийг асууна. Хэмжүүрийн system="gemini" шошго цуваа тасрахгүйн тулд аль ч үйлчилгээ үзүүлэгчид хэвээр.
  • Шалгасан: go test ./internal/kernel/llm/ (httptest: текст, system prompt, tool call-ийн нэг тойрог, 429 дахин оролдлого, 401, түлхүүргүй, дуу), ./internal/workspace/ai/ (үйлчилгээ үзүүлэгч сонголт, загварын анхдагч), npx tsc --noEmit.

[1.32.0] - 2026-10-14

Changed — Native клиентүүд цөмд буцаж ирэв (2026-10-14)

  • Юу: #185-ийн тусгаарлалтыг буцаав — native-apps/, native-clients.yml, native-release.yml, scripts/check-native-tests.mjs, make build-mac/run-mac, npm run i18n:export-native дахин энэ репод.

Fixed — Go хамаарлын шинэчлэлт canary дээр унахаа болив (2026-10-14)

  • Юу: Dependabot зөвхөн backend/go.mod-ыг шинэчилдэг тул backend/testdata/canary/go.mod-ийн indirect мөр хоцорч, Go-ийн bump бүр «Minimal distribution» дээр унадаг байв (#191). Алхам одоо GOFLAGS=-mod=mod-оор ажиллана (.github/workflows/downstream.yml, #195).
  • Хамаарал: golang.org/x/crypto 0.58.0, x/term 0.47.0 (#191); @playwright/test 1.64.0, jsdom 30.1.2 (#192).

[1.31.0] - 2026-10-13

Changed — Нэвтрэх нэр, нууц үг анхдагчаар асаалттай; нүүрэнд зураг (2026-10-13)

  • Юу: auth.username_login-ийн анхдагч true боллоо: нэвтрэх карт нь нэвтрэх нэр/нууц үг, eID нь «eID-ээр нэвтрэх» товч. Консол хэвээр унтрааж чадна. Setup-ийн bootstrap нь тохиргоог үргэлж бичнэ.
  • Нүүр хуудас: нууц үг асаалттай үед hero-ийн eID карт харагдахгүй — оронд нь платформын зураг (components/landing/HeroArt.tsx), эхний товч /login руу. Зөвхөн eID үед карт хэвээр.
  • Шалгасан: tests/hero-sign-in.test.tsx, 375/768px дээр хэвтээ гүйлгээгүй (Playwright).

Changed — go-react-ийн backend сайжруулалт шингэв (2026-10-13)

  • Юу: nexus-gerege-go-react-ийн backend-ийн B1/B2 refactor (kernel primitive, workspace/app composition root, accounts package, 500+ мөрийн файлууд хуваагдсан), B4a security (eID binding/sign-in key separation, mock flags production-д boot хийхгүй, reset нь OAuth token-уудыг цуцална, консол audit-before-side-effect), B4b correctness (quota transaction дотор 409, scheduler дахин шалгана, gateway tenant FK, CORS PATCH/X-API-Key), backend-slim (pgx.CollectRows, operator.Act, stdlib).
  • Устсан: ДАН-аар нэвтрэх rail (POST /api/v1/auth/dan/login, DAN_* env, nexus.DAN()), cmd/catalog-diff, cmd/tenant-bootstrap, /cp/api redirect, retry metric ба Grafana-ийн retry panel, хэрэглэгдэхгүй nginx config (nexus.gerege.mn.conf, admin.nexus.gerege.mn.conf, snippet-ууд). Баримтын ДАН гарын үсгийн зам хэвээр.
  • Хадгалсан: манай console no-reason/no-step-up, TOTP/passkey, email OTP код, launch gate, console-оос eID RP, pre-registration (accounts руу шилжсэн). Native backend (device line, push token, staff PIN) хэвээр — native клиентүүд nexus-gerege-native-д.
  • Migration: go-react-ийн 00137/00139–00141 → 00142–00145; 00138 нь манай 00141-тэй ижил тул хасагдсан. API gateway 00002 (tenant FK).
  • Шалгасан: Postgres дээр go test -p 1 ./..., golangci-lint 0 issue.

Changed — Deploy: image нэг удаа, drain ард (2026-10-13)

  • Юу: nexus-gerege-nodejs-next-ийн deploy сайжруулалт. Image-уудыг CI (ci.yml, images) тестүүдтэй зэрэг НЭГ удаа барьж, main дээр SHA-гаар түлхэнэ; deploy.yml дахин барихгүй — тестлэгдсэн image яг тэр хэвээр гарна. latest нь амжилттай rollout-ын дараа л шилжинэ.
  • deploy-production.sh: хуучин slot-ын 45 секундийн drain ард явна (deploy lock-ийг барьсаар, лог drain.log), nginx reload-ын дараах HTTPS шалгалт curl --retry-тэй.
  • check-private-nginx.sh: түр cert-үүд checkout дотор (.nginx-check.*) — Mac дээрх Docker-т mount болно.
  • Шалгасан: actionlint, shellcheck, nginx -t (docker), deploy/scripts/test_*.py.

[1.30.0] - 2026-10-13

Changed — Native клиентүүд тусдаа репод (2026-10-13)

  • Юу: native-apps/ (macOS, iOS/iPadOS, Windows, Android) түүхтэйгээ nexus-gerege-native руу шилжив. native-clients.yml, native-release.yml, scripts/check-native-tests.mjs, scripts/check-shell-contract.mjs хамт явсан; make build-mac/run-mac хасагдсан.
  • Бүрэн салсан: native репогийн build/тест/CI цөмийг татдаггүй. shell.ts-ийн нэрс тэнд shared/shell-contract.json snapshot (check-shell-contract.mjs --sync <core>), орчуулга sync-i18n.sh <core>-оор шинэчлэгдэнэ. Клиентийн хүснэгт (хуучин §9) → native SHELL_CLIENTS.md.
  • Цөмд үлдсэн шалгалт: scripts/check-shell-contract.mjs — shell.ts ↔ SHELL_CONTRACT.md хүснэгт (ci.yml, frontend job).
  • Устсан: npm run i18n:export-native — nexus-gerege-native/sync-i18n.sh ашигла.

Security — Go toolchain go1.26.9, golang.org/x/net v0.60.0 (2026-10-12)

  • Юу: backend/go.mod-ийн toolchain go1.26.6 → go1.26.9, golang.org/x/net v0.58.0 → v0.60.0.
  • Яагаад: govulncheck 2026-10-09-нөөс хойш 11 эмзэг байдал мэдээлж Vulnerability Check-ийг унагаж байсан (GO-2026-6599, 6600, 6603, 6605, 6607, 6608, 6610, 6611, 6612, 6613, 6617 — net/http, crypto/tls, net/textproto, html/template, x/net). Кодын өөрчлөлтөөс үүдээгүй; main ч мөн адил унана.
  • Хаана: зөвхөн backend/go.mod, backend/go.sum. Бүх workflow go-version-file: backend/go.mod уншдаг.
  • Шалгасан: go1.26.9 дээр govulncheck (дуудагдсан эмзэг байдал 0), go vet, go build.

Fixed — хостын Docker image цэвэрлэгээ репод, cron суусан (2026-10-12)

  • Юу: deploy/scripts/docker-image-prune.sh — ghcr.io/gerege-systems/* repo тус бүрээс контейнерт (зогссон slot-ыг оруулаад) ашиглагдаж буй image ба сүүлийн 3 tag-ийг үлдээж бусдыг устгана, дараа нь dangling image. nexus.gerege.mn дээр /usr/local/bin/-д суулгаж /etc/cron.d/docker-image-prune (04:30).
  • Яагаад: 2026-10-12-нд хостын диск 100% (166 tag / repo, containerd 58 GB) → private_nexus_postgres recovery checkpoint дээр PANIC давталт → прод eID нэвтрэлт «session could not be started» хэдэн цаг. Цэвэрлэгээ «гараар хийгддэг» жагсаалтад байсан ч хэзээ ч суугаагүй; RUNBOOK-ийн loop хуучин private-gerege-nexus repo нэрийг заасан тул юу ч устгахгүй байв.
  • Ops (хост дээр гараар): docker image prune -a --filter until=72h → 48.8 GB чөлөөлж 100% → 44%; restart backoff-д гацсан nexus_js_postgres-ийг docker restart.
  • Хаана: deploy/scripts/docker-image-prune.sh, docs/OPERATIONS.md (Гараар хийгддэг зүйлс), docs/RUNBOOKS.md (NexusDiskFillingUp / NexusDiskAlmostFull).
  • Шалгасан: бүх postgres healthy, POST /api/v1/auth/eid/start 200 + https device_link_url; скриптийг хост дээр ажиллуулсан (rc 0, ашиглагдаж буй image хөндөгдөөгүй).
  • Үлдсэн: NexusDiskFillingUp/NexusPostgresDown дүрэм байгаа ч Alertmanager receiver хоосон — мэдэгдэл хэнд ч очдоггүй. Receiver (Telegram г.м.) тохируулах хүртэл диск/DB уналтыг хэрэглэгч л олно.

Fixed — Консолын «холбоос илгээх» 42501-ээр унадаг байсан (2026-10-09)

  • Юу: Консол урилга/сэргээх холбоос илгээхдээ хүний хүчинтэй хуучин холбоосуудыг эхлээд хаадаг (UPDATE registry.credential_grants SET redeemed_at = NOW()), гэвч gerege_nexus_operator-т 00050-аас хойш зөвхөн SELECT, INSERT эрх байсан тул илгээлт бүр permission denied for table credential_grants-аар 500 буцаадаг байв.
  • Засвар: Migration 00141 — GRANT UPDATE (redeemed_at) ON registry.credential_grants TO gerege_nexus_operator. Зөвхөн тэр багана: токен, зорилго, хугацааг консол өөрчилдөггүй.
  • Шалгасан: backend/db/migrations/console_credential_link_test.go — консолын ролиор support.go-ийн яг тэр UPDATE ажиллаж, бусад баганад UPDATE эрх байхгүйг баталгаажуулна. Илэрсэн газар: digital-nomads тест орчин (v1.29.0).

Changed — И-мэйл баталгаажуулалт verify.gecloud.mn, OTP код (2026-10-05)

  • Юу: И-мэйлийн rail enigma.mn-ийн холбоосноос verify.gecloud.mn-ийн 6 оронтой код руу шилжив (POST /v1/verify/send {to, channel:"email"}, POST /v1/verify/check {request_id, code}, X-API-Key). Үндсэн хаяг https://verify.gecloud.mn/v1/verify; түлхүүр консолын email.verify_api_key (env fallback).
  • Урилга ба нууц үг сэргээх: консол код илгээж, урилгыг тэр илгээлттэй холбоно (credential_grants.verification_id). Хүн /login/set-password дээр и-мэйл, код, шинэ нууц үгээ оруулна (POST /api/v1/auth/credential/redeem-code). И-мэйлд холбоос байхгүй тул консол урилгын дараа тэр хаягийг харуулна; нэвтрэх хуудсанд «Урилга эсвэл сэргээх код ирсэн үү?» холбоос нэмэгдсэн.
  • Хамгаалалт: нэг баталгаажуулалтад 5 оролдлого (emailverify.MaxCodeAttempts, нэг нөхцөлт UPDATE), дээр нь нэвтрэлтийн rate limit. Бүртгэлгүй и-мэйл ба хүчингүй код ижил хариу авна.
  • Устсан: /api/v1/verify/landed, токентой /auth/credential ба /auth/credential/redeem, EMAIL_VERIFY_REDIRECT_HOSTS. Шинэ: POST /api/v1/verify/check. Migration 00140.

[1.29.0] - 2026-10-05

Added — Сайтыг нээхээс өмнөх заавал хийх алхмууд (2026-10-05)

  • Юу: Анхны тохиргоо консол хүртэл хүрээд сайт хаалттай үлддэг байсан; одоо «Эхлүүлэх» нь серверээс уншдаг жагсаалт (GET /api/platform/v1/launch, internal/operator/launch): заавал — системийн орчин (шифрлэлтийн түлхүүр, PUBLIC_ORIGIN), ажилладаг нэвтрэх арга (асаалттай eID холбогдсон байх), и-мэйл илгээх, админтай эхний байгууллага; санал болгох — AI, Gerege Core бүртгэл.
  • Хамгаалалт: platform.launched-ийг true болгохыг сервер заавал хийх алхам үлдсэн бол татгалзана (launch.ErrNotReady); хаах нь үргэлж боломжтой. Сайт хаалттай үед консолын нүүр «Эхлүүлэх» рүү шилжиж, бүх дэлгэцийн дээр тууз харагдана.
  • Түлхүүр: EMAIL_VERIFY_API_KEY консолын credential боллоо (email.verify_api_key), env нь fallback.
  • Шалгасан: TestTheSiteIsNotReadyWithoutMail, TestAnOrganisationCountsOnceItHasAnAdmin, TestTheSiteOpensOnlyWhenReady, go test ./... (CI-ийн бүх *_TEST_DATABASE_URL-тэй), npx vitest run.

Fixed — Шинэ суулгацын анхны админы тойрог (#166) (2026-10-05)

  • Юу: Анхны байгууллагын админ eID-ээр баталгаажсан платформын хэрэглэгч байх ёстой ч тийм хүн зөвхөн хаалттай сайт дээр нэвтэрч үүсдэг байсан. Одоо: супер админ хэрэглэгчийг регистрийн дугаар, нэр, сонголтоор и-мэйлээр урьдчилан бүртгэнэ (POST /api/platform/v1/people, tenant.create, аудиттай; auth.PreRegisterEIDPerson). Тэр хүн eID-ээр анх нэвтрэхэд регистрээрээ (civil ID хараахан бөглөгдөөгүй мөр л) энэ данс руу орж, eID холболт бөглөгдөнө. «Шинэ байгууллага» цонх ийм хүнийг газар дээр нь бүртгэж сонгоно. Оператор консолд eID-ээ холбох/eID-ээр нэвтрэхэд өөрөө ч платформын хэрэглэгч болно.
  • Бусад: «Бүртгэлээс хайх» зөвхөн core.api_token тохируулсан үед гарна (өмнө нь 503).
  • Шалгасан: TestAPreRegisteredPersonSignsInByRegisterNumber, TestAnOperatorsOwnEIDIsProvisionedOnAPrivatePlatform, go test ./..., npx vitest run (366).

[1.28.0] - 2026-10-04

Changed — Ажлын муж хоёр суурь апптай; товч, оролт 44px (2026-10-04)

  • Ажлын муж: Платформ ба Тохиргоо — хоёр built-in. Хувийн гэрт «Хувийн платформ» («Миний гэр» бүлэг), байгууллагад «Байгууллагын платформ» («Байгууллага» бүлэг); хоёулаа layout-grid. Тохиргоо: «Тохиргоо» бүлэгт Харагдац (байгууллагад нэмж Хандалтын тохиргоо, join_requests.manage), сүүлд «Апп Дэлгүүр» бүлэг. Профайл, Байгууллага, Хандалт суулгадаг core апп байхаа больсон; Төхөөрөмж, демо аппууд App Store-оос. Хувийн ажлын муж «Хувийн гэр» гэж нэрлэгдэнэ (#159, #160).
  • Консол: App Store-оос апп суулгах/асаах/унтраах/устгахад rail шууд шинэчлэгдэнэ (useReloadConsoleApps, #159). «Эхлүүлэх» Тохиргоо апп руу шилжсэн (#158).
  • UI: товч, оролт, select, pill таб 44px (--style-control-height); нүүр, консол, нэвтрэх, зөвшөөрлийн толгой 60px, доторх хэлний сонголт ба «Нэвтрэх» 36px (#161–#164).
  • Шалгасан: go test ./... (Postgres 16), npx vitest run, хөтөч дээр хэмжилт (локал ба production).

Changed — Консол хоёр суурь апптай: Платформ ба Тохиргоо; Ops суулгадаг апп (2026-10-04)

  • Юу: Профайл ба Байгууллагууд Платформ апп руу, App Store Тохиргоо апп руу нэгдсэн (Тохиргоо: тохиргоо, туслах, имэйл баталгаажуулалт, операторууд, App Store; rail-ийн ёроолд). Үйл ажиллагаа (Ops) App Store-оос суулгаж/хасдаг цөмийн апп боллоо. Хасахад зөвхөн tile нь алга болно — API нь эрхээрээ хамгаалагдсан хэвээр.
  • Түгээлт: консолын апп бүр nexus.RegisterConsoleBase-ээр бүртгэгдэнэ (pkg/nexus/console.go). Түгээлт host.Options.Modules дотроос ижил id-гаар өөрийнхийгөө бүртгэвэл цөмийнхийг сольж, tile/цэс/API-г нь цөмд гар хүрэлгүй өөрчилнө.
  • Migration: 00139 — ажиллаж буй консол Ops-оо алдахгүй (00110-ийн мөр үлдэнэ); шинэ суулгац (оператор үүсээгүй) Ops-гүй эхэлнэ. Профайл/Байгууллагууд/Платформын хуучин мөрийг арилгана.
  • Шалгасан: TestTheConsoleHasTwoBaseApps, TestOpsIsInstalledFromTheStore, TestADistributionReplacesABaseApp, go test ./... (Postgres 16 дээр), 00139-ийг шинэ ба шинэчлэгдэх баазад гараар; npx vitest run.

Changed — Консол шалтгаан ба step-up код асуухаа больсон (2026-10-04)

  • Юу: Операторын консол үйлдэл бүрт шалтгаан бичүүлэх, нэвтэрсний дараа TOTP кодыг дахин асуух (step-up) хоёрыг болиулсан. useAction нь энгийн баталгаажуулах цонх боллоо; тохиргоо, оператор, байгууллага, зарлалын цонхнуудаас шалтгаан ба кодын талбар хасагдсан.
  • Backend: operator.Console.Do шалтгаан шаардахаа больсон (ErrReasonRequired устсан); RequireStepUp middleware ба бүх маршрут дээрх хэрэглээ хасагдсан. Аудит мөр хэвээр бичигдэнэ (шалтгаан хоосон). Хамгаалалт: нэвтрэлтийн хоёр хүчин зүйл, үүргийн эрх (RequireCapability), аудит.
  • Үлдсэн: /step-up endpoint ба stepped_up_at багана ашиглагдахгүй ч устгаагүй.
  • Шалгасан: go test ./... (Postgres 16), npx vitest run.

Added — eID relying party-г консолоос тохируулна (2026-10-03)

  • Юу: eid.rp_uuid, eid.rp_secret консолын түлхүүр болсон (env EID_RP_UUID/EID_RP_SECRET нь fallback). Нэвтрэлт (identity/eid), гарын үсэг (identity/eidmongolia) ба консолын eID нэвтрэлт клиентээ одоогийн утгаар үүсгэж, утга өөрчлөгдөхөд дахин ачаалалгүй шинэчилнэ. Консолын eID хаалга EIDGateway.Ready()-ээр тухай бүр шалгагдана. «Эхлүүлэх» хуудасны eID мөр Тохиргоо → Түлхүүрүүд рүү хөтөлнө.
  • Хамгаалалт: production-д EID_VERIFY_RESPONSE enforce биш бол консолоос тавьсан relying party-г тохируулаагүй гэж үзнэ (config.EIDVerificationAllowed, boot-ийн дүрэмтэй ижил). Дансны түлхүүр: EID_ACCOUNT_KEY → env EID_RP_SECRET → консолын нууц — одоо байгаа суулгацын digest өөрчлөгдөхгүй.
  • Шалгасан: TestTheRelyingPartyFollowsTheCredentials, go test ./internal/... ./pkg/....

[1.27.0] - 2026-10-03

Added — Консолын «Эхлүүлэх» хуудас ба сайтыг нээх унтраалга (2026-10-03)

  • Юу: Шинэ platform.launched тохиргоо (анхдагч true — одоо байгаа суулгацууд өөрчлөгдөхгүй). Анхны тохиргооны шидтэн операторыг үүсгэхдээ нэг transaction-д false болгоно. Хаалттай үед: auth.IssueSession бүх нэвтрэлтийг 403 ErrNotLaunched-аар татгалзана; /, хэсгийн хуудсууд ба /login /opening («Тохиргоо хийгдэж байна») руу шилжинэ; /setup/status ба /auth/sso/config launched-ийг өгнө.
  • Консол: Платформ апп-д «Эхлүүлэх» (/cp/start) — eID холболт, нэвтрэх арга ба горим (/cp/config руу), эхний байгууллага (/cp/tenants руу) гэсэн жагсаалт ба «Сайтыг нээх / хаах» товч (шалтгаан + step-up, байгууллагагүй бол нээх идэвхгүй).
  • Хаана: backend/internal/kernel/settings/keys.go, workspace/auth/handlers.go, workspace/identity/sso.go, operator/operator/bootstrap.go, operator/setup/setup.go, operator/apps/builtin.go; frontend/app/cp/start, app/opening, lib/setup.ts (siteStateOnServer), app/login/page.tsx.
  • Шалгасан: TestNobodySignsInBeforeLaunch, TestTheFirstOperatorIsTheOnlyOne (сайт хаагдсан эсэх), section-screen.test.tsx; шинэ DB дээр шидтэн → / ба /login → /opening → консолд нэвтрэх → /cp/start (1440px ба 375px, хажуу гүйлгээгүй).
  • Үлдсэн: eID RP-ийн нууцыг консолоос тохируулах (одоогоор .env).

Changed — Анхны тохиргоо консолоос эхэлдэг болсон (2026-10-03)

  • Юу: /setup одоо зөвхөн удирдлагын консолыг тохируулна: системийн шалгалт (шинэ «Удирдлагын консол» мөр — CONTROL_PLANE_HOST ба CONTROL_PLANE_ALLOWED_CIDRS/CONTROL_PLANE_ALLOW_ALL; production-д дутвал блоклоно), консолын анхны супер админ (нэр, и-мэйл, ≥12 тэмдэгт нууц үг), баталгаажуулагч (QR → 6 оронтой код). Дуусахад «Консол руу орох» товч консолын хост руу (https://<CONTROL_PLANE_HOST>/cp) хөтөлнө. Байгууллага, хэрэглээний горим, eID-ийн сонголтыг шидтэнээс хассан — тэдгээрийг консолоос хийнэ.
  • Хаалга: оператор ч, байгууллага ч байхгүй үед л нээлттэй; аль нэг нь үүсмэгц /checks, /enrol, /complete 404. Бүртгэл operator.CreateFirstOperator-оор нэг transaction-д advisory lock дор, кодоор баталгаажсан TOTP-тэй бичигдэнэ — хоёр хүн зэрэг дуусгавал хоёр дахь нь ErrNotFirst.
  • Хаана: backend/internal/operator/setup/setup.go, operator/bootstrap.go (CreateOperator-той нэг createOperator), operator/address.go (AddressesConfigured), frontend/app/setup/page.tsx, lib/i18n/addons/setup.ts, lib/api/client.ts.
  • Шалгасан: go test ./internal/operator/... ./pkg/host/ (шинэ TestTheFirstOperatorIsTheOnlyOne, консолын шалгалт, хаалга), frontend setup-wizard.test.tsx ба бүх 365 тест.
  • Үлдсэн: консолын «Эхлүүлэх» хуудас ба сайтыг нээх унтраалга (platform.launched) — дараагийн PR.

Changed — Консолын үндсэн апп-ууд байнгын болсон (2026-10-03)

  • Юу: Профайл, Платформ, Байгууллагууд, Ops апп-ууд App Store-ын адил консолын суурь: rail дээр үргэлж харагдаж, суулгасан/идэвхтэй гэж жагсаагдана; устгах, идэвхгүй болгох, шинэчлэхийг ErrBuiltin-ээр татгалзана. operator.console_apps дахь тэдгээрийн мөр (00110, 00121) цаашид нөлөөгүй — өмнө нь устгасан эсвэл идэвхгүй болгосон core апп дахин гарч ирнэ.
  • Яагаад: Платформ эсвэл Байгууллагуудыг устгавал оператор зайлшгүй дэлгэцүүдийн цэсээ алддаг байсан (хаягаар нь орж болох хэвээр). Консолоос эхлэх setup-ын суурь.
  • Хаана: backend/internal/operator/apps/builtin.go, store.go, тест apps_db_test.go.
  • Шалгасан: go test ./internal/operator/apps/ — идэвхгүй/устгасан мөртэй үед ч rail-д байх, гурван үйлдэл татгалзах.

[1.26.0] - 2026-10-03

Changed — Нэвтрэх дэлгэц: нэр/нууц үг гол, eID нь товч (2026-10-03)

  • Юу: Нууц үгээр нэвтрэх асаалттай (auth.username_login) үед нэвтрэх карт нь нэвтрэх нэр/и-мэйл ба нууц үгийн форм; доор нь «эсвэл»-ийн дараа eID-ээр нэвтрэх, Passkey, Google товчнууд (passkey-ийн адил). eID товч картанд eID-ийн урсгалыг (регистр / QR) «Буцах»-тай нээнэ. eID/username табууд хасагдсан. Зөвхөн eID асаалттай бол өмнөх шигээ eID шууд харагдана.
  • Хаана: frontend/app/login/page.tsx, lib/i18n/addons/auth.ts, тест login-password.test.tsx, e2e workspace.spec.ts.
  • Шалгасан: frontend тест (товч нээх/буцах шинэ тест), 375px ба 1440px хажуу гүйлгээгүй.

[1.25.0] - 2026-10-03

Added — Утасны апп: серверийн route тохиргоо, баримт скан (bridge v1.6), Android биометр (2026-10-03)

  • Route тохиргоо: апп <base>/.well-known/gerege-shell-routes.json-ийг pinned клиентээр татаж кэшлэнэ; өөрийн гарлын зам бүр web / external / native:<таб>-аар нээгдэнэ (холбоос, хадгалсан мөр, гол frame-ийн шилжилт). Апп шинэчлэхгүйгээр замыг web↔native болгоно. RouteConfig (Swift + Kotlin, ижил тест).
  • Bridge v1.6: document.scan → { base64, mime: "application/pdf", pages } (iOS VisionKit, Android ML Kit document scanner); iOS ба Android fs.saveAs (системийн файл сонгогч).
  • Android биометр: biometric.authenticate, auth.lock (BiometricPrompt, биометр эсвэл PIN); хадгалсан нэвтрэлтийг сэргээхэд түгжээ (iOS-ийн passBiometricGate-ийн дүйцэл).
  • Шалгасан: swift test (28), ./gradlew test assembleDebug, iOS ба macOS build, check-shell-contract.
  • Үлдсэн: simulator дээр VisionKit скан боломжгүй (reject); Android скан Play services шаардана.

[1.24.0] - 2026-10-03

Added — Bridge v1.5: saved — ажлын мужийн хуудсыг утсан дээр офлайн хадгалах (2026-10-03)

  • Юу: saved.put / saved.remove / saved.list method ба saved capability. Ажлын муж хуудсаа (гарчиг, тайлбар, ≤6 мөр төлөв) хадгалуулж, бүрхүүл түүнийг Keychain / Android Keystore-оор шифрлэж самбар дээр «Хадгалсан» хэсэгт сүлжээгүй үед ч харуулна; мөрийг дарахад ажлын муж тэр замыг нээнэ. Гарахад устна.
  • Хаана: frontend/lib/shell.ts, docs/SHELL_CONTRACT.md (§4, §5, §7, §9), GeregeShellKit/SavedItem.swift (+тест), shell/SavedItem.kt (+тест), iOS Features/Saved/SavedStore.swift, Android SavedStore.kt, ShellBridge-ууд. Гэрээний хувилбар 1.5 (бүх клиент).
  • Шалгасан: swift test, ./gradlew test, check-shell-contract; emulator дээр хадгалах → самбар → airplane mode-д дахин нээх → мөр дарахад паспорт нээгдэв.
  • Үлдсэн: macOS, Windows saved-ийг зарлахгүй (reject → web тал товчоо нуудаг).
  • Юу: https://<applinks host>/… холбоос аппыг нээж, ажлын мужид тэр хуудсыг ачаална (нэвтрээгүй бол нэвтэрсний дараа). Холбооснаас зөвхөн зам+query авна — хуудас үргэлж аппын өөрийн суурь хаяг дээр.
  • Хаана: GeregeShellKit/WorkAreaLink.swift (+тест), shell/WorkAreaLink.kt (+тест); iOS onOpenURL → WorkAreaNavigation.open, Android autoVerify intent-filter (appLinksHost, appLinksPathPrefix брэндээс) → AppState.pendingLink. Android Debug-д EID_DEBUG_TAB (iOS-ийн дүйцэл).
  • Шалгасан: swift test, ./gradlew test; emulator дээр e-property.mn/units/… холбоос паспортыг ажлын мужид нээв.
  • Үлдсэн: Android-ийн баталгаажуулалт release гэрчилгээний assetlinks.json-оос хамаарна.

[1.23.0] - 2026-10-03

Анхны тохиргооны шидтэнгийн шинэ 4 алхамт урсгалын код backend/v1.22.0-д орсон боловч тэмдэглэл нь энд байна; энэ хувилбар шидтэнг токенгүй болгосон.

Changed — Анхны тохиргооны шидтэн: Nexus Light-ийн 4 алхамт урсгал (2026-10-03)

  • Юу: /setup-ийг дахин бичив — Gerege Nexus Light-ийн урсгалаар, өөрийн дизайнаар:
  • Системийн шалгалт — PostgreSQL холболт ба хувилбар, схем энэ хувилбартай таарч буй эсэх, Redis, INTEGRATION_ENCRYPTION_KEY, PUBLIC_ORIGIN; мөр бүр хэвийн / анхаар / засах шаардлагатай. «Засах шаардлагатай» байвал цааш явахгүй (production-д шифрлэлтийн түлхүүр, нийтийн хаяг заавал).
  • Байгууллага ба админ — нэр, богино нэр (нэрнээс автоматаар латинжина), админы и-мэйл, нууц үг ×2.
  • Хэрэглээний горим — Хаалттай / Нээлттэй (platform.access_mode) ба eID нэвтрэлт (auth.eid_login).
  • Хянаж эхлүүлэх — бүгдийг нэг POST /setup/complete-ээр, tenants.Bootstrap-ийн нэг transaction-д. Карт дэлгэцийн өргөнд (max-w-4xl) тааруулагдана; утсан дээр нэг багана.
  • Хадгалсан: setup токен (лог руу нэг удаа, 404 хаалга, дуусмагц хаагдана). Токен одоо зөвхөн X-Setup-Token header-ээр — хаягийн ?token= (лог, түүхэнд үлддэг) хүлээн авахгүй.
  • Хасагдсан: Gerege Core регистрээр хайх (POST /setup/organisation), шидтэн доторх консолын эхний оператор (POST /setup/operator, /operator/confirm) — оператор cmd/operator-bootstrap-аар. Шинэ: GET /setup/checks.
  • Light-ийн алдааг давтаагүй: нэр тэмдэгтээр тоологдоно (кирилл байтаар биш), хоосон нэр татгалзана, бүх бичилт нэг transaction, шалгалт complete дээр дахин хийгдэнэ.
  • Засвар: SignInFrame-ийн grid-ийн auto багана картыг агуулгын хэмжээгээр (Stepper) тэлж, утсанд хажуу гүйлгээ үүсгэж байсан — grid-cols-1 болов.
  • Шалгасан: backend тест (токен хаалга, алхам 2–3-ын шалгалт, кирилл 200 тэмдэгт, DB дээрх системийн шалгалт, bootstrap-ийн горимын тохиргоо), route golden; frontend 364 тест (шидтэнгийн 2 шинэ); хоосон DB дээр хөтчөөр 4 алхмыг дуустал (байгууллага, тохиргоо бичигдэж, /checks 404 болов); 375px ба 1440px-д хажуу гүйлгээгүй.

Changed — Setup шидтэн токен асуухгүй (2026-10-03)

  • Юу: /setup нээгдэхэд токен асуухгүй, шууд 1-р алхамаас эхэлнэ (Nexus Light-ийн адил). Шидтэнгийн алхмууд (GET /setup/checks, POST /setup/complete) зөвхөн байгууллага байхгүй үед хариулна; эхний байгууллага үүссэн мөчөөс 404. Зэрэг хоёр хүн дуусгавал tenants.Bootstrap-ийн lock зөвхөн эхнийхийг зөвшөөрнө.
  • Эрсдэл: интернэтэд ил шинэ суулгацыг хамгийн түрүүнд /setup-д хүрсэн хүн тохируулна. Суулгацаа асаамагц шидтэнгээ дуусгах, эсвэл ил гаргахаас өмнө tenant-bootstrap командыг ашиглах. Эхлэх лог үүнийг анхааруулна.
  • Хасагдсан: boot үеийн setup токен, X-Setup-Token header (CORS-оос ч), токен оруулах дэлгэц.
  • Шалгасан: хоосон DB дээр токенгүй checks 200 → complete 201 → checks ба дахин complete 404; backend тест (байгууллагатай үед алхмууд 404), frontend 364 тест.

[1.22.0] - 2026-10-03

Changed — Monitoring landing: нээлттэй самбарууд hero-гийн доор эхэнд (2026-10-03)

  • Юу: monitoring.nexus.gerege.mn-д «Нээлттэй хяналтын самбарууд» hero-гийн дараах эхний хэсэг боллоо (01 · Самбарууд, 02 · Хаана орох); толгойн цэс ч тэр дарааллаар.
  • Хаана: deploy/nginx/monitor-landing/index.html. 375px: хажуугийн гүйлгээгүй.

Added — Утасны апп: distribution-ийн брэндийн overlay (2026-10-03)

  • Юу: Distribution бүр (эхнийх нь E-Property) утасны аппыг өөрийн нэр, bundle/application ID, хост, app2app scheme, брэндийн хоёр өнгөтэй fork хийлгүй барина. Overlay өгөөгүй бол бүх утга өмнөхтэй ижил.
  • Хаана: iOS native-apps/mobile/ios/Brand.xcconfig ← Brand.local.xcconfig (#include?, gitignore); Android -PbrandFile=…/brand.properties (жишээ); AppConfig, AuthCallback.scheme, Theme.Brand, Gw.kt. Тайлбар: native-apps/README.md § Брэнд.
  • Шалгасан: анхдагч build-ийн Info.plist/APK өмнөхтэй ижил; E-Property overlay-р APK mn.eproperty.app, iOS simulator дээр нэр/өнгө/хост зөв. CI жишээ брэндээр Android барьж package name шалгана.
  • Үлдсэн: шинэ scheme бүрийг EID_APP_CALLBACKS ба eID RP-ийн callback_hosts-д бүртгэнэ.

Added — Monitoring landing: нээлттэй хяналтын самбарууд (2026-10-02)

  • Юу: monitoring.nexus.gerege.mn-ийн «Самбарууд» хэсэг дэлгэцийн зурагтай дөрвөн карт боллоо (API тойм, Инфраструктур, Тэсвэрлэлт, Гадаад системүүд) — petronet-gerege-nexus-ийн загвараар. Карт бүр Grafana-ийн shared dashboard (нэвтрэлтгүй, зөвхөн унших, бараан загвар) руу нээгдэнэ. Anonymous нэвтрэлт биш: зөвхөн самбарт хадгалсан асуулга ажиллана; засах, Explore, бусад самбар нэвтрэлтийн ард хэвээр.
  • Хэрхэн: setup_monitor_branding.sh самбаруудыг нийтэд нээж (PUBLIC_BOARDS), /boards/<uid> → токен холбоосын nginx snippet (snippets/monitor-nexus-public-boards.conf) ба cards/<uid>.png агшинг (headless Chromium) бичнэ. Landing токен агуулахгүй — deploy бүр дахин суулгадаг ч эвдрэхгүй.
  • Засвар: 375px дэлгэцэнд header-ийн «Grafana руу нэвтрэх» товч хуудсыг 465px хажуу тийш түлхэж байсан — утсан дээр нуув (hero-ийн нэвтрэх карт доор нь).
  • Хаана: deploy/nginx/monitor-landing/index.html, deploy/nginx/monitoring.nexus.gerege.mn.conf, deploy/scripts/setup_monitor_branding.sh.

Changed — Байхгүй болон салсан downstream-уудыг салгав (2026-10-02)

  • Юу: eid-mongolia-mn (2026-09-27-нд fork, цөмийг дагахгүй) ба eid-gerege-nexus (GitHub дээр байхгүй)-ийг одоогийн downstream гэж тооцохоо больсон. docs/DISTRIBUTION.md-ийн «Одоо хэн» хүснэгт бодит downstream-уудыг жагсаана (Go dependency 8, snapshot 3); жишээнүүд petronet-gerege-nexus руу шилжив.
  • Тест: numbering_test.go-ийн 00113 залгаасны онцгой зөвшөөрөл хасагдав — дугаар нөөцлөгдсөн хэвээр, гэхдээ цөмд ямар ч файл (залгаас ч) авахгүй. operator/backup тестийн жишээ репо нэр саармаг болов.
  • Хөндөөгүй: түүхэн тэмдэглэл (CHANGELOG, хэрэглэгдсэн migration-ий тайлбар, spec-үүд).

Fixed — Нийтийн самбарууд «Өгөгдөл алга» харуулж байсан (2026-10-02)

  • Юу: Provision хийгддэг 7 самбарын panel-уудад id байгаагүй. Grafana-ийн shared (public) dashboard panel бүрийг /panels/<id>/query-ээр асуудаг тул бүх panel нэг болж давхцаж, «Өгөгдөл алга» гэж харагдаж байв. Panel бүрт (row-ийг оруулаад) давтагдахгүй id нэмэв.
  • Хамгаалалт: deploy/scripts/check_dashboards.py (CI-ийн «Monitoring config») id-гүй эсвэл давхардсан id-тай самбарыг унагана.
  • Шалгасан: нэвтрэлтгүйгээр api/public/dashboards/<token>/panels/1/query өгөгдөл буцаана; картын агшин графиктай болсон.

[1.21.0] - 2026-10-02

Added — Нууц үгээр нэвтрэхэд хоёр шатлалт баталгаажуулалт (TOTP) (2026-10-02)

  • Юу: Профайл дээр «Хоёр шатлалт баталгаажуулалт» асаавал нууц үгээр нэвтрэхэд authenticator аппын 6 оронтой код (эсвэл нэг удаагийн нөөц код) нэмж асууна. Асаахад 10 нөөц код нэг л удаа харагдана, бусад session-ууд хаагдана. Унтраахад одоогийн код эсвэл нөөц код шаардана.
  • Аюулгүй байдал: TOTP нууц INTEGRATION_ENCRYPTION_KEY-ээр битүүмжлэгдэнэ; нэг код нэг л удаа (сүүлд зарцуулсан алхам); нөөц кодоос зөвхөн sha256; нууц үгийн дараах challenge 5 минут, 5 оролдлого, нэг удаа. Буруу код нууц үгийн адил account-ийн алдааны тоолуурт орно (5 → 15 мин түгжинэ), зөв нууц үг тоолуурыг кодыг зөв оруулах хүртэл тэглэхгүй. Асаахад сүүлийн 10 минутад нэвтэрсэн session хэрэгтэй.
  • Нөлөөгүй: eID, ДАН, Google, SSO, embed нэвтрэлт.
  • Хаана: migration 00137 (registry.user_totp, user_recovery_codes, login_challenges — grant-гүй), kernel/totp (operator консолоос салгасан), workspace/auth/secondfactor.go, workspace/profile/twofactor.go, frontend/app/login/page.tsx, components/profile/TwoFactorSection.tsx. Route golden: POST /api/v1/auth/login/second-factor, GET /api/v1/profile/2fa, POST /api/v1/profile/2fa/{setup,confirm,disable}.
  • Шалгасан: нэвтрэлт (challenge, зөв/буруу код, давтан код, нөөц код нэг удаа, түгжээ), бүртгэл (хуучин session татгалзана, битүүмжлэл, давхар setup 409, унтраах), migration-ий эрхийн тестүүд, frontend код алхам.
  • Үлдсэн: passkey (WebAuthn); Google/SSO нэвтрэлтэд 2FA.

Added — Passkey-ээр нэвтрэх (2026-10-02)

  • Юу: Профайл → «Passkey» хэсэгт утас/компьютерийн хурууны хээ, царай, PIN-ээр passkey нэмнэ (нэг хүнд олон, нэртэй). Нэвтрэх картанд «Passkey-ээр нэвтрэх» товч — юу ч бичихгүй, төхөөрөмж нь хэний passkey болохыг хэлнэ. Passkey бол бүтэн нэвтрэлт (2FA асуухгүй), session auth_method = passkey.
  • Аюулгүй байдал: user verification ба resident key заавал; RP ID нь PUBLIC_ORIGIN-ий host (Host header биш) тул бүх subdomain-д нэг passkey; зөвшөөрөгдсөн origin — web origin ба түүний доорх ALLOWED_ORIGINS. Begin/finish хоорондох ceremony 5 минут, нэг удаа. Sign count буурвал (clone warning) нэвтрүүлэхгүй, audit-д бичнэ. Нэмэхэд сүүлийн 10 минутад нэвтэрсэн session хэрэгтэй. Embed (iframe) дотор товч гарахгүй.
  • Хаана: migration 00138 (registry.user_passkeys, passkey_ceremonies — grant-гүй), workspace/auth/passkey.go (github.com/go-webauthn/webauthn v0.18), frontend/lib/passkey.ts (сан нэмээгүй, хөтчийн WebAuthn API), components/profile/PasskeysSection.tsx. Route golden: POST /api/v1/auth/passkey/{begin,finish}, GET|POST /api/v1/profile/passkeys, POST /api/v1/profile/passkeys/begin, DELETE /api/v1/profile/passkeys/{id}.
  • Шалгасан: жинхэнэ go-webauthn-ийн эсрэг программ authenticator-аар бүртгэл → нэвтрэлт, ceremony давтахгүй, өөр origin татгалзана; base64url ↔ ArrayBuffer хөрвүүлэлт.

Fixed — Анхны админ нэвтэрч чадахгүй байсан, prod env загвар асахгүй байсан (2026-10-02)

  • Юу: Setup wizard ба tenant-bootstrap анхны админыг зөвхөн нууц үгтэй үүсгэдэг ч auth.username_login анхдагчаар унтраалттай тул тэр админ нэвтрэх боломжгүй байв. Одоо эхний байгууллага үүсэх transaction дотор username нэвтрэлтийг асаана (аль хэдийн асаалттай бол хөндөхгүй), history-д оператор NULL-ээр бичигдэнэ.
  • Юу (deploy): deploy/.env.prod.example нь EID_VERIFY_RESPONSE=warn-ийг EID_VERIFY_RESPONSE_ALLOW_WARN-гүй өгдөг байсан тул EID_RP_UUID тавьмагц production асахаас татгалздаг байв. ALLOW_WARN=true, EID_RP_UUID, EID_RP_SECRET түлхүүрүүдийг нэмэв.
  • Хаана: operator/tenants/bootstrap.go, kernel/settings/store.go (Set хоосон operatorID-г NULL болгоно), deploy/.env.prod.example.
  • Шалгасан: TestBootstrapMakesAnAdministratorWhoCanSignIn засваргүйгээр унаж, засвартай давна; backend go test ./... шинэ Postgres + бүх migration дээр ногоон.

Changed — monitor.nexus.gerege.mn → monitoring.nexus.gerege.mn (2026-10-02)

  • Юу: Ажиглалтын стекийн домэйн monitoring.nexus.gerege.mn боллоо. Хуучин monitor.nexus.gerege.mn үйлчлэхээ больсон (redirect-гүй); түүний гэрчилгээг хассан.
  • Сервер (2026-10-02-нд хийгдсэн): monitoring.nexus.gerege.mn гэрчилгээ (certbot nginx), серверийн .env.monitoring-д MONITORING_PUBLIC_URL=https://monitoring.nexus.gerege.mn (хуучин нь .env.monitoring.bak-20261002), Grafana ба Alertmanager дахин асаасан, nginx-ийн хуучин vhost ба monitor.nexus.gerege.mn гэрчилгээ хасагдсан.
  • Хаана: deploy/nginx/monitoring.nexus.gerege.mn.conf (хуучин файлыг нэрлэж сольсон), deploy-production.sh (VHOSTS, хуучин vhost-ийг устгана), check-private-nginx.sh, .env.monitoring.example, setup_monitor_branding.sh, landing page, docs/OPERATIONS.md.
  • Үлдсэн: Grafana-ийн OAuth-ийг асаах үед PRODUCTION_ENV-ийн OAUTH_REDIRECT_HOSTS-д monitoring.nexus.gerege.mn нэмэх. monitor.open.gerege.mn (open stack) хөндөөгүй.

Added — Google нэвтрэлтийг консолоос асаах/унтраах (2026-10-02)

  • Юу: Хоёр шинэ console setting, restart-гүй үйлчилнэ: auth.google_login (анхдагч true, env AUTH_GOOGLE_LOGIN) — унтраавал нэвтрэх картанд Google товч гарахгүй, /auth/google/{start,link,callback} татгалзана; auth.google_allowed_domains (env fallback GOOGLE_LOGIN_ALLOWED_DOMAINS) — Google-ээр нэвтэрч болох домэйнууд.
  • Хэвээр (env): GOOGLE_LOGIN_CLIENT_ID / SECRET / REDIRECT_URI / TENANT, SSO_CLIENT_*, EMBED_HOSTS, DAN_* — client нь boot үед эдгээрээс үүсдэг тул консолд гаргавал restart хүртэл үйлчлэхгүй "худал дэлгэц" болно.
  • Хаана: kernel/settings/keys.go, workspace/identity/google.go, workspace/ssoclient/google.go.
  • Шалгасан: TestGoogleClosesWhenTheConsoleSwitchesItOff (switch асаалттай үед унадаг); backend go test ./... шинэ Postgres дээр ногоон.

Fixed — eID QR нэвтрэлт дуусдаггүй байсан (2026-10-02)

  • Юу: eID Mongolia 2026-10-02-нд хуучин статик …/dl?sessionId=…&vc=… холбоосыг унтраасан (developer.eidmongolia.mn §4.2): апп DEVICE_LINK_REQUIRED харуулж, session RUNNING → TIMEOUT. Prod EID_DEVICE_LINK_MODE-гүй (= legacy) байсан тул QR уншуулсан иргэн нэвтэрч чадахгүй байв. Одоо EID_DEVICE_LINK_MODE анхдагчаар (ба танихгүй утгад) v3: QR нь elapsedSeconds + authCode-той, секунд тутам шинэчлэгдэнэ; утсан дээр Web2App. legacy-г тусгайлан тавьсан бол эхлэхэд WARN.
  • Хаана: kernel/eidrp/devicelink.go (ParseDeviceLinkMode), workspace/identity/eid/eid.go, docker-compose.prod.yml (${EID_DEVICE_LINK_MODE:-legacy} → хоосон; эхний deploy үүнээс болж legacy хэвээр байв), deploy/.env.prod.example, .env.example, docs/OPERATIONS.md.
  • Шалгасан: v3 холбоос CA-ийн golden вектортой байт-ижил (өмнөх тест), горимын тест шинэчлэгдсэн, backend go test ./....

Removed — eID-ийн хуучин sso.gov.mn OAuth зам (2026-10-02)

  • Юу: POST /api/v1/auth/eid/login (sso.gov.mn-ийн OAuth code солих, mock OTP) ба түүний ExchangeCode / AuthenticateWithMethod / GetAuthorizeURL / userinfo, eid.Provider interface, EID_CLIENT_ID / EID_CLIENT_SECRET / EID_AUTHORIZE_URL / EID_TOKEN_URL / EID_USERINFO_URL, анхдагч gerege-open-erp-client client id. Frontend (loginWithEID), mobile, desktop аль нь ч дууддаггүй байв; eID нэвтрэлт бүхэлдээ QR / same-device / push (/auth/eid/start|start-id|poll)-ээр явна.
  • Өөрчлөлт: integrations самбар дээрх eID-ийн Mode() EID_CLIENT_SECRET-ээр биш EID_RP_UUID-ээр "live" болно, Endpoint() нь CA-ийн хаяг (config.EIDBaseURL()). Тайлбар дахь "Gerege ID"-г "eID" болгов.
  • Хэвээр: identity/gerege (ХУР ба тоон гарын үсэгт ашиглагддаг), EID_PROVIDER registry.
  • Шалгасан: backend go test ./... шинэ Postgres дээр ногоон, route golden шинэчлэгдсэн, frontend tsc / eslint цэвэр.

Added — Mini App: Nexus дотор бусдын апп, бусдын дотор Nexus (2026-10-01)

  • Юу (host): type:"external" + external.embed:"iframe" апп (талбар validation-д байсан, уншдаг код байгаагүй) одоо шинэ табын оронд /module/ext/<slug> дээр sandboxed frame-д нээгдэнэ. POST /api/v1/miniapps/{slug}/launch суусан эсэхийг шалгаад launch_url ба аппын OAuth2 client-д зориулсан 5 минутын id_token (JWKS-ээр шалгагдана) буцаана; consent-ийг /oauth2/auth-тай ижил дүрмээр нэг удаа асууна. postMessage протокол (frontend/lib/miniapp/protocol.ts) ба гуравдагч талын SDK (/miniapp-sdk.js). CSP-д frame-src https:.
  • Юу (guest): embed.<домэйн> шугам — Nexus нь Telegram bot, банкны апп, өөр Nexus дотор mini app. POST /api/v1/auth/embed/exchange нь EMBED_HOSTS-ийн host-ын гарын үсэгтэй launch-ыг (Telegram initData HMAC эсвэл RS256 id_token) шалгаж, холбогдсон данс руу нэвтрүүлнэ; анх удаа бол Google-ийн адил eID binding. Тэр host дээр API cookie-г SameSite=None; Partitioned болгоно, frontend frame-ancestors-ийг EMBED_FRAME_ANCESTORS-оос тавина. nginx vhost deploy/nginx/embed.nexus.gerege.mn.conf (deploy жагсаалтад; гэрчилгээ 2026-10-01-нд гарсан).
  • Засвар: OIDC install gate хүний өөрийн (/me/apps) суулгацыг тоодоггүй байсан — person гадаргуутай external апп цэсэнд гарч, нэвтрэх үед татгалзагддаг байв. Мөн хувийн цэс external аппыг manifest-ийн .read (байгууллагын эрх)-ээр шүүж, хүн өөрөө суулгасан аппаа хардаггүй байв.
  • Хаана: workspace/appinstall/{miniapp,externals}.go, workspace/ssoprovider/launch.go, workspace/identity/embed.go, kernel/security/embed.go, workspace/menu/menu.go, frontend/app/{module/ext/[slug],embed}, frontend/lib/miniapp/, frontend/proxy.ts; MINI_APPS. Route golden: POST /api/v1/miniapps/{slug}/launch, POST /api/v1/auth/embed/exchange.
  • Шалгасан: launch токен (consent, 5 мин, JWKS-ээр verify, impersonation/scope татгалзал — DB тест), хүний суулгац RLS дор, цэсний зам, Telegram initData (гарын үсэг, өөр bot, өөрчилсөн user, хугацаа), EMBED_HOSTS задлал, cookie partition зөвхөн embed host дээр, протоколын шалгалт, frame-ancestors.
  • Үлдсэн: EMBED_HOSTS/EMBED_FRAME_ANCESTORS-ийг PRODUCTION_ENV-д тавих; launch токены nonce нэг удаагийн биш; Partitioned дэмждэггүй хөтөч iframe дотор; payments/notify гүүр.

Security — eID гарын үсгийн хариуг баталгаажуулах, RAW_DIGEST_SIGNATURE, stamp-ийн орлуулгыг хадгалахгүй (2026-09-30)

eID-ийн гарын үсгийн session COMPLETE/OK гэж хариулахад Nexus буцаж ирсэн signature-ийг огт уншилгүй баримтыг «зурагдсан» гэж тэмдэглэдэг байв; гэрчилгээний серийн ба РД зөрвөл зөвхөн анхааруулга бичдэг (fail-open). CA-ийн эх кодтой (ca-eidmongolia-mn 88834efa) тулгасан шалгалтаар дараах зөрүү олдсон.

  • Баталгаажуулалт (шинэ eidrp.VerifySignedDigest, eidrp/signverify.go): гинж (нэвтрэлттэй ижил trust store), EKU emailProtection (CA-ийн SIGN profile), KeyUsage non-repudiation (нэвтрэлтийн гэрчилгээ ч emailProtection-той тул энэ нь ялгана), иргэний issuer SKI allowlist, түвшин ≥ QUALIFIED, SHA-256, Nexus-ийн өөрийн хадгалсан digest дээрх гарын үсэг (ECDSA / rsassa-pss), PNOMN-<person.civilId> = subject serial, эхлүүлсэн иргэн = person.civilId эсвэл person.regNo. Амжилтгүй бол session failed + slog.Error (шалтгаантай, PII-гүй). Гарын үсгийн объект ба гэрчилгээ state-д нотолгоо болж хадгалагдана. regNoMatches хасагдсан. Trust store-ийг eidmongolia.New (signTrustFromEnv) нэвтрэлтийнхтэй ижил env-ээс угсарна. Mock горимд хөндөгдөөгүй.
  • Протокол: гарын үсэгт signatureProtocol: RAW_DIGEST_SIGNATURE (өмнө нь ACSP_V2).
  • endResult: USER_REFUSED* бүх хувилбар → rejected, TIMEOUT → expired.
  • 403: зөвхөн onBehalfOf хүссэн үед ErrNotRepresentative; бусад үед RP-ийн эрхийн алдаа («администратортай холбогдоно уу»).
  • Хэмжээ: eID-ээр зурах PDF 10 MB (CA stamp 10 MiB-ээс илүү уншдаггүй); overlay-ийн дараа ч шалгана; тавиур дээрх 25 MB баримтыг eID-ээр зурахад 413. Frontend EidSignView 10 MB, file_too_large мөрөнд {max}.
  • Stamp-ийн орлуулга: stamp унавал Document-Signer-ийн PDF DownloadResult.Fallback тэмдэгтэй, ERROR логтой; /esign settle түүнийг хадгалахгүй — session pending үлдэж дараагийн poll stamp-ийг дахин оролдоно.
  • DB (migration 00135_a_signed_document_fits_its_state.sql): 00103 нь registry.eid_sign_state.value-ийг varchar(8192) болгосноос хойш ~6 KB-аас том бодит PDF бүр гарын үсэг эхлэх үед state бичих дээр унадаг байв (prod-д varchar(8192), мөр 0 гэж 2026-09-30-нд read-only баталсан). 10 MiB PDF-ийн base64 + JSON ≈ 14.1M тэмдэгт нь varchar-ийн дээд хязгаараас (10 485 760) их тул багана text + нэртэй CHECK eid_sign_state_value_size (octet_length(value) <= 16777216); schema_hygiene-ийн columnsThatAreDeliberatelyText-д шалтгаантай нэмсэн. Down нь varchar(8192) руу буцаана — түүнээс урт мөр (30 минутын TTL-тэй явагдаж буй ёслол) байвал зориудаар унана. Тест: db/migrations/eid_sign_state_size_test.go (14.2M тэмдэгт орно, 16 MiB+1 нь 23514).
  • Баримт: docs/OPERATIONS.md — «eID гарын үсэг: хариуны баталгаажуулалт ба stamp».

Хаана: backend/db/migrations/00135_a_signed_document_fits_its_state.sql (+тест, schema_hygiene_test.go), backend/internal/kernel/eidrp/signverify.go (+тест), backend/internal/kernel/eidsign/eidsign.go (+verify_test.go), backend/internal/workspace/identity/eidmongolia/eidmongolia.go, backend/internal/workspace/signing/handlers_sessions.go, frontend/components/esign/EidSignView.tsx, frontend/app/module/documents/pdf/page.tsx, frontend/lib/i18n/addons/esign.ts.

Шалгасан: gofmt -l, go vet ./..., go test -race (eidsign, eidrp, workspace/signing, workspace/identity/...), GOOS=linux GOARCH=amd64 go build ./.... Шинэ тестүүд: ECDSA ба rsassa-pss зөв; өөр digest, citizen биш issuer, clientAuth-only, non-repudiation-гүй, ADVANCED, өөр иргэн/person block-гүй, гарын үсэггүй, SHA-256 биш, trust store-гүй — бүгд татгалзана; endResult-ийн хувилбарууд; илгээсэн protocol/level; 10 MiB-ээс том PDF eID-д хүрэхгүй. Frontend-ийг локалд build/typecheck хийгээгүй (node_modules байхгүй) — CI. DB шаардсан migration тест локалд SKIP (TEST_DATABASE_URL байхгүй) — CI-ийн Postgres дээр ажиллана.

Үлдсэн: бодит гарын үсгээр (CA prod) шалгаагүй. settle-ийн Fallback салааны тест (DB шаарддаг) байхгүй. nexus.Signer.SignedDocument (rail) Fallback-ийг модульд дамжуулдаггүй хэвээр.

Fixed — eID stamp-ийн тасалдал зурсан гарын үсгийг алдахгүй; stamp-ийн давталт backoff + нэг оролдлоготой; prod trust store-гүй бол эхлэхгүй (2026-09-30)

PR #119-ийн go-pg-review-ийн гурван олдвор (блоклоогүй):

  • (a) Зурсан гарын үсэг алга болдог байв. eID-ийн stamp унавал session pending хэвээр үлдэж, 20 минутын дараа housekeeping түүнийг expired / abandoned болгодог; eID-ийн state (PDF, digest, гарын үсэг) 30 минутын TTL-тэй. CA-ийн ~20 минутын тасалдал иргэний аль хэдийн PIN2-оор өгсөн гарын үсгийг «орхигдсон» болгоно. Одоо: баталгаажсан гарын үсэг эхлээд шинэ stamping төлөвт бичигдэнэ (beginStamping), session-ийн хугацаа eidsign.StampWindow = 24 цаг болж, eID state completed үед 25 цаг хадгалагдана (cache.Set-д TTL-ийг eidsign төлвөөр сонгоно: хүлээж буй 30 мин, зурсан 25 ц). Sweep stamping-ийг abandoned болгохгүй; цонх дуусвал failed / stamp_unavailable + ERROR лог. CA completed session-ийг устгадаггүй (зөвхөн RUNNING-ийг timeout хийдэг — ca-eidmongolia-mn 53b2ac2d, session_repo.go), тиймээс хожуу stamp хариулагдана.
  • (b) Poll бүр 10 MiB дахин илгээдэг байв. Stamp-ийн оролдлого бүрийг мөр дээр claimStampAttempt авна: UPDATE … SET stamp_attempts+1, stamp_next_at = NOW() + backoff WHERE state='stamping' AND stamp_next_at <= NOW() RETURNING — хоёр poll, blue-green-ийн хоёр slot зэрэг байсан ч нэг л оролдлого. Backoff 30с → 1м → 2м → 4м → 5м (дээд), DB-д хадгалагдсан. Оролдлого context.WithoutCancel + 25с timeout-тай (backoff-ийн 30с-ээс бага, eID client 15с) тул poll тасарсан ч stamp хийгдсэн PDF хаягдахгүй. Хөтөч хаагдсан ч housekeeping 5 минут тутам хугацаа нь болсон stamping мөрүүдийг (≤10) ижил claim-ээр оролдоно (retryDueStamps, actor = ёслол эхлүүлсэн хэрэглэгч). Зөвхөн ErrStampUnavailable давтагдана; eID state алга бол failed / stamp_state_lost. Poll completed state-ийг хадгалж чадаагүй бол алдаа буцаана (өмнө нь Warn-аад completed гэдэг байсан — одоо энэ нь stamping руу орж засрахгүй гацаа болох байв). Stamp-ийг шинэ eidsign.Usecase.Stamp / eidmongolia.Service.StampSigned-ээр авна — Document-Signer орлуулга огт үүсгэхгүй (ErrStampUnavailable), өмнөх шиг орлуулгыг үүсгээд хаях 30с-ийн PDF ажил үгүй.
  • (c) Trust store-гүй prod. config.ValidateProduction → validateEIDTrustStore: EID_RP_UUID тохируулсан production-д eID-ийн embedded trust store угсрагдахгүй бол API эхлэхгүй (нэвтрэлт өмнө нь чимээгүй off болдог байв). eidmongolia.New → requireSignTrust: live гарын үсэг тохируулсан prod-д мөн адил алдаа буцаана. Mock, eID тохируулаагүй, prod биш орчинд хуучин зан (лог).
  • Frontend: EidSignView stamping төлөвт «Гарын үсэг баталгаажлаа — eID-ийн тамгыг хүлээж байна» + тайлбар (дахин PIN2 хэрэггүй, хуудсаа хаасан ч оролдлого үргэлжилнэ), 5 секунд тутам poll; failed/stamp_unavailable-д тусгай мессеж. SessionState-д stamping.
  • DB (migration 00136_a_signed_ceremony_waits_for_its_stamp.sql): esign_sign_sessions_state_check-д stamping, stamp_attempts smallint NOT NULL DEFAULT 0 CHECK (>= 0), stamp_next_at timestamptz, stamping мөрийн partial index. Down: stamping → pending (00135-ын зан руу).

Хаана: backend/db/migrations/00136_a_signed_ceremony_waits_for_its_stamp.sql, backend/internal/kernel/eidsign/eidsign.go (+verify_test.go, eidsign_test.go), backend/internal/workspace/identity/eidmongolia/{eidmongolia,state,mock}.go (+тест), backend/internal/workspace/signing/{handlers_sessions,handlers_batch,store,domain,housekeeping}.go (+store_db_test.go), backend/internal/kernel/config/production.go (+тест), frontend/components/esign/EidSignView.tsx, frontend/lib/esign.ts, frontend/lib/i18n/addons/esign.ts, frontend/tests/esign-sign-flow.test.tsx, docs/OPERATIONS.md.

Шалгасан: gofmt -l, go vet ./..., go test -race (eidsign, eidrp, config, workspace/signing, workspace/identity/..., cmd, db/migrations), go test ./..., GOOS=linux GOARCH=amd64 go build ./...; frontend npm ci, tsc --noEmit, eslint, vitest run (345). Шинэ тестүүд: stamp тасалдалд ErrStampUnavailable + орлуулгагүй, сэргэхэд ижил session stamp-тэй; completed state TTL ≥ StampWindow; дуусаагүй ёслолд stamp дуудагдахгүй; хадгалж чадаагүй completion алдаа буцааж дараагийн poll-оор засрана; prod trust store-гүй бол эхлэхгүй (dev/eID-гүй эхэлнэ); requireSignTrust; frontend stamping → completed, stamp_unavailable мессеж. DB тестүүд (stamping sweep-ээс амьд үлдэх, цонх дуусвал stamp_unavailable, зэрэг хоёр claim-ээс нэг нь, backoff 30с/1м/2м/4м/5м/5м, dueStamps зөвхөн хугацаа нь болсныг, failStamping pending-д хүрэхгүй) локалд SKIP (TEST_DATABASE_URL байхгүй) — CI-ийн Postgres дээр.

Үлдсэн: хуудсаа хаасан иргэнд stamp-ийн дараа session-ийг дахин нээх UI байхгүй (баримт тавиур дээр SIGNED болно). Batch-ийн poll stamping item бүрт дараалан ≤25с зарцуулж болно. Deploy-ийн цонхонд хуучин slot stamping-ийг terminal гэж үзнэ (богино хугацаа). nexus.Signer.SignedDocument (rail) Document-Signer орлуулгатай хэвээр. Бодит CA тасалдлаар шалгаагүй.

Changed — eID smoke: callback-гүй QR session, 2 цаг тутам (2026-09-30)

  • Юу/яагаад: deploy/scripts/eid-smoke.sh callbackUrl-тай session эхлүүлдэг байсан тул CA түүнийг same-device (App2App) оролдлого гэж бүртгэж, хэн ч батлахгүй тул 30 мин тутам App2App TIMEOUT болж CA-ийн статистикт «эвдэрсэн App2App» мэт харагдаж байв. Одоо callback-гүй (QR) session эхлүүлж device_link_url-ийг шалгана (legacy ?sessionId=&vc= болон v3 deviceLinkType=QR…&authCode= аль алиныг хүлээн авна). Хуваарь 7,37 * * * * → 7 */2 * * * (өдөрт 48 → 12); deploy бүрийн дараах шалгалт хэвээр.
  • Хаана: .github/workflows/eid-smoke.yml, deploy/scripts/eid-smoke.sh, docs/RUNBOOKS.md.
  • Шалгасан: bash -n; parser-ийг legacy/v3/буруу гурван жишээгээр; deploy-ийн дараа прод дээр ажиллуулж, CA DB-д «Gerege Nexus»-ийн шинэ App2App session үүсэхгүйг тоолно.

Fixed — eID v3: binding, профайлын холбох, консол утсан дээр «аппаар нээх» товчтой (2026-09-30)

  • Юу/яагаад: EID_DEVICE_LINK_MODE=v3 үед CA Web2App/App2App холбоосыг зөвхөн callback-тай session-д олгодог. Binding (binding.go), профайлын eID холбох (link_eid.go), консол (pkg/host/eid_gateway.go) QR session-ийг callback-гүй эхлүүлдэг байсан тул v3-д утсан дээр товч гарахгүй, зөвхөн тэр утсаар уншуулах боломжгүй QR үлддэг байв (#120-ийн «Үлдсэн»).
  • Backend: утсан дээрх эхлэл буцах хаягаа илгээнэ — binding/link callbackUrl (нэвтрэлтийн validEIDCallback дүрмээр: зөвхөн PUBLIC_ORIGIN/auth/eid/callback), консол {"same_device":true,"browser":"retScheme=…|retPkg=…"} → gateway PUBLIC_ORIGIN/auth/eid/callback?…&to=cp угсарна (eid.CallbackURL). Зөвхөн v3-д (EIDService.SameDeviceReturns); legacy-д илгээсэн callback-ийг огт уншихгүй — зан өөрчлөгдөөгүй. Desktop callback-гүй → v3 QR (1 с). flowType = олгосон төрлүүд (eidrp хэвээр). Link/bind/консолын дуусгалт user_challenge_verifier хүлээн авч нэвтрэлттэй ижил шалгана (returnVerifier/returnMatches; консол — gateway ReturnMismatch/ErrInvalidReturn): буруу хэлбэр 400, өөр зөвшөөрлийнх 403, flow зарцуулагдахгүй.
  • Frontend: components/EIDReturn.tsx (callback хуудасны логик, дуусгалт параметртэй); /auth/eid/callback note-ийн kind (signin/link/bind + binding token)-оор дуусгалт сонгоно, to=cp бол CONTROL_PLANE_HOST (server-ээс уншина) дээрх шинэ /cp/eid/callback руу зөвхөн id + verifier-тэй дамжуулна; Console тэр замд sign-in дэлгэц зурахгүй. EIDLogin link/bind/консолд утсан дээр буцах хаяг илгээнэ. Callback хуудас амжилтгүй/403 үед ч note-ийг (binding token-той) устгана. Консолын start бие уншигдахгүй бол desktop гэж үзнэ (400 биш), буруу browser hint-ийг gateway хаяна.
  • Хаана: backend/internal/workspace/identity/{handlers,binding,link_eid}.go, identity/eid/eid.go, internal/operator/operator/eid.go, pkg/host/eid_gateway.go; frontend/components/{EIDReturn,EIDLogin}.tsx, app/auth/eid/callback/{page,EIDCallback}.tsx, app/cp/eid/callback/page.tsx, components/cp/Console.tsx, app/cp/profile/account/page.tsx, lib/{eidSession,cp}.ts, lib/api/client.ts; docs/OPERATIONS.md.
  • Шалгасан: go test -race identity/operator/host/eidrp (шинэ: same_device_test.go, eid_gateway_test.go, консолын 3 тест, binding DB тест — локалд SKIP, CI Postgres), go vet, linux build; vitest 55 файл/349 тест (callback-ийн link/bind/консол дамжуулалт, утсан дээрх link/консол эхлэл), tsc --noEmit, eslint, next build.
  • Үлдсэн: бодит утсан дээр (build 59, v3) гараар шалгах — release үед.
  • Юу/яагаад: QR ба «аппаар нээх» товч хоёулаа статик /dl?sessionId=&vc= холбоос байсан — утас QR уу, Web2App уу ялгаж чадахгүй, холбоос session-ий турш хүчинтэй тул дамжуулсан (relay) холбоосоор өөр хүн нэвтэрч болно. CA (ca-eidmongolia-mn docs/DEVICE_LINK_V3.md) Smart-ID v3 холбоос гаргасан: deviceLinkType + authCode (HMAC-SHA256, sessionSecret), QR-д elapsedSeconds. Хуучин апп (<59) v3 уншихгүй тул EID_DEVICE_LINK_MODE=legacy|v3 (анхдагч legacy) — CA min_version=59 тавихад v3 болгоно.
  • Backend: kernel/eidrp/devicelink.go BuildDeviceLink (CA-ийн golden 15 векторыг байт-ижил давтана, testdata/devicelink_golden.json = CA-ийн файлын хуулбар); start хариуны sessionToken/sessionSecret/ deviceLinkBase + receivedAt-ийг pending бичлэгт (registry.eid_rp_pending) хадгална — secret INTEGRATION_ENCRYPTION_KEY-ээр битүүмжлэгдэнэ (legacy горимд ч). v3: QR = секунд тутмын шинэ холбоос (POST /api/v1/auth/eid/qr, консол POST /api/platform/v1/session/eid/qr; flow token-оор холбогдсон, тусдаа rate budget eid-qr/console-eid-qr 600/мин; sign-in switch-ээс хамаарахгүй — flow token л эрх, зөвхөн HMAC — CA дуудахгүй), callback-тай sign-in-д device_link = Web2App (native app scheme бол App2App); санал болгосон flowType = олгосон төрлүүд. Legacy зан хэвээр.
  • Баталгаажуулалт (CA !191): ACSP_V2 payload-ийн initialCallbackUrl-ийг хариуны signature.initialCallbackUrl-ээс авна — зөвхөн "" эсвэл Nexus-ийн илгээсэн callback-тай байт-ижил бол; Web2App/App2App бол заавал өөрийн callback; талбар байхгүй (хуучин CA) бол илгээсэн callback (өмнөх зан). verify.go signedCallback, тест TestTheSignedCallbackIsTheOneTheCASignedOverWithinLimits. v3 асаах / legacy унтраахаас өмнө шаардлагатай.
  • Frontend: EIDLogin — qr_refresh үед QR-ийг 1 с тутам татна (хуудас нуугдсан / хүлээхгүй үед зогсоно; 403 (flow дууссан)-д зогсоно); утсан дээрх товч device_link (v3-д Web2App).
  • Шалгасан: go test ./internal/kernel/eidrp/ ./internal/workspace/... ./internal/operator/... ./pkg/host/, route golden (/auth/eid/qr), vitest tests/eid-qr-refresh.test.tsx + бүх 55 файл, tsc --noEmit, eslint.
  • Үлдсэн: v3 горимд callback-гүй эхлэлүүд (binding, profile link, консол) утсан дээр товчгүй (CA Web2App-ыг callback-гүй session-д татгалздаг) — утсан дээр QR л харагдана. Прод legacy хэвээр.

Removed — notify-downstream workflow (2026-09-30)

  • Юу: .github/workflows/notify-downstream.yml устгав; docs/DISTRIBUTION.md-ийн sync хэсгийг товчлов.
  • Яагаад: цорын ганц хүлээн авагч eid-mongolia-mn 2026-09-27-нд fork болж core-sync job-оо устгасан — GitLab trigger 2026-09-29-өөс хойш main-ий push бүрд 400 буцааж, workflow улаан байв.
  • Үлдсэн (гараар): EID_MONGOLIA_GITLAB_TRIGGER, CORE_SYNC_TOKEN secret-ийг устгаж, gitlab.gerege.mn дээрх тэр pipeline trigger token-ийг revoke хийх.

Added — AUTH_SIGNIN_LANDING: нэвтрэлт байгууллагаар нээгдэж болно (2026-09-29)

  • Юу: шинэ тохиргоо auth.signin_landing (env AUTH_SIGNIN_LANDING, консолоос ч): home (анхдагч) — өнөөдрийнх шиг сесс бүр хүний өөрийн гэрээр; organisation — хамгийн эртний идэвхтэй гишүүн байгууллагаар, аль ч байгууллагад гишүүн биш бол гэрээр. Гэр аль ч горимд үүснэ — сэлгэгч түүнийг зөвхөн байгаа үед нь жагсаадаг.
  • Яагаад: бүх хэрэглэгч нь нэг байгууллагад ажиллахаар нэвтэрдэг distribution (киоскийн удирдлага) нэвтрэх бүрдээ нэг сэлгэлт төлж байв.
  • Мөн: FirstOrganisationFor идэвхгүй гишүүнчлэлийг алгасна — хасагдсан байгууллага руу нээсэн сесс session.go-ийн гишүүнчлэлийн шалгалтад эхний хүсэлт дээр унах байв.
  • Хаана: backend/internal/kernel/settings/keys.go, backend/internal/workspace/auth/tenants.go, home_db_test.go (TestALandingOfOrganisationOpensTheOrganisation).
  • Шалгасан: go build ./..., go vet ./..., settings/auth unit тестүүд; DB тест CI-д.

eID нэвтрэлтийн smoke шалгалт

  • Юу: deploy/scripts/eid-smoke.sh — POST /api/v1/auth/eid/start-ийг утасны хөтөч шиг дуудаж 200 + device_link (https://…/dl?sessionId=…&vc=…) шаарддаг. Deploy-ийн «Verify public HTTPS» алхамд, мөн шинэ .github/workflows/eid-smoke.yml (30 мин тутам + гараар) ажиллана; docs/RUNBOOKS.md#eid-sign-in-smoke.
  • Яагаад: 09-29-нд rp.eidmongolia.mn /v3 дор 404 болж, EID_BASE_URL-д /v3 үлдсэн тул бүх eID нэвтрэлт 502 байсан ч /health ногоон, alert-ийн receiver тохируулаагүй — хэн ч мэдээгүй.
  • Шалгалт: прод руу скрипт OK; буруу origin (example.com) → exit 1. CI shellcheck.
  • Үлдсэн: CA callback-ийг хүлээн авсан эсэхийг (App2App) харахгүй; ажил бүр CA дээр зөвшөөрөгдөөгүй нэг session үүсгэнэ (өдөрт 48).

Fixed — eID нэг төхөөрөмж дээр (Web2App): CA-ийн https холбоос, буцалтын verifier (2026-09-29)

Утсан дээрх хөтөч eID аппыг eidmongolia://approve?sessionId=…-аар шууд нээдэг байв: апп суугаагүй бол чимээгүй унаж, iOS дээр хоёр дахь async алхмын дараа хэрэглэгчийн товшилт алдагддаг, vc дамждаггүй, холбох/binding/консолд (callback-гүй) ч автоматаар шилждэг байв. eID-ийн гэрээ (ca-eidmongolia-mn, eid-mongolia-mobile, 2026-09-29) нэг холбоосыг заадаг: {deviceLinkBase}?sessionId=<id>&vc=<код>.

  • Backend холбоос: eidrp.QRInitiate CA-ийн deviceLinkBase-ийг уншиж eidrp.SameDeviceLink (https биш бол https://ca.eidmongolia.mn/dl) үүсгэнэ. Start-ын хариунд шинэ device_link талбар (/auth/eid/start, холбох, binding, консолын /session/eid/start); QR-ийн device_link_url ч мөн энэ https холбоос болов (камер нээнэ, eID аппын сканнер 2.2.1 sessionId=/&vc=-г хайдаг тул уншина). QRPayload хасагдсан. Push-д холбоос байхгүй хэвээр.
  • Вэб (components/EIDLogin.tsx): утсан дээр нэвтрэхэд start-ын ганц await-ын дараа device_link руу шилжинэ; холбох/binding/консолд автоматаар шилжихгүй, «eID аппаар нээх» товч (<a href=device_link>) зурна; нэвтрэлтэд ч товч нөөц болж үлдэж, утсан дээр QR-ийн оронд харагдана. iPadOS desktop UA (Macintosh + maxTouchPoints > 1) утас гэж тооцогдоно. retScheme/retPkg hint-ийг eID аппын allowlist-тэй тулгасан (firefox-focus, yandexbrowser-open-url, org.mozilla.focus хасагдсан; demo-ийн sameDeviceReturn.ts-тэй ижил). lib/eidProvider.ts-ийн appScheme ашиглагдахгүй болж хасагдсан.
  • Буцалт (app/auth/eid/callback): userChallengeVerifier-ийг уншиж poll-той (user_challenge_verifier) илгээнэ. /auth/eid/poll BASE64URL(SHA-256(verifier)) == зөвшөөрсөн хариуны signature.userChallenge (constant-time) шаардаж, таарахгүй бол flow-г зарцуулалгүй 403 + лог; буруу хэлбэр 400; verifier-гүй poll өмнөх шигээ. Эхлүүлээгүй хөтөч/таб эсвэл өөр утсанд буусан буцалт (нэвтрээгүй) алдааны оронд «eID апп дээр баталгаажлаа. Нэвтрэлтийг эхлүүлсэн цонх руугаа буцна уу.» гэж хэлнэ. Шинэ мөрүүд lib/i18n/web.ts-д (web.eid.*): addons/auth.ts native багц руу бүтнээрээ экспортлогддог.
  • Консол: /session/eid, /me/eid flow токеныг CA-аас асуухаас өмнө шалгана (eidflow.Checker) — ажлын талын holdsEIDFlow-тэй ижил; токенгүй бол 403, CA-д хүрэхгүй.
  • Баримт: docs/IDENTITY.md (нэг төхөөрөмжийн хэсэг, QR мөр, «Gerege ID» → eID Mongolia), docs/OPERATIONS.md (flow токены урьдчилсан шалгалт).

Шалгасан: go test ./... (69 багц ok; DB шаардсан 404 тест DB-гүй орчинд SKIP), go vet, gofmt, GOOS=linux GOARCH=amd64 go build; шинэ тестүүд — SameDeviceLink (base-ийн шалгалт, дараалал), App2App flowType callback-тай session дээр баталгаажна / callback-гүйд татгалзана, verifier таарах/таарахгүй/байхгүй, консолын poll токенгүй бол CA-д хүрэхгүй. Frontend: tsc --noEmit, vitest run (54 файл, 340 тест), eslint (алдаа 0).

Үлдсэн: бодит утсан дээр (iOS Safari/Chrome, Android Chrome, iPad) шалгаагүй. Native клиент (native-apps/) энэ PR-д хөндөгдөөгүй — тусдаа шугамаар. auth.callback.foreign мөр вэбт ашиглагдахгүй болсон ч native экспортыг хөндөхгүйн тулд addons/auth.ts-д үлдээв.

Changed — eID картын таб ба илгээх товч header-тэй нэг хэлбэртэй (2026-09-29)

Өндөр нь аль хэдийн 44px байсан ч нүдэнд ялгаатай харагдаж байв: таб дотор pill 36px (header-ийн MN/EN 38px), радиус 8px (header 6px), илгээх товчны фонт 14px («Нэвтрэх» 15px). frontend/app/globals.css — нүүр (.gp-landing) ба /login (.signin-card) дотор табын замд 1px хүрээ + 2px зай, --gerege-radius-control радиус; илгээх товчийг 15px. Шалгасан: прод дээр computed style-аар pill 38px, радиус 6px, фонт 15px.

eID: EID_BASE_URL-аас /v3 хасав (прод ops + баримт)

  • Юу: прод PRODUCTION_ENV-ийн EID_BASE_URL=https://rp.eidmongolia.mn/v3 → https://rp.eidmongolia.mn; config/eid.go, .env.example, deploy/.env.prod.example, docs/OPERATIONS.md-ийн «rp. /v3 дор хариулдаг хэвээр» гэсэн хуучирсан заавар засагдав.
  • Яагаад: rp.eidmongolia.mn /v3/...-д 404 буцаадаг болсон (09-29 шалгасан: nexus-ийн POST /v3/authentication/device-link/anonymous → 404, root зам → app хариу) тул nexus.gerege.mn-ийн eID нэвтрэлт бүхэлдээ 502 байв.
  • Хаана: GitHub production environment secret PRODUCTION_ENV (хостын өмнөх .env нөөц /root/prod-env-before-eid-base-*.env), workflow_dispatch deploy.
  • Шалгалт: /api/v1/auth/eid/start 200, CA session device_link_type=App2App (callback-тай), poll cookie-гүй 403, консолын poll flow-гүй 403.

eID Mongolia-гийн RP хаяг rp.eidmongolia.mn

  • Анхдагч EID_BASE_URL (config/eid.go, eidrp.defaultBase) https://ca.eidmongolia.mn/v3-ээс https://rp.eidmongolia.mn/v3 болов: relying-party API нь rp. дээр, ca. нь CA (гэрчилгээ, PKI) — RP дуудлагыг хүлээж авдаггүй. EID_BASE_URL тохируулсан суулгац өмнөх шигээ өөрийнхөө утгаар явна.
  • Frontend-ийн eidProvider.host мөн rp.eidmongolia.mn — нэвтрэх, хөгжүүлэгчийн хуудас, гарын үсгийн дэлгэц хаягийг нэрлэхдээ RP-г нэрлэнэ.
  • Runbook-ийн хүрэх шалгалт, .env жишээнүүд дагаж өөрчлөгдөв.

eID-ийн замаас /v3 арилав

  • Эстонийн Smart-ID-гээс дуурайсан /v3 сегментийг кодоос хасав: анхдагч EID_BASE_URL https://rp.eidmongolia.mn (config/eid.go, eidrp.defaultBase), eidsign-ийн /v3/signature/…, /v3/session/… замууд /signature/…, /session/… (Config.V3BaseURL → BaseURL), eidmongolia.go-ийн TrimSuffix("/v3") алга.
  • Хувилбарын сегмент одоо зөвхөн тохируулсан EID_BASE_URL-д: сервер /v3 дор байгаа цагт (амьд rp.eidmongolia.mn 2026-09-28-нд тийм) EID_BASE_URL=…/v3 гэж заана; серверээс хасагдмагц .env-ээс л хасна.

Security — eidmongolia.mn порталын дахин аудитын цөмийн засварууд (2026-09-27)

  • Юу:
  • Профайлын «eID холбох» дансыг өөр иргэн рүү дахин холбодог байсныг хаав. writeEIDLink-ийн upsert одоо зөвхөн ижил иргэн (person_etsi / civil_id / reg_number таарах, civil_id зөрөхгүй) үед л мөрийг шинэчилнэ; өөр иргэн → ErrEIDAlreadyLinkedToAnotherCitizen → 409, users.ge_id дарагдахгүй. Холбох урсгал өөрийн эхлэлтэй боллоо: POST /profile/identities/eid/start (нэвтэрсэн, impersonation-гүй) QR/push-ийг «eID холбох: <данс>» бичвэртэй эхлүүлж, eid_flows-д link:<session> түлхүүрээр бүртгэнэ — нийтийн /auth/eid/start-аар эхэлсэн session-ийг HandleLinkEID 403-оор татгалзана (link_eid.go, eid_flow.go, eid.go: StartDeviceLinkNamed, frontend EIDLogin.tsx/client.ts: startLinkEID).
  • Google→eID холболтын токен хөтөчид бэхлэгдэв. /login/bind?b= redirect-д gerege_bind cookie (HttpOnly, Lax, Secure prod, path /api/v1/auth/bind, 20 мин) — токены SHA-256; session/consent/start/poll бүр cookie ба b-г constant-time тулгаж, зөрвөл 404 (мэдэгдэхгүй токентой ижил). QR салбар ч bindingDisplayText-тэй.
  • Тенант role хамт ажиллагчийн нууцыг уншихгүй (migration 00134): registry.users-ийн password_hash, user_eid_identities-ийн civil_id, reg_number, document_number, claims баганын SELECT-ийг gerege_nexus_tenant-аас хураав; өөрийн мөрийн уншилт registry.own_password, own_eid_identity, link_eid_identity SECURITY DEFINER функцээр (app.current_user шалгана). Go: profile/password.go, identity/linked.go, signing/store.go, auth/handlers.go. Migration ба binary хамт гарна — хуучин binary 00134 дээр 42501 авна.
  • ge_id-ээр олдсон данс civil_id NULL хуучин мөртэй бол reg_number-ээр ч ижил иргэн гэж танина (түгжрэлгүй).
  • Консолын directory: регистрийн дугаар маскалж, хайлт яг таарахаар (operator/tenants/directory.go, people.MaskNationalID).
  • OIDC: /oauth2/logout зөвхөн баталгаажсан id_token_hint эсвэл бүртгэлтэй post_logout_redirect_uri-тай client үед л session-ийг хаана (бусад тохиолдолд /login?logout=confirm руу, revoke үгүй); refresh grant client-ийн grant_types-д байх ёстой (unauthorized_client), offline_access ч түүнгүйгээр refresh token өгөхгүй; /oauth2/userinfo (401) ба /oauth2/introspect (active:false) идэвхгүй хэрэглэгчийг шалгана.
  • eID push: decoy зөвхөн CA-гийн ErrInitiateRejected-д (богино регистр ч энд орно); бусад алдаа 502 — CA унасан үед иргэн мөнхийн spinner харахгүй. TenantsForUser идэвхгүй membership-ийг жагсаахгүй.
  • Яагаад: портал (distribution)-ын 2026-09-27-ны дахин аудит — High: холбох замаар байнгын identity spoofing; Medium: bind токен bearer, RLS мөрийн бүхэл уншилт, РД ил.
  • Хаана: backend/db/migrations/00134_*.sql (+ person_secrets_test.go), backend/internal/workspace/{auth,identity, profile,signing,ssoprovider}, backend/internal/operator/{people,tenants}, frontend/components/EIDLogin.tsx, frontend/lib/api/client.ts, docs/OPERATIONS.md (link start + bind cookie).
  • Шалгасан: gofmt, go vet, go build (darwin + linux/amd64), unit тестүүд локалд; DB тестүүд (00134 хамгаалалт, дахин холболт 409, ge_id reg_number, directory маск, refresh grant, active, logout) CI дээр.
  • Үлдсэн: ?logout=confirm-ыг нэвтрэх хуудас хараахан харуулдаггүй (зөвхөн hint); distribution-ууд (eidmongolia.mn) core-sync-ээр авна.

Нэвтрэх хуудасны брэнд тэмдэг app shell-тэй ижил хэмжээтэй

  • Юу: frontend/components/SignInFrame.tsx — лого 24px → 32px (size-8, width/height атрибуттай), нэр text-sm → text-base, зай 8 → 10px.
  • Яагаад: Layout (app shell) ба Console-д лого 32px байхад зөвхөн нэвтрэх хуудас 24px байсан; eidmongolia.mn-ийн нүүр хуудас (32px / 16px 600) ба /login хоёр өөр харагдаж байв.
  • Шалгасан: tsc --noEmit, lint.

Added — Distribution-ий модулийн migration-ыг migrate алхам хэрэгжүүлдэг болсон (аудит №03) (2026-09-26)

  • Юу: cmd/migrate-ийн логик backend/pkg/migrate (нийтийн багц) руу шилжсэн: migrate.Main(Options) / migrate.Run(ctx, dbURL, Options, command, args...). Options.Modules (map[app ID]fs.FS) — distribution-ий өөрийн модулийн schema-г цөмийн модулийн дараа, эзэмшигч role-оор, API-ийн уншдагтай ижил public.goose_db_version_<ID-ийн сүүлийн хэсэг> түүхэнд хэрэгжүүлнэ. Цөмд байгаа ID-г давхар өгвөл татгалзана. cmd/migrate = migrate.Main(migrate.Options{}) — цөм болон одоогийн distribution-уудад зан төлөв өөрчлөгдөөгүй.
  • Яагаад: distribution-ий модулийн migration (жишээ нь eid-mongolia-mn modules/developer) зөвхөн API-ийн catalogue sweep-ээр gerege_nexus_api-аар ажиллаж «permission denied» гэж логт үлддэг байв; ганц арга нь backend-ийг superuser-ээр асаах байсан.
  • Хаана: backend/pkg/migrate/, backend/cmd/migrate/main.go, CI-ийн skip guard, OPERATIONS, MODULES.
  • Шалгасан: TestADistributionModuleIsAppliedByTheMigrateStep (CI Postgres: хүснэгт + түүх үүснэ, gerege_nexus_api бичих эрхгүй, хоёр дахь ажиллагаа no-op), TestAModuleTheCoreCarriesIsRefused, TestValidateArgs; go vet, go test, linux/amd64 build.
  • Үлдсэн: eid-mongolia-mn core-sync-ийн дараа өөрийн cmd/migrate + Dockerfile + CLAUDE.md runbook-ийг хасах.

Security — eID: зөвхөн иргэний issuing CA-ийн гэрчилгээ нэвтэрнэ, хуучин anchor-ууд хасагдсан (аудит №01) (2026-09-26)

  • Юу/яагаад: kernel/eidrp/verify.go нь үндэсний root (болон хуучин self-signed issuing CA-ууд) руу гинжлэгдсэн дурын clientAuth гэрчилгээг хүлээн авдаг байв. eID Mongolia Organization Issuing CA (05) ижил root дор clientAuth гэрчилгээ (RP mTLS O-ORG-AUTH, P-ORG-REP) олгодог тул subject-д PNOMN serial-тай тийм гэрчилгээ иргэн болж нэвтэрч болох байв. Одоо гинжийн шалгалтын дараа leaf-ийг шууд олгосон CA-ийн SKI (байхгүй бол leaf-ийн AKI) allowlist-д байх ёстой; анхдагч — eID Mongolia Issuing CA (04, b8a37f2c64ad677912db7d9e34f246358a2fb909). Иргэний 77 амьд гэрчилгээ бүгд 04-өөр олгогдсон (CA DB, 2026-09-26).
  • Trust store: kernel/eidrp/trust/ = CA репогийн pki/chain/ 01–05 (04/05 нь өнөөдөр дахин олгогдсон, ижил түлхүүр, засварласан AIA/CDP). legacy-selfsigned-* ба legacy-dpod-2026-07-28-* (6 файл) хасагдсан — тэдгээрээр олгосон амьд гэрчилгээ 0. Эхлэх лог: anchors=1 intermediates=4 citizen_issuers=1.
  • Override: EID_CITIZEN_ISSUER_SKIS (таслалаар, hex, : зөвшөөрнө; анхдагчийг солино) — EID_ROOT_CA_PEM/_FILE-тэй staging CA-д. identity/eid/eid.go, .env.example, deploy/.env.prod.example, docker-compose.prod.yml, OPERATIONS.
  • Шалгасан: go test ./internal/kernel/eidrp/ — 04-өөс admit, 05 (PNOMN-тай) татгалзана, иргэн олгодоггүй anchor-оор татгалзана, хоосон allowlist хэнийг ч оруулахгүй, override staging issuer-ийг admit хийнэ; embedded 04-ийн SKI = анхдагч. Шалгалтгүйгээр тест унадаг нь батлагдсан. go vet, linux build.
  • Үлдсэн: eID Mongolia-аас өөр CA руу заасан distribution EID_CITIZEN_ISSUER_SKIS тавих ёстой.

Security — eID-ийн нээлттэй зүйлс: push хаалт, дансны түлхүүр, жагсаалтын хязгаар (аудит №28, №30, №32, №33, №34) (2026-09-26)

  • №28 push хаалт: регистрээр push (/auth/eid/start-id, /auth/bind/eid/start) нь регистр тутамд 10 минутад 3 байсан тул регистр мэдэх хэн ч иргэний push нэвтрэлтийг дахин дахин хааж чаддаг байв. Одоо (регистр, IP) тутамд 3, регистрт бүх IP-ээс нийт 10 (identity/push_limit.go; Redis-тэй бол replica-д нийт, eid-push + eid-push-id). IP-ийн төсөв эхэлж асуугддаг тул татгалзсан оролдлого регистрийн таазыг идэхгүй. Хаахад одоо ≥4 хаяг хэрэгтэй; spam 10-аар хязгаарлагдана.
  • №30 RP secret солих: синтетик хаягийн digest (eid+<digest>@identity.invalid) ба хуучин нууц үгийн шалгалт EID_RP_SECRET-ээр түлхүүрлэгддэг байсан тул secret солиход eID холбоос мөргүй данс олдохгүй болж хоёр дахь данс үүсэх байв. Шинэ EID_ACCOUNT_KEY (хоосон бол EID_RP_SECRET — өнөөдрийн зан төлөв өөрчлөгдөхгүй). docker-compose.prod.yml, .env.example, OPERATIONS-д: солихын өмнө EID_ACCOUNT_KEY-г одоогийн secret-ээр тавь.
  • №32 хязгааргүй жагсаалт: directory.People → LIMIT nexus.DirectoryPeopleLimit (2000; pkg/nexus-д нэмэлт тогтмол, гарын үсэг өөрчлөгдөөгүй, api_changes.txt); /person/apps/{slug}/history → байгууллагын timeline-ийн адил 500 + truncated.
  • №33: frontend/tests/e2e/citizen-eid.spec.ts — иргэн QR-аар eID нэвтрэлт (start → RUNNING×2 → COMPLETE → next руу), API page дотор stub.
  • №34: ESIGN_MOCK_MODE production-д асахгүй (config/production.go); AI copilot түүх сүүлийн 20 бичлэг, бичлэг бүр 4000 тэмдэгт; мэдлэгийн хайлтын ILIKE-д %/_/\ escape; frontend @types/node → ^22 (Dockerfile node:22).
  • Шалгасан: go vet ./..., go test ./..., GOOS=linux go build ./...; vitest 315/315, tsc, eslint, Playwright 11/11. DB тестүүд (TestThePeopleListIsCapped, TestPersonAppHistoryIsCapped, TestAnInventedAddressSurvivesAnRPSecretRotation) зөвхөн CI-д.
  • Үлдсэн: advisory-lock ачааллын тест хийгээгүй; devices/kiosk route-ыг тохиргоогүй үед холбохгүй унтраалга кодод байхгүй тул хөндөөгүй.

Security — Төхөөрөмжийн шугамыг унтраах DEVICES_ENABLED (аудит №05) (2026-09-26)

  • Юу: DEVICES_ENABLED=false үед /api/v1/devices/* (enroll, me, telemetry, token/rotate, staff/pin) ба /api/v1/admin/devices/* mount хийгдэхгүй — үл мэдэгдэх зам шиг 404. nexus.devices core апп App Store-д санал болгогдохгүй, анхдагч суулгалтад орохгүй, бүртгэгдэхгүй (цэсэнд гарахгүй). /api/v1/push-tokens (хүний push токен) хэвээр.
  • Яагаад: eidmongolia.mn-д киоск/терминал огт байхгүй (DEVICE_LINE_ORIGINS хоосон) ч бүртгэлийн endpoint бүх суулгацад нээлттэй байсан — ашиглалтгүй халдлагын гадаргуу.
  • Хаана: backend/internal/kernel/config/devices.go, workspace/service.go, workspace/devices/routes.go (PushRoutes салгасан), workspace/coreapps/coreapps.go; docker-compose.prod.yml, .env.example, deploy/.env.prod.example, OPERATIONS.
  • Анхдагч: тавиагүй (эсвэл false/0/no/off-оос өөр) бол асаалттай — өнөөдрийн зан төлөв, route golden-ууд өөрчлөгдөөгүй.
  • Шалгасан: TestDevicesSwitchedOffAreNotMounted (унтраахад /devices зам 0, enroll/telemetry 404, push-tokens үлдсэн; gate-гүйгээр 9 зам илэрч унана), TestTheDeviceFleetGoesWithTheDeviceLine, TestDevicesAreOnUnlessSwitchedOff; go vet, go test ./..., linux/amd64 build.
  • Үлдсэн: eidmongolia.mn-ийн distribution env-д DEVICES_ENABLED=false тавих. Frontend-ийн /settings/devices хуудас өөрөө үлдсэн (цэсгүй; шууд орвол API 404-ийн алдааны төлөв).

Added — «Шинэчлэл» дэлгэц GitLab дээрх дистрибуцыг уншдаг болов (2026-09-25)

  • /cp/ops/updates нь зөвхөн GitHub-ийн core-sync PR уншдаг байв. eidmongolia.mn дистрибуц өөрийн GitLab руу шилжиж байгаа тул CORE_SYNC_PROVIDER=gitlab нэмэв: хүлээгдэж буй шинэчлэл нь core-sync/ салбараас default branch руу нээгдсэн (fork биш) хамгийн шинэ MR, CI нь MR-ийн head pipeline (success / failed|canceled / бусад нь pending), «Шинэчлэх» нь аудит core.sync.apply-ийн дараа trigger/pipeline-ийг variables[MERGE_MR]=<iid>-тэй дуудаж pipeline-ийн хуудсыг буцаана. GitHub-ийн зам өөрчлөгдөөгүй (анхдагч github).
  • Хаана: backend/internal/operator/backup/updates_gitlab.go (шинэ), updates.go (provider-ийн салаа, JSON-д provider), fail.go (ErrGitLabUnavailable → 502, ErrPipelineRefused → 400); frontend app/cp/ops/updates/page.tsx — «MR !9», «GitLab pipeline» гэсэн үг, cp.updates.gitlab.* алдааны түлхүүр (mn+en); docker-compose.prod.yml, .env.example — CORE_SYNC_PROVIDER, CORE_SYNC_GITLAB_URL, CORE_SYNC_GITLAB_TOKEN, CORE_SYNC_TRIGGER_TOKEN; OPERATIONS «Цөмийн шинэчлэл» — токены эрх, trigger дуудлага.
  • github_error талбарын нэр хуучин клиентийн төлөө хэвээр — GitLab-ийн алдааг ч тэнд бичнэ.
  • Шалгасан: go test ./internal/operator/backup/ — httptest хуурамч GitLab: тохиргоогүй (дутуу нэрс), MR сонголт (хуучин, fork, өөр салбар, өөр target үл тоомсорлогдоно), pipeline → CI, буруу MR / ногоон биш / pipeline-гүй үед trigger хийгдэхгүй, trigger-ийн form (token, ref, MERGE_MR); go vet ./..., GOOS=linux go build ./..., go test ./...; vitest tests/cp (GitLab-ийн үг, холбоос), tsc, eslint.
  • Үлдсэн: eidmongolia.mn-ийн env-д дөрвөн хувьсагч тавих, .gitlab-ci.yml-ийн core-merge job.

Security — Товлосон тайлан: байгууллага scheduler-ийн бүртгэлийг бичихгүй (2026-09-25)

  • Юу: workspace.report_schedules-ийн RLS-ийг хянаж (policy_shape_test.go-д «unreviewed» байсан) reviewed болгов. Тенантын роль одоо зөвхөн records_adapter.go-ийн бичдэг баганыг бичнэ: INSERT (tenant_id, report_key, name, params, cron, format, recipients, active, created_by), UPDATE (report_key, name, params, cron, format, recipients, active, updated_at). last_run_at, last_status, last_error болон дараа нь created_by, tenant_id-г солих нь permission denied.
  • Яагаад: tenant_isolation (00045) мөрийг зөв хаадаг — өөр байгууллагын хуваарийг уншиж, засаж, өөр байгууллагын нэрээр оруулж чадахгүй — гэхдээ өөрийн мөрийн аль ч баганыг бичих боломжтой байв: ирээдүйн last_run_at хуваарийг чимээгүй зогсооно, last_status = 'SENT' консолын «унасан хуваарь» самбараас алдааг нууна, created_by солих нь товлосон гүйлтийн аудитын actor-ыг хуурамчаар бичүүлнэ. Go эдгээрийг тенантын замаар хэзээ ч бичдэггүй; RLS хуучин/шинэ утгыг харьцуулж чаддаггүй тул баганын GRANT. Нарийн хэлбэр зориудаар: хуваарь бол нэг байгууллагын өгөгдлийг гадагш илгээх заавар, код бүр tenant_id = $1-ээр нэрлэдэг тул app.allowed_tenants юу ч нэмэхгүй.
  • Хаана: backend/db/migrations/00133_the_scheduler_keeps_its_own_record.sql, report_schedules_policy_test.go, policy_shape_test.go.
  • Шалгасан: scheduler нь session-гүй (nexus.WithoutWorkspace → gerege_nexus_api → 00115-ийн platform_path) — бодлоготой, тестэд claim ба үр дүн бичих хоёулаа 1 мөр; консол 00096-ийн operator_read-ээр зөвхөн уншина. Тест SET LOCAL ROLE gerege_nexus_tenant-ээр: өөрийн хуваарь үүсгэх/засах/устгах ажиллана, өөр байгууллагын нэрээр INSERT RLS-ээр, scheduler-ийн баганууд эрхээр татгалзана, өөр байгууллага 0 мөр харна/засна/устгана. gofmt, go vet, go build (linux/amd64 ч), go test ./... локалд; DB тест CI-д.
  • Үлдсэн: хүлээн авагчийн хаяг ба тайлангийн эрхийг DB илэрхийлж чадахгүй (ValidateSchedule + апп-ын эрх). INSERT дээрх created_by-г app.current_user-тай тэнцүү гэж шаардаагүй — Create-ийг дууддаг reports апп CreatedBy-г юунаас авдгийг энэ репоос шалгах боломжгүй. Scheduler гүйлт бүрд үүсгэгчийн одоогийн эрхийг дахин шалгадаггүй (Go-ийн асуудал, RLS биш).

Changed — notify-downstream: eid-mongolia-mn-ийг GitLab trigger-ээр мэдэгдэнэ (2026-09-25)

  • Юу: .github/workflows/notify-downstream.yml — gerege-systems/eid-mongolia-mn руу repository_dispatch-ийн оронд gitlab.gerege.mn-ийн products/gerege-line/eid/eid-mongolia-mn төслийн pipeline trigger (CORE_SYNC=1). Secret EID_MONGOLIA_GITLAB_TRIGGER (тусдаа trigger token); байхгүй бол алгасна.
  • Яагаад: distribution 2026-09-25-нд GitLab руу нүүсэн; GitHub дээрх нь зөвхөн толь, Actions унтраалттай.
  • Шалгасан: YAML parse; trigger-ийн зам/талбарууд нь тэр төслийн .gitlab-ci.yml-ийн core-sync дүрэмтэй тулгасан.
  • Үлдсэн: хуучин CORE_SYNC_TOKEN secret өөр хэрэглэгчгүй болсон бол устгаж болно.

Changed — нэвтрэлт үргэлж «Миний гэр»-ээр нээгдэнэ; дансны цэснээс tenant сэлгэгч хасагдсан (2026-09-25)

  • Нэвтрэлт: хэдэн байгууллагад гишүүн байхаас үл хамааран сесс бүр хүний өөрийн гэр (kind='personal')-ээр нээгдэнэ (auth.FirstTenantFor). Өмнө нь байгууллагатай хүн хамгийн хуучин байгууллага руугаа ордог байв. Гэрийг үүсгэж чадахгүй бол (лог бичээд) анхны байгууллага руу унана. Сэлгэгчийн жагсаалтад гэр одоо хамгийн дээр.
  • Дансны цэс (UserMenu): «Байгууллагууд» хэсэг ба showTenants prop хасагдсан — зүүн талын сэлгэгч (rail mark, sidebar толгой, lg-ээс доош top bar) аль хэдийн байгаа. Native ribbon мөрөнд зөвхөн нэр харуулдаг байсныг сэлгэгч болгосон, эс бөгөөс desktop цонхноос байгууллага солих газаргүй болох байв.
  • Яагаад: бүтээгдэхүүний шийдвэр — нэг тогтмол эхлэх цэг; сэлгэгч нэг л газар.
  • Хаана: backend/internal/workspace/auth/{tenants,session}.go, home_db_test.go; frontend/components/{UserMenu,Layout,TenantChoices}.tsx, tests/{cp/ui,user-menu-height}.test.tsx.
  • Шалгасан: go vet/build (linux/amd64)/test, tsc, vitest 315/315, lint 0 алдаа; DB тест CI-д.
  • Үлдсэн: байгууллагад ажилладаг хүн нэвтрэх бүрд нэг удаа сэлгэх шаардлагатай болсон (зориуд).

Security — eID-ийн үлдэгдэл: QR төсөв, /profile/eid, ДАН (аудит №07, №23, №29, №34) (2026-09-25)

  • №07 QR төсөв: /auth/eid/start (QR/App2App, push илгээдэггүй) нууц үгтэй нэг login төсөвт (IP тутамд минутад 5) байсан тул carrier NAT-ын ард олон иргэн нэг хаягаас QR дээрээ 429 авах байв. Одоо өөрийн eid-start төсөв: минутад 30, burst 20 (auth.EIDStartRatePerMinute, Redis-тэй бол replica-д нийт). /auth/eid/start-id (регистрээр push) ба нууц үг login дээрээ үлдэв.
  • №23: /profile/eid/* (платформын RP credential-аар CA руу) төсөвгүй байв — eid-record IP тутамд минутад 60, burst 20.
  • №29: profile/eidpki.go CA-ийн статус, биеийг шууд дамжуулдаг байсан: CA 401 нь аппад «session дууссан» гэж уншигдаж иргэнийг гаргадаг. Одоо зөвхөн 2xx бичлэг дамжина; 404 → eid_not_found; 403 → 502 eid_forbidden (дэлгэц «PKI_READ олгоогүй» гэж хэлсээр); бусад (401, 400, 5xx) → 502 eid_unreachable. CA-ийн бие хэзээ ч гарахгүй, статус нь WARN логт. Frontend errorKey eid_forbidden-ийг таньдаг болов.
  • №34 ДАН гарын үсэг: documents/shared.tsx — ДАН-аар гарын үсэг зурсны дараа onDone унавал «гарын үсэг амжилтгүй» гэж харуулдаг байв (#83 eID-д зассантай ижил). Одоо зурсан баримт амжилттай хэвээр.
  • ДАН аудит: auth.dan_gerege_login_success нь dan_session ID-г (mock-д регистр агуулдаг) бичиж, audit.Record түүнийг stdout руу гаргадаг байв → dan_session_ref (SHA-256-ийн 16 hex).
  • Шалгасан: go vet, go test ./..., GOOS=linux go build, route_chains.txt шинэчлэгдсэн; vitest (documents-sign-action, eid-person-record), tsc, eslint. Тест бүр засваргүйгээр унана.
  • Үлдсэн: /profile/eid/* төсөв IP-ээр (хэрэглэгчээр биш — платформд ийм л limiter бий).

Removed — gerege eID provider (Gerege ID, ca.gerege.mn); анхдагч нь eID Mongolia (2026-09-25)

  • Юу: EID_PROVIDER-ийн gerege утга (Gerege ID, https://ca.gerege.mn/v3, geregesmartid://) болон geregeid хочийг цөмөөс хасав. Ганц provider нь eidmongolia (eID Mongolia, https://ca.eidmongolia.mn/v3) бөгөөд анхдагч болов. EID_PROVIDER=gerege тохируулсан хэвээр байрлуулалт үл мэдэгдэх утга гэж тооцогдоод нэг удаагийн WARN логтойгоор eidmongolia руу унана — ca.gerege.mn руу хэзээ ч заахгүй. eidrp-ийн defaultBase ч мөн ca.eidmongolia.mn болов.
  • Яагаад: ca.gerege.mn (VM gerege-ca-1) нь туршилтын CA байсан бөгөөд 2026-09-25-нд устгагдаж байна.
  • Нөлөө: EID_PROVIDER хоосон байрлуулалт одоо нэвтрэх дэлгэц, мессежид «eID Mongolia» гэж харуулж, ca.eidmongolia.mn руу хандана (өмнө нь «Gerege ID» / ca.gerege.mn). EID_BASE_URL тохируулсан бол хаягийн хувьд түүнийх хэвээр ялна. EID_RP_UUID/EID_RP_SECRET нь ca.eidmongolia.mn-ийн олгосон байх ёстой.
  • Хаана: backend/internal/kernel/config/eid.go (+ eid_test.go), backend/internal/kernel/eidrp/eidrp.go, eidsign/eidsign.go, workspace/identity/eid/eid.go (тайлбар); frontend/lib/eidProvider.ts (+ tests/eid-provider.test.ts), scripts/export-native-auth-i18n.mjs, components/EIDLogin.tsx, components/eid/PersonRecord.tsx; native-apps/generated-i18n/*.json (sync-i18n.sh-ээр дахин экспортолсон); .env.example, deploy/.env.prod.example, docker-compose.prod.yml, docs/IDENTITY.md, docs/RUNBOOKS.md. pkg/nexus-ийн экспортолсон API өөрчлөгдөөгүй.
  • Шалгасан: gofmt, go vet ./..., go build ./..., GOOS=linux GOARCH=amd64 go build ./..., go test ./...; frontend tsc --noEmit, vitest (53 файл / 315 тест), eslint (0 алдаа).
  • Үлдсэн: native iOS/Android нь App2App-д geregesmartid схемийг eidmongolia-гийн дараах нөөц болгон (MobileLoginView.swift, LoginScreen.kt, Info.plist, AndroidManifest) хадгалсаар — native build/тестгүйгээр хөндсөнгүй. Native shell-ийн тестүүд ca.gerege.mn-ийг зүгээр гадаад origin жишээ болгон ашигладаг (pin биш). app/dev/docs дахь ca-gerege-mn репогийн SDK холбоосыг хөндсөнгүй.

Changed — CHANGELOG-ийн бичлэг changelog.d/ фрагментаар (2026-09-25)

  • Гаргаагүй өөрчлөлтийн бичлэг CHANGELOG.md-д шууд биш, changelog.d/<YYYY-MM-DD>-<сэдэв>.md тусдаа файлаар бичигдэнэ; release-ийн өмнө node scripts/changelog.mjs collect нэгтгэнэ. Журам changelog.d/README.md.
  • Яагаад: зэрэг нээлттэй PR бүр ## [Unreleased]-ийн дээд мөрийг засдаг тул дараагийнх нь merge болоход conflict гарч CI дахин ажилладаг байв (2026-09-25-нд #80 гурван удаа).
  • Хаана: scripts/changelog.mjs (collect/guard), CI Lint job-д guard алхам, .github/pull_request_template.md, docs/DISTRIBUTION.md (tag-ийн өмнө collect).
  • Шалгасан: collect-ийг хуулбар дээр ажиллуулж дараалал, ../ холбоос хөрвүүлэлтийг; guard-ийг таван тохиолдлоор: энэ PR (давна), шууд засвар (унана), collect-ийн дараах шууд засвар (унана), collect (давна), зөвхөн фрагмент (давна).

Security — eID нэвтрэлтийн бэхжүүлэлт (аудит №03, №09, №13, №17, №28) (2026-09-25)

  • №03 impersonation: auth.RefuseImpersonated нь GET /profile/eid/{resource} (иргэний CA дээрх бичлэг — хүүхэд, төхөөрөмж, гэрчилгээ) ба /api/v1/esign/* бүлэгт (signing/esign.go Mount) нэмэгдэв. /oauth2/auth нь өмнө нь зөвшөөрөл байвал consent дэлгэцгүйгээр код олгодог байсан — impersonated session-д одоо access_denied.
  • №09 login CSRF: нэвтрээгүй хүнд session cookie байхгүй тул CSRFMiddleware алгасдаг; гадны сайтын enctype=text/plain форм {"session_id","flow_token"}-ийг poll руу илгээж хохирогчийг халдагчийн бүртгэлээр нэвтрүүлж чадах байв. security.RequireJSONBody (identity.SignInRoutes бүхэлд): хоосон биш бие заавал application/json (эс бөгөөс 415), Sec-Fetch-Site: cross-site нь ALLOWED_ORIGINS-д байхгүй бол 403. Хоосон биетэй QR start, native клиентүүд (JSON илгээдэг, Sec-Fetch-Site-гүй) хөндөгдөхгүй.
  • №13 давхар бүртгэл: ResolveOrProvisionEIDUser анхны eID нэвтрэлтэд user_eid_identities холбоосыг users INSERT-тэй НЭГ транзакцад бичнэ (writeEIDLink). Өмнө нь commit-ийн дараа best-effort бичигдэж, унавал зөвхөн WARN — дараагийн нэвтрэлт тухайн иргэнд хоёр дахь бүртгэл нээдэг байв.
  • №17 binding push: /auth/bind/eid/start регистрээр push илгээхдээ нэвтрэх ерөнхий текст биш «Google бүртгэл холбох: ba***@gmail.com» гэж үйлдэл ба (далдалсан) бүртгэлийг нэрлэнэ (StartSignature-ээр).
  • №28 poll: /auth/eid/poll, /auth/bind/eid/poll, /profile/identities/eid CA-аас асуухаасаа ӨМНӨ flow token-ийг зарцуулахгүйгээр шалгана (eidflow.Checker, migration 00132 registry.eid_flow_check — SECURITY DEFINER, tenant/operator/api-д EXECUTE). QR-аас session id уншсан хүн төлөвийг уншиж чадахгүй.
  • Шалгасан: go vet ./..., go test ./..., GOOS=linux go build ./... ногоон; route golden шинэчлэгдсэн. DB тестүүд (нэг бүртгэл + нэг холбоос, холбоос унавал бүртгэлгүй; eid_flow_check дөрвөн роль; impersonated /oauth2/auth; binding push текст; /profile/eid/* 403) CI-ийн postgres дээр.
  • Үлдсэн: binding-д регистрээр push-ийг бүр хаах (зөвхөн QR) нь frontend-ийн «регистр» табыг хасах, утасны хөтчөөс нэвтрэх UX шийдвэр шаардана. Халдлагад өртсөн иргэнд мэдэгдэл илгээх туслах байхгүй. credential/redeem (RecoveryRoutes) RequireJSONBody-гүй хэвээр.

Security / Fixed — эрүүл мэндийн цэвэрлэгээ (2026-09-25)

  • Дотоод алдааны текст клиент рүү гарахаа больсон (#81). Bulk байгууллага нээх мөр, invite_error, eSign HSM probe, и-мэйл health, GitHub deploy татгалзал (dispatch URL гардаг байв) — танигдсан sentinel текстээрээ, бусад нь slog + тогтмол өгүүлбэр (operator.Refusal, operator.GenericFailure). Нэр/admin имэйл/eID-гүй хүн гэсэн validation 500 → 400. Шалгасан: шинэ tenants/fail_test.go, CI ногоон.
  • Хадгалагдаж харагддаг алдааны талбар (#86). report_schedules.last_error, каталогийн синкийн last_sync_error түүхий PostgreSQL/транспорт текстийн оронд алхмын тогтмол ангилал (HTTP статус код л дамжина) хадгална; бүтэн алдаа slog-д. Үлдсэн: integrations connector, urtuu link-ийн last_error — backend нь client-gerege-nexus-д.
  • Консолын rate limit бүх replica-д хуваалцагдана (#78, #84). Нэвтрэлт, step-up, eID poll, /me/password Redis-ийн SharedLimiter-ээр (console-* түлхүүр); blue/green зэрэг ажиллахад хязгаар хоёр дахин болдог байв. Redis-гүй бол процесс доторх bucket хэвээр. Шалгасан: хоёр replica нэг budget тест (CI Redis-тэй).
  • report_grants RLS (#82, migration 00130). Grantee өөрийн хүсэлтийг өөрөө зөвшөөрөх (accepted_at INSERT/UPDATE) боломжийг DB түвшинд хаав — зөвхөн Go хамгаалж байв. Бодлого SELECT / INSERT (зөвхөн grantee, хүлээгдэж буй) / UPDATE болж хуваагдсан, report_grants_policy_test.go app role-оор шалгана. audit_events-ийн нарийн бодлого хянагдаж «reviewed». schema_hygiene_test системийн бус бүх schema-г хамарна. Үлдсэн: report_schedules хянагдаагүй.
  • jsdom 30.1.1 (#79, #75-ыг орлосон). 30.1.0-ийн хуурамч blur/focusout регресс Radix цэсийг хаадаг байв; upstream засвар. Тест/компонент өөрчлөгдөөгүй, vitest 289/289.
  • Баримт (#80). docs/ARCHITECTURE.md хэмжээсийг дахин тоолж (Go 61,674 мөр, 192 зам, migration 128/00129), routes.txt-ийн [app]/ANY мөрийг тоолох командыг засав; API гарц каталогт байгааг README (7 хэл) болон ARCHITECTURE-д бичив. frontend/app/module/{documents,urtuu,sso-clients} үхсэн код БИШ — distribution-ууд цөмийн web образаар ашигладаг тул үлдээв.
  • Git: нэгтгэгдсэн 4 локал салбар, #66-д орсон feat/api-gateway-screens remote салбар устгагдсан.

Added — Консолын «Шинэчлэл» дэлгэц: цөмийн core-sync PR-ийг хараад шийдэх (2026-09-25)

  • Юу: /cp/ops/updates (Ops → Хувилбар). Дистрибуц цөмийн аль snapshot дээр ажиллаж байгаа, хүлээгдэж буй core-sync/* PR юу авчирч байгаа (commit жагсаалт, CI төлөв), суусан аппуудын суусан/боломжит хувилбар. «Шинэчлэх» нь шалтгаан + step-up-тай, дистрибуцын core-sync.yml-ийг merge_pr input-тэй dispatch хийнэ — консол өөрөө merge хийхгүй.
  • Яагаад: core-sync PR --auto-гоор CI ногоон болмогц өөрөө merge болдог байсан — цөмийн өөрчлөлт прод руу хэн ч харахгүйгээр ордог байв. Одоо оператор юу орж ирж байгааг хараад шийднэ.
  • Хаана: backend/internal/operator/backup/updates.go (+_test.go; backup дэлгэцийн Routes-д GET /updates, POST /updates/apply; dispatchWorkflow inputs-тэй болж deploy-тэй хуваалцана), apps/builtin.go (цэс), pkg/host/testdata/routes.txt, frontend/app/cp/ops/updates/page.tsx, lib/cp.ts, lib/i18n/addons/cp.ts, docker-compose.prod.yml/.env.example (CORE_SYNC_REPOSITORY, GITHUB_SYNC_TOKEN, CORE_SYNC_WORKFLOW, CORE_SNAPSHOT_FILE), docs/OPERATIONS.md «Цөмийн шинэчлэл» (токены эрх, PR body-н ```core-commits блокийн формат, дистрибуцын хийх 3 зүйл).
  • Шалгасан: go test ./internal/operator/... (fake GitHub: тохиргоогүй, commit блок, CI rollup, fork-ийн салбар, буруу PR / улаан CI-д татгалзах, dispatch payload), go vet, GOOS=linux go build, golangci-lint, gosec; vitest (297), tsc, eslint.
  • Үлдсэн: дистрибуцын тал (snapshot-ийг image-д хуулах, merge_pr input, commit блок, --auto хасах) — тусдаа PR-аар тэр репод. Env тавиагүй суулгацад дэлгэц «тохиргоогүй» гэж харуулна.

Security — хамт ажилладаг хүн бие биеийнхээ бүртгэлийг бичихгүй; аудитад регистрийн дугаар орохгүй (2026-09-25)

  • Migration 00131 (00131_a_colleague_reads_but_does_not_rewrite_a_person.sql): registry.users, user_sso_identities, user_eid_identities дээрх 00102-ийн person_isolation нь FOR заалтгүй, бичилтэд ч «миний байгууллагын гишүүн»-ийг зөвшөөрдөг байв. Одоо person_isolation = FOR SELECT (өмнөхтэй ижил өргөн), бичилт нь person_inserts_self / person_updates_self / person_deletes_self — зөвхөн app.current_user-ийн мөр. Тенантын ролиор өөр хүний мөр бичдэг хууль ёсны зам байхгүйг Go-оос шалгасан (profile, unlink, eID холбох нь claims.UserID; нэвтрэлт/бүртгэл/recovery нь платформын зам; консол нь өөрийн бодлоготой). Down нь 00102-ийн хэлбэрт буцаана.
  • Аудит: auth.eid_login_success, auth.dan_gerege_login_success, esign.cert_checked нь reg_number/civil_id-ийн оронд *_ref = eidmongolia.AuditRef (PersonEtsi хэлбэрийн domain-separated SHA-256, 16 hex) бичнэ. audit.Record details-ийг stdout руу slog-доно, хүснэгт нь append-only тул хуучин мөрүүдэд хүрээгүй.
  • Консол: /cp/people/{id}-ийн eID subject-ийг УБ******78 хэлбэрээр далдална (maskNationalID) — frontend зөвхөн харуулдаг.
  • Яагаад: аудит №11 (гишүүн нөгөө гишүүнийхээ password_hash/email/person_etsi-г солих → eID нэвтрэлт булаах), №14 (иргэний дугаар аудит, Docker лог, операторын дэлгэцэд ил).
  • Шалгасан: go build/vet, linux/amd64 build, go test (identity, signing, operator/people, eidmongolia — AuditRef, maskNationalID unit тест). DB тест TestAColleagueReadsButDoesNotRewriteAPerson (SET LOCAL ROLE gerege_nexus_tenant) зөвхөн CI Postgres дээр.
  • Үлдсэн: AuditRef түлхүүргүй — дугаарын орон зай жижиг тул аудитын хүснэгт эзэмшигч бүрэн давтаж сэргээж чадна; HMAC-д deployment бүрийн нууц хэрэгтэй. Хуучин аудит мөрүүдэд дугаар хэвээр. signing логийн хүснэгтийн RegNo (m.log) энэ PR-т хамаагүй.

Fixed — eID App2App буцалт, poll, апп доторх browser (2026-09-25)

  • №06: утсан дээр eID апп callback-ийг шинэ tab-д нээхэд next алдагдаж үргэлж /profile руу очдог байв — гуравдагч талын «eID-ээр нэвтрэх» (/login?next=/oauth2/auth?…) OAuth урсгал дуусдаггүй. EIDLogin одоо gerege_eid_session-д {session_id, next} бичнэ (lib/eidSession.ts), callback нь зөвхөн өөрийн session-ий next-ийг safeReturnPath-аар шүүж бүтэн навигацаар очно. Хуучин энгийн мөр хэлбэрийн тэмдэглэл уншигдсаар.
  • №08: /auth/eid/callback 2 секундын interval-аар 25 секундын long-poll давхарлаж (иргэн бүрд ~12 зэрэг хүсэлт) per-IP poll хязгаарыг NAT хөршүүдэд шавхдаг байв → EIDLogin-ийн нэг-нэгээр loop (AbortController, 400ms зай, алдааны дараа 2s).
  • №24: Android-ийн Facebook/Messenger/Instagram WebView (; wv), FBAN, FBAV, FB_IAB, Instagram) retPkg=com.android.chrome авч eID апп session-гүй Chrome руу буцдаг байв → hint өгөхгүй, browser-оор нээхийг санал болгох нэг мөр (auth.eid.in_app_hint).
  • №27: components/eid/PersonRecord.tsx — орчуулгагүй CA enum утга түлхүүрээрээ (profile.eid.rep.right.SIGNER, USER_REFUSED) гардаг байв → enumLabel утгыг хүний хэлбэрт оруулна. ActivityList шүүлтүүр солигдоход хуучин хариуг хаяж, ачаалал бүрд error-ийг цэвэрлэнэ.
  • №34: lib/api/client.ts localStorage-г try/catch-д; eID регистрийн талбарт maxLength=20; баримтын eID гарын үсэг COMPLETE болсны дараа onDone унавал «гарын үсэг амжилтгүй» гэж харуулахгүй (components/documents/shared.tsx).
  • Шалгасан: npx vitest run бүтэн 302+ ногоон (шинэ tests/eid-person-record.test.tsx, callback/browser-hint тест нэмэгдсэн; засваргүй эх код дээр 13 шинэ тест унана), tsc --noEmit, eslint цэвэр.
  • Үлдсэн: регистрийн талбарт хэлбэрийн pattern тавиагүй — талбар иргэний бүртгэлийн дугаар ч хүлээн авдаг (auth.eid.instruction), backend зөвхөн урт шалгадаг; хэлбэрийг бүтээгдэхүүний шийдвэрээр. DAN гарын үсгийн onDone мөн адил (энэ PR-д хөндөөгүй).

Security — eID identity-г гэрчилгээтэй заавал холбох (2026-09-24)

  • kernel/eidrp/verify.go: person block ↔ гэрчилгээний SERIALNUMBER тулгалт аль нэг тал хоосон бол алгасагддаг байсан. Хоосон civil id үед нэвтрэлт регистрийн дугаар руу (ResolveOrProvisionEIDUser) шилждэг тул гарын үсэгт ороогүй талбараар хэн ч болж болох байв. Одоо хоёулаа заавал, serial нь PNOMN-<civil id>. Гадаад иргэний гэрчилгээ (PNO<улс>-<паспорт>) өмнөх шигээ татгалзагдана: бүртгэл eidmongolia.PersonEtsi-ээр (улсаас үл хамааран PNOMN-) түлхүүрлэгддэг тул нэг дугаартай хоёр паспорт нэг бүртгэл болно — зөвшөөрөхийн тулд бүтэн serial-ийг дамжуулах шаардлагатай.
  • auth.ResolveOrProvisionEIDUser: ge_id нь гарын үсэггүй person block-оос ирдэг. Тэр дугаараар олдсон бүртгэл өөр иргэний eID-тэй (person_etsi зөрүү) аль хэдийн холбогдсон бол ErrEIDGeIDBelongsToSomebodyElse — өмнө нь тэр бүртгэлийг авч LinkEIDIdentity холбоосыг нь дарж бичдэг байв. Нэг иргэн RegNumber-ээр (PNOMN-<рд>) холбогдсон байж болох тул хадгалсан civil_id таарвал мөн зөвшөөрнө.
  • Audit: auth.eid_app_login_success-ийн verified нь литерал true байсан → VerifiedStatus.
  • docker-compose.prod.yml: EID_VERIFY_RESPONSE default warn → enforce (прод .env аль хэдийн enforce, 09-13 23:21-ээс).
  • Шалгасан: go test ./internal/kernel/eidrp/ ./internal/workspace/auth/ ./internal/workspace/identity/... ногоон (DB тест CI-ийн postgres дээр). Прод (nexus.gerege.mn, eidmongolia.mn) хоёулаа enforce, логт «did not verify» 09-13-аас хойш 0.
  • Үлдсэн: eID-гүй холбогдсон ч ge_id-тэй бүртгэл (жишээ нь Gerege ID-аар нээгдсэн) гарын үсэггүй ge_id-ээр авагдах боломж хэвээр — CA geID-г гэрчилгээнд/гарын үсэгт оруулах эсвэл Core-оос civil id-аар тулгах шийдвэр шаардана. Үүнтэй адил халдагч хохирогчийн ge_id-г шинэ бүртгэлдээ эзэлж (idx_users_ge_id unique) хохирогчийг түгжиж болно — ge_id-г гарын үсэгтэй civil id-аар баталгаажуулсны дараа л бичих ёстой.

Removed — X-Road-ийн дурдлага (2026-09-24)

  • Мобайл нэвтрэх footer «eID Mongolia · X-Road хамгаалалттай» → «eID Mongolia · аюулгүй холболт» (Android 7 хэл, iOS 4 хэл). Windows desktop: ашиглагддаггүй XRoadClient тохиргоо, SignXroadCsrAsync stub + OrgXroadCert хасагдсан, тайлбар засав. pkg/nexus/personfeed.go ба migration 00086-ийн тайлбарт X-Road-ыг нэрлэхээ болив (SQL өөрчлөгдөөгүй).
  • Яагаад: eID Mongolia X-Road ашигладаггүй; 10-р сарын прод нээлтээс өмнө бүрэн салгав (eID CA талын ижил ажил — ca-eidmongolia-mn).
  • Шалгасан: CI. .NET/Xcode/Gradle build локалд хийгээгүй — зөвхөн ашиглагддаггүй гишүүд хасагдсан, текст солигдсон.

Changed — frontend хамаарлын багц (jsdom-гүйгээр) (2026-09-23)

  • @sentry/nextjs 10.74.0→10.75.0, lucide-react 1.45.0→1.47.0, @types/node 26.5.1→26.6.2, vitest 5.0.0→5.0.1. jsdom 30.0.1 дээр үлдэв (dependabot #63 бүлэгт байсан 30.1.0-ийг оруулаагүй).
  • Яагаад: jsdom 30.1.0 дээр Radix цэс/сонголт тестэд нээгдэхээ болино — tests/user-menu-height.test.tsx (2), tests/cp/apps-store.test.tsx, tests/cp/operators.test.tsx дөрвөн тест «Unable to find role menu/menuitem/option» гэж унана. Локалд яг тэр бүлгийг суулгаад зөвхөн jsdom-ийг 30.0.1 болгоход 3/3 ногоон болсноор шалтгааныг тусгаарлав.
  • Шалгасан: npm ci + npx vitest run → 289/289 ногоон.
  • Үлдсэн: jsdom 30.1.0 руу гарахын тулд тэдгээр тестийг (эсвэл цэсний render-ийг) шинэ jsdom-д тохируулах хэрэгтэй — dependabot #63 зөвхөн jsdom-оор үлдэж, улаан хэвээр нээлттэй байна.

Added — eID RP mTLS client cert (2026-09-23)

  • EID_CLIENT_CERT_FILE + EID_CLIENT_KEY_FILE: eID Mongolia нь RP-д require_mtls асаасан үед (rp.eidmongolia.mn) CA-ийн олгосон client cert-ийг танилцуулна. config.EIDTransport → eidrp.WithTransport (нэвтрэлт + PKI) ба eidsign.Config.Transport (PDF гарын үсэг); модульд nexus.EIDTransport (pkg/nexus, API golden шинэчлэгдсэн). Хос handshake бүрд уншигдана (сунгалт restart-гүй); эвдэрсэн бол handshake унана, cert-гүйгээр чимээгүй явахгүй. Production нэг нь дутуу / уншигдахгүй / хугацаа дууссан/эхлээгүй хостой асахгүй (ValidateProduction).
  • docker-compose.prod.yml: хоёр env + үргэлж mount хийгдэх ./eid-client:/run/eid-client:ro; .gitignore-д eid-client/. Баримт docs/OPERATIONS.md §eID. Тест TestEIDTransportPresentsClientCert (httptest RequireAndVerifyClientCert), TestValidateEIDClientCert.

Added — консолоос одоо байгаа гишүүний ролийг солино (2026-09-23)

  • Байгууллагын хуудасны «Хэрэглэгчид» хүснэгтийн мөр бүрт «Роль» товч (tenant.create эрхтэй оператор): сонгосон рольнууд тухайн хүний бүх эрх болно, дор хаяж нэг админ үлдэнэ. PUT /tenants/{id}/people/{userID}/roles (CapTenantCreate + step-up), аудит tenant.member.roles (before/after). Workspace-ийн «Хандалтын тохиргоо»-той ижил дүрэм: үл мэдэгдэх/идэвхгүй роль 400, сүүлийн админыг хасах 409 (ErrLastAdministrator), гишүүн биш 400. backend/internal/operator/tenants/members.go.
  • «Хүн нэмэх» цонх аль хэдийн гишүүн хүнийг «Аль хэдийн гишүүн» гэж идэвхгүй харуулна; 409-д ерөнхий «өөрчлөлт орсон» биш тодорхой мессеж. Шалтгаан: petronet.mn дээр гишүүнийг дахин нэмэх гэж оролдоход ойлгомжгүй алдаа гарсан.
  • Консолын frontend-ийн эрхийн хуулбарт tenant.create дутуу байсныг backend-тэй тааруулав. Тест: TestAMembersRolesAreReplaced (DB), tests/cp/tenant-detail.test.tsx.

Added — консолоос хүн нэмэхдээ роль сонгоно (2026-09-23)

  • Консолын байгууллагын хуудасны «Хүн нэмэх» цонхонд байгууллагын өөрийн идэвхтэй рольнууд (user-ээс бусад) checkbox-оор гарна; admin сонговол анхааруулга. POST /tenants/{id}/people нь roles: string[] авна — хоосон бол өмнөх шигээ зөвхөн trigger-ийн user. Үл мэдэгдэх код бүх өөрчлөлтийг цуцална (ErrUnknownRole → 400); аль хэдийн гишүүн бол 409 (өмнө нь 500). Аудитын after-д roles. GET /tenants/{id} нь roles буцаана. Шалтгаан: petronet.mn-ийн хүсэлт — консол байгууллага нээхдээ анхны админыг аль хэдийн томилдог тул шинэ эрх нээхгүй; байгууллагын админ «Хандалтын тохиргоо»-оос буцааж болно. backend/internal/operator/tenants/{directory,fail,tenants}.go, frontend/app/cp/tenants/[id]/page.tsx, frontend/lib/cp.ts, cp.* i18n (mn/en). Тест: TestAnAddedPersonGetsTheChosenRoles (DB), tests/cp/tenant-detail.test.tsx.

Fixed — /settings/access админд роль ба гишүүд хоосон харагддаг байв (2026-09-23)

  • useAccess() mount бүрд isAdmin=false-оор эхэлдэг, дэлгэц ганц удаа (useLoadOnMount) ачаалдаг тул админд ч зөвхөн нэгдэх хүсэлтийг татаж, «Role ба эрх», «Хэрэглэгчид» таб хоосон үлддэг байсан (/admin/access/overview огт дуудагдаагүй). Identity ирэх хүртэл spinner, дараа нь isAdmin-ийг prop-оор — frontend/app/settings/access/page.tsx. Тест frontend/tests/access-settings.test.tsx (хуучин кодод унана). petronet.mn дээр илэрсэн (petronet-gerege-nexus#6).

Fixed — нүүр/нэвтрэх хуудасны control-ын өндөр нэг болов (44px)

  • Header-ийн «Нэвтрэх» 43, hero CTA 53, eID картын таб 40, оролт 48, илгээх товч 36px байсныг бүгдийг 44px (design-research 13-landing «xl») болгов — frontend/app/globals.css, --gp-control-h. Шалтгаан: gerege-ui-ийн data-style="nexus" нь Button-ийн size-ийг --style-control-height (36px)-аар дардаг тул size="xl" ажилладаггүй байсан; eID карт дотор хувьсагчийг нь 44px болгож засав. Хэлний сэлгүүр, mobile цэсний товч, /login-ийн eID карт мөн адил.

Added — API гарц апп (io.gerege.nexus.api_gateway)

  • Цөмд багтсан апп backend/internal/apps/apigateway (apps.Bootstrap), catalog/apps.json-д бүртгэгдсэн, байгууллага App Store-оос суулгах хүртэл идэвхгүй. Нийтийн гарц /api/v1/gw/<slug>/* + X-API-Key: gwk_…: түлхүүрийг SHA-256-аар хайх, key→API эрх, аппын суулгац (30с cache), (түлхүүр, API) тутмын минутын хязгаар, upstream руу API бүрийн шифрлэсэн нууц X-Gateway-Secret (nexus.Secrets). SSRF: https 443/8443, dial үед хувийн/metadata хаяг татгалзана; Cookie, X-Real-IP, X-Forwarded-* дамжихгүй, Set-Cookie буцахгүй, CSP sandbox; ../..;/\/NUL зам 400. Удирдлага /api/v1/api-gateway/* gate-ийн ард (api_gateway.read/.manage), аудит мөртэй. Хэрэглээ санах ойд тоологдож 5с тутам tenant бүрд бичигдэнэ (StartHousekeeping).
  • Схем: модулийн өөрийн migration (workspace.api_gateway_*, FORCE RLS, platform_path + tenant_isolation, composite FK). cmd/migrate up цөмд багтсан модулийн migration-ыг платформынхын дараа эзэмшигч role-оор хэрэгжүүлдэг болов — API gerege_nexus_api-аар DDL хийж чадахгүй тул.
  • Мэдэгдэж буй хязгаар: түдгэлзүүлсэн байгууллагын түлхүүр ажилласаар байна (SDK түдгэлзүүлэлтийг илэрхийлдэггүй).
  • frontend/app/module/api-gateway/{apis,keys,usage} — гурван дэлгэц: upstream API нэмэх/засах/устгах ба нийтийн хаяг /api/v1/gw/<slug>/, upstream-ийн нууц (X-Gateway-Secret, үүсгэх/солих үед нэг удаа), гарцын түлхүүр үүсгэх (нэг л удаа харагдана)/хүчингүй болгох, өдрийн хэрэглээ (7/30/90 өдөр). lib/api/api-gateway.ts, толь lib/i18n/addons/api_gateway.ts (mn/en), цэсний icon-ууд (network, key-round, activity) generate-icon-map.mjs-ийн жагсаалтад аль хэдийн бий.

Security — хүний хувийн суулгацууд хүснэгтийн эзэнд нээлттэй байв

  • workspace.person_app_installations (00112) нь RLS-ээ ENABLE хийсэн ч FORCE хийгээгүй тул хүснэгтийн ЭЗЭН ролиор ирсэн холболт бүх хүний App Store-ийн жагсаалтыг уншиж, бичиж чаддаг байв (00102 нь гурван registry хүснэгтэд ENABLE-ийн дараа шууд FORCE хийдэг — энэ мөр дутсан). 00128 FORCE-ийг нэмнэ. Апп нь gerege_nexus_tenant-аар холбогддог тул прод дээрх зан төлөв өөрчлөгдөхгүй. Тест: каталогийн relforcerowsecurity + тенант ролиор бодлого хэвээр шүүж байгаа эсэх (db/migrations/person_installs_forced_test.go).

Fixed — алдааны дэлгэц өөрөө унаж 500 буцаадаг байв

  • Танигдахгүй Next-Action толгойтой хүсэлт (сканнерын хуурамч утга, эсвэл өмнөх build-ийн action id) дээр Next нь app/error.tsx-ийг layout-гүйгээр render хийдэг — тэнд useI18n()-ийн хамгаалалт useI18n must be used inside I18nProvider гэж шидэж, хүсэлт хоосон 500 буцаж байв (eidmongolia.mn: 72 цагт 3 удаа, бүгд сканнераас). Хоёр хил (app/error.tsx, app/not-found.tsx) одоо I18nBoundary-д ороосон: provider олдохгүй үед л өөрийнхөө provider-ийг холбоно, байвал юу ч хийхгүй. tests/boundaries-without-providers.test.tsx хоёуланг нь provider-гүй render хийж шалгана.

Fixed — deploy хаалга GITHUB_TOKEN-ийг татгалздаг байв

  • deploy-gate.sh token-ийг ^[A-Za-z0-9_]{20,255}$-ээр шалгаж бодит GITHUB_TOKEN-ийг татгалзав; одоо хоосон биш, ≤4096, зай/удирдах тэмдэгтгүй эсэхийг л шалгаж, алдаанд уртыг бичнэ (#59). Хост дээрх хаалгыг гараар шинэчилсэн.

Fixed — root-гүй deploy-ийн эхний ажиллагаа файл хуулахдаа унав

  • scp-action-ийн tar_tmp_path хоосон үед tar нь deploy хэрэглэгчийн root-ын эзэмшлийн HOME руу бичигдэх гэж Process exited with status 1-ээр унадаг байв. tar_tmp_path нь 0700 staging (.env.next агуулсан tar /tmp-д үлдэхгүй). docs/OPERATIONS.md: authorized_keys-ийн мөрийг сонголтгүй хуулах, хоёр алдааны мөр хүснэгтэд.

Security — CI deploy root-оор нэвтрэхээ больсон

  • deploy.yml нь private-nexus-deploy хэрэглэгчээр (docker бүлэгт биш, SSH түлхүүр restrict) staging руу хуулж, sudoers дахь ганц команд /usr/local/sbin/private-nexus-deploy-г (deploy/scripts/deploy-gate.sh, root гараар суулгадаг) дуудна. Хаалга staging-ийг атомикаар зөөж, symlink/төхөөрөмжийг татгалзаж, зөвшөөрсөн замуудыг root:root-оор хуулна; registry token stdin-ээр (sudo лог руу орохгүй). Анхны тохиргоо docs/OPERATIONS.md-д.

Changed — monitor.nexus-ийн Alertmanager open stack-аас тусдаа нууц үгтэй

  • /alertmanager/ одоо /etc/nginx/.htpasswd-private-nexus-monitoring-ийг уншина; өмнө нь нэг хост дээрх open stack-тай нэг файл хуваалцдаг байв (аудит M15).

Fixed — S3 толь/өөр байршил руу илгээлт «signature does not match»

  • backup_offsite.py итгэмжлэлийг percent-encode хийж MC_HOST_store URL-д бичдэг байв; mc тэр userinfo-г decode хийдэггүй тул тусгай тэмдэгттэй нууц MinIO-д өөрчлөгдөж хүрч, 09-14-өөс толь руу илгээлт бүр унав. Одоо mc-env задгай MC_ENDPOINT/MC_ACCESS_KEY/MC_SECRET_KEY гаргаж, backup.sh контейнер дотор mc alias set-ээр тохируулна; мөр таслалттай итгэмжлэлийг татгалзана.

Added — хостоос гадуурх нөөц restic rest-server руу

  • backup.sh: RESTIC_REPOSITORY + RESTIC_PASSWORD тохируулсан бол dump-ийг restic (append-only rest-server) руу илгээж, restic ls latest-ээр шалгана. restic өөрөө шифрлэдэг тул age шаардахгүй. nexus_backup_offsite_* хэмжүүр S3 зорилттой ижил дүрмээр (endpoint-ийн хост ангилал); шинэ шалтгаан restic_missing. Нэг зорилтын амжилтыг нөгөөгийн бүтэлгүйтэл дарахгүй. Алдааны мөрөөс repo-гийн http нууц үгийг хасна.

Fixed — backup-verify.sh restore бүх role-ыг үүсгэнэ

  • Role давталт доторх docker exec -i stdin-ийг залгиж зөвхөн эхний role үүсдэг байв — dump-ийн GRANT/POLICY «role ... does not exist» гэж унана (</dev/null).

Changed — Сэлгэгч дээр гэр «Миний гэр» гэж харагдана

  • Sidebar-ын сэлгэгч болон TenantChoices-ийн гэрийн мөрөнд хэрэглэгчийн нэрийн оронд «Миний гэр» (#54).

Changed — workspace.New туслах функцүүдэд хуваагдав

  • 334 мөр байсан New 117 мөр болж, publishCapabilities, federatedSignInFromEnv, loadCatalogue, googleLoginFromEnv, buildPackages, attachToSSOProvider, registerCachesAndBudgets-д хуваагдав (#51). nexus.Provide-ийн дараалал, side effect, алдааны мессеж өөрчлөгдөөгүй.

Added — identity/eidmongolia-ийн тест

  • Адаптерын unit тест (ETSI танигч, PEM эх үүсвэр, Representations-ийн wire хэлбэр, usecase руу дамжуулалт) ба гарын үсгийн төлөвийн DB тест (round-trip, хугацаа дуусалт + sweep, mock ceremony) (#52, аудит L17).

Fixed — Цэсийн мөрөнд icon нэрийн дээр тусдаа мөрөнд буудаг

  • Консол (ConsoleLink) ба workspace (NavLink, ExternalNavLink)-ийн цэсийн мөр icon-оо SidebarItem asChild-ийн хүүхэд болгож дамжуулдаг байв. Сан хүүхдийг бүхэлд нь inline truncate span-д ороодог тул icon нэг мөрөнд, нэр доор нь бууж, мөр h-8-аас халидаг байв. Icon одоо icon prop-оор, гадагшаа заах тэмдэг trailing prop-оор дамжина.

Fixed — SECURITY DEFINER функцүүдийн зам, гадны асуугч дараалалд, хаалганы татгалзлын хариу

  • Migration 00125: SECURITY DEFINER функц бүр search_path-аа pg_catalog-оор эхэлж pg_temp-ээр төгсгөнө. create_tenant_profile, resolve_device_enrollment, authenticate_device-ийн биеийн хүснэгтийн нэр schema-тай болсон (pg_temp-гүй зам дээр дуудагчийн түр хүснэгт devices-ийг орлож чадах байв); eID, OIDC түлхүүр, аудитын гинжийн есөн функцэд pg_temp төгсгөлд нэмэгдсэн. Гарын үсэг, эрх өөрчлөгдөөгүй. definer_search_path_test.go дүрмийг схемээс шалгана.
  • Байгууллагын нэгдэх хүсэлтийн дараалал (GET /api/v1/admin/access/join-requests) гаднаас асуусан хүнийг харуулдаггүй байв: registry.users-ийн person_isolation (00102) гишүүн биш хүнийг tenant session-ээс нууж, JOIN мөрийг чимээгүй хаядаг. Migration 00126-ийн registry.join_request_askers(tenant) зөвхөн тухайн байгууллагад PENDING хүсэлттэй хүмүүсийн id/нэр/и-мэйлийг буцаана, уясан session-д зөвхөн өөрийн ажиллаж буй байгууллагынхыг. registry.users-ийн бодлого өргөсөөгүй.
  • Нэгдэх хүсэлтийн татгалзал: askRefusal нь request_to_join-ий хэзээ ч өгдөггүй P0001-ийг хүлээж байсан тул байхгүй нэр ба түр зогссон байгууллага 500, «аль хэдийн гишүүн» нь «аль хэдийн хүсэлт илгээсэн» гэсэн буруу 409 буцаадаг байв. Одоо функцийн жинхэнэ код (P0002 / 55000 / 23505) ба мессежээр таньж 404 / 409 / 409.

Security — eID RP-ийн хүлээгдэж буй бичлэг (багц 3)

  • eID: a bound database session (module code, the console) can no longer replace another person's pending eID relying-party record; registry.eid_rp_pending_put refuses to overwrite a live record outside the platform path (migration 00127).
  • Migration tests fail if any SECURITY DEFINER function is executable by PUBLIC.

Changed — CI, контейнер, баримтын эрүүл ахуй (багц 3)

  • CI-ийн хилийн шалгуур юу ч хэмждэггүй байв: байхгүй backend/domain-ыг pipefail-гүй pipe-аар шалгаж, хуучин module замыг grep хийдэг байсан тул үргэлж ногоон. Одоо internal/kernel болон урсгал бүрийн (workspace/operator/person) дамжсан хамаарлыг go list -m-ээс авсан module замаар, pipefail-тэй шалгана — internal/planes_test.go шууд import-ыг шалгадгийн нөхөлт.
  • GitHub-ийн өөрийн action-ууд (checkout, setup-go, setup-node, cache, upload-artifact, setup-dotnet, setup-java) бүх workflow-д commit SHA + # vX.Y.Z-ээр тогтоогдсон; govulncheck@latest → v1.8.0.
  • docker-compose.prod.yml: backend/frontend хоёр slot-д no-new-privileges, cap_drop: ALL, read_only (+ /tmp, .next/cache tmpfs), pids_limit: 512, mem_limit (backend 1g, frontend 768m).
  • CI-д Shellcheck ажил нэмсэн. Олсон бодит алдаа: setup_device_lines.sh-ийн heredoc доторх backtick нь тусламжийн текстийг команд болгон ажиллуулдаг байв.
  • Tracked файлаас бодит хостын IP болон албаны IP-г хассан (docs/OPERATIONS.md, address_test.go → RFC 5737 / RFC 1918 жишээ хаяг).
  • Баримтын зөрүү: migration-ийн тоо, kernel багцын жагсаалт, module нэр, .golangci.yml болон ci.yml-ийн хувилбарын тайлбар.
  • Shellcheck-ийн хувилбарыг v0.11.0 + sha256-аар тогтоосон; backup-verify.sh SC2015-ийг шалтгаантай disable хийсэн.

Changed — workspace-ийн замын бүртгэл (багц 3)

  • workspace: замын бүртгэл дэд багц бүрд шилжсэн (auth, identity, access, profile, devices, appinstall, ssoprovider, emailverify, ai — тус бүр routes.go); service.go-ийн Routes 346 → 60 мөр. Зан төлөв өөрчлөгдөөгүй.
  • test: internal/workspace/testdata/route_chains.txt — зам бүрийн middleware-ийн дараалал ба rate-limit budget-ийг бэхэлсэн golden (DB шаардахгүй).

Security — backend хатууруулалт (багц 3)

  • Хүсэлтийн биеийн дээд хэмжээ процесс даяар. Router бүх хүсэлтэд 1 MiB (httpx.LimitBody) тавина; Content-Length хэтэрвэл handler-т хүрэхээс өмнө 413 PAYLOAD_TOO_LARGE. Зөвхөн POST /api/v1/esign/* (PDF upload) нь signing.MaxUploadBody (34 MiB) хүртэл. Handler-уудын өөрсдийн хязгаар хэвээр.
  • Mock ба demo seed зөвхөн explicit. *_MOCK_MODE, SEED_DEMO_DATA тавиагүй бол ямар ч орчинд (development ч гэсэн) унтраалттай. ENVIRONMENT хоосон атал PUBLIC_ORIGIN нь HTTPS бол сервер асахгүй. docker-compose.yml local-д mock/seed-ийг өөрөө асаана; өөрийн compose/env-тэй dev, staging орчинд эдгээрийг нэмэх шаардлагатай.
  • Консолын мониторингийн уншилт SSRF-ээс хамгаалагдсан. Prometheus / Alertmanager хаяг руу redirect дагахгүй, link-local (cloud metadata), multicast, unspecified хаяг руу залгахгүй, зөвхөн http(s); loopback/дотоод хаяг хэвээр.
  • Унтрах хугацаа хязгаартай. Trace/metric/Sentry flush нэг 3 секундын deadline-тай; /health, /ready load shedder-т тоологдохгүй.
  • Каталогийн rollback хаагдсан. Registry-ээс хүлээн авсан хамгийн шинэ generated_at-аас хуучин гарын үсэгтэй баримтыг (дискний кэштэй ч) татгалзаж, одоогийнхоо үлдээнэ; generated_at нь RFC 3339 биш бол хүлээж авахгүй.
  • OAuth2 client secret тогтмол хугацаат харьцуулалт; last_used_at бичиж чадаагүйг логлоно.
  • /oauth2/token, /oauth2/revoke (600/мин), /oauth2/introspect (6000/мин) IP тутамд хязгаартай, Redis байвал deployment даяар.

Security — нэвтрэлтийн жижиг цоорхойнууд (auth batch 3)

  • Операторын консол: нэг TOTP код зэрэг ирсэн хоёр хүсэлтэд хоёр session / хоёр step-up нээж чаддаг байв. Алхмыг totp_last_step < $step нөхцөлтэй UPDATE-ээр зарцуулж, мөр өөрчлөгдөөгүй бол буруу код гэж хариулна.
  • Консолын auth.eid_login унтраалттай үед /auth/eid/poll ба /auth/dan/login мөн 403 sign_in_method_disabled буцаана. /auth/bind/eid/start|poll нь RequireLocalLogin-д орсон; eID унтраалгад зориуд ороогүй (Google-ийн анхны нэвтрэлтийн хагас, дангаараа нэвтрэх зам биш).
  • /auth/dan/login алдааны шалтгааныг клиентэд буцаахаа болив: DAN_UNAVAILABLE (503) эсвэл DAN_VERIFICATION_FAILED (401), шалтгаан нь зөвхөн логт.
  • auth.login_failed audit мөр бичсэн login утгыг хадгалахаа болив (буруу талбарт бичсэн нууц үг орж болдог байв); бүртгэлтэй таарвал зөвхөн user_id.
  • /oauth2/userinfo нь ge_id-г id_token-той ижил дүрмээр — email эсвэл profile scope байхад л — буцаана.
  • /auth/eid/start-id (ба binding-ийн регистрээр эхлүүлэх) eID татгалзсан дугаарт 400 EID_NATIONAL_ID_NOT_REGISTERED буцаахаа болив: амжилттайтай ижил хэлбэрийн decoy session өгч, poll нь хэзээ ч COMPLETE болохгүй (2 минутын дараа EXPIRED). Шалтгаан логт ба start_by_national_id гадаад дуудлагын метрикт үлдэнэ.
  • eID бүртгэлийг <дугаар>@gemail.com хаягаар хайхаа болив (зөвхөн ge_id); provisioning-ийн ON CONFLICT(email) өөр ge_id-тай мөрийг өөрийн болгохгүй; @gemail.com хаягийг «зохиомол» гэж дарж бичихээ болив. Домэйн өөрөө хэвээр.
  • /oauth/consent нь 401-ийг орчуулсан мессежээс regex-ээр таньдаг байсан тул монгол хэл дээр нэвтрээгүй хүнийг /login?next= руу илгээдэггүй байв — одоо isUnauthorized (статус).
  • sign_in_method_disabled (403) кодыг describeError нь auth.message.sign_in_method_disabled болгоно: нууц үгийн таб «эрхгүй» гэж, eID карт англи серверийн текстийг харуулахаа болив.
  • Sidebar-ын external_url нь зөвхөн http(s) байвал холбоос болно (lib/externalUrl.ts); javascript: зэрэг хаяг бүхий оруулга хаягдана.
  • Профайл: нэвтрэх нэрийн талбар autoComplete="off" (хөтөч и-мэйл бөглөж 400 авдаг байсан), түүний мессеж өөрийн мөрөнд; profile.password.not_set нь eID-г дурдахаа болив (Google/SSO хэрэглэгчид буруу байсан).
  • Анхны нууц үг тохируулсны дараа цонхны гарчиг/амжилтын текст «солих» болж хувирдаг байсан — горимыг нээх үед нь тогтооно.
  • /login талбаруудад maxLength (нэвтрэх 320, нууц үг 1024); профайлын шинэ нууц үг ч 1024-өөр хязгаарлагдана.
  • ESLint @typescript-eslint/no-explicit-any дахин асав (үлдсэн any-г засав); ашиглагдаагүй disable/хувьсагчийн анхааруулга арилав.

Fixed — байгууллагын экспорт бүх хүснэгтээ харна, нууц баганыг авч явахгүй

  • GET /api/platform/v1/tenants/{id}/export нь RequireOperator-ын gerege_nexus_operator role дээр ажиллаж байсан тул зөвхөн консолын эрхтэй ~10 хүснэгтийг олж, бусдыг (төхөөрөмж, push token, модулийн бүх хүснэгт) 200 хариутай чимээгүй орхидог байв — устгахын өмнөх экспорт бараг хоосон. Одоо dbguard.OnPlatformPath-аар платформын зам дээр уншина (эрх, step-up урьдын адил), хүснэгтийг pg_catalog-оос олно, бүх мөрийг нь уншиж чадахгүй хүснэгт байвал экспорт нэрлэж унана.
  • Нууцын багана (*_hash — audit гинж ба document_hash-аас бусад, token, secret, ciphertext, password, pin, totp, nonce г.м.) экспортод орохгүй; юу хасагдсаныг bundle-ийн withheld талбар нэрлэнэ.

Changed — App Store-оос өөр ямар ч апп built-in шиг орлохгүй

  • Tenant shell: ямар ч суусан апп эзэмшээгүй хуудсанд «Байгууллага» / «Миний гэр»-ийн самбар автоматаар орж ирэхээ болив — тэд бусад апп шиг зөвхөн өөрийн хуудсандаа.
  • Консол: /apps/menus уншигдахгүй үед Профайл, Платформ tile-ыг кодоос зурахаа болив; зөвхөн built-in App Store үлдэнэ.
  • EnsureDefaultApps хувь хүний workspace-д (kind = 'personal') байгууллагын апп суулгахаа болив (тэдний цэс хүний өөрийн дэлгүүрээс бүтдэг).

Added — нэвтрэх картанд нэвтрэх нэр/нууц үгийн таб; аргуудыг консол асаана

  • Консолын тохиргоо: auth.eid_login (анхдагч true, AUTH_EID_LOGIN) ба auth.username_login (анхдагч false, AUTH_USERNAME_LOGIN). Нэвтрэх карт хоёулаа асаалттай бол eID / Нэвтрэх нэр гэсэн хоёр таб, нэг нь бол зөвхөн тэр; хоёулаа унтарсан бол eID асаалттай гэж тооцогдоно. Унтраалттай аргын API (/auth/login, /auth/eid/login|start|start-id) 403 sign_in_method_disabled буцаана. Хуучин «Системийн админ нэвтрэлт» accordion устав.
  • /auth/login нь и-мэйл эсвэл нэвтрэх нэрийг (login талбар; email хэвээр) хүлээж авна.
  • Профайл → «Нэвтрэх нэр ба нууц үг»: POST /profile/username; POST /profile/password нь нууц үггүй (eID-ээр л ирсэн) хүнд одоогийнхыг асуулгүй анх тохируулна.
  • Migration 00122: registry.users.username (varchar 32, жижиг үсэг, @-гүй, unique) ба password_set_at; одоо байгаа дансыг нууц үгээр нэвтэрч байсан / холбоосоор сонгосон / солисон / админ эсэхээр нь нөхөж бөглөнө.

Changed — Профайлууд энгийн апп боллоо; цорын ганц built-in нь App Store

  • Консол: nexus.console.profile нь built-in байхаа больж, Платформ/Байгууллагууд/Ops-ийн адил цөмийн апп (суулгаж, идэвхгүй болгож, хасаж болно). Цорын ганц built-in нь nexus.console.store — /cp/apps. Migration 00121 одоо байгаа консолд профайлыг суусан төлөвтэй seed хийнэ.
  • Workspace: «Миний гэр» (nexus.profile) болон байгууллагын профайл (nexus.organisation_profile) нь цэсэнд шууд зурагддаг built-in биш, internal/workspace/coreapps-ийн апп. Байгууллагын профайлыг tenant бүрд EnsureDefaultApps суулгана; «Миний гэр»-ийг хүн бүрийн анхны уншилт дээр суулгана. Хассан ч мөр нь enabled = FALSE-ээр үлдэж, дахин суулгагдахгүй.
  • /menus нь цорын ганц built-in nexus.store-ыг илгээнэ (байгууллагад /apps, гэрт /me/apps); shell болон консол түүнийг rail-ийн ёроолд id-гаар нь тогтооно.

Security — аудитын засварууд (fix/security-hardening, 00115–00118)

  • Impersonation session байгууллага солих / унших хүрээгээ тэлэхээс татгалзана; нууц үг, identity холбох/салгах, logout-all, push token, OAuth consent нь impersonated session-д 403. Идэвхгүй membership session-ээ алдана.
  • 00115 gerege_nexus_api: superuser/BYPASSRLS-гүй login role (platform_path бодлого). dbguard эрх ихтэй login role-ыг production-д ERROR + метрикээр мэдэгдэнэ.
  • eID: эхлэлийн flow token (browser — HttpOnly cookie, native — flow_token), finish бүр нэг удаа (00118 registry.eid_flows); VerifiedStatus бодит шалгалтаас; production-д EID_VERIFY_RESPONSE=enforce биш бол асахгүй; консолын eID нэвтрэлт step-up биш; national ID-аар push хийхэд хязгаар.
  • Консол: step-up TOTP-д limiter + 5 алдааны дараа session цуцлагдана; оператор нууц үг солиход бусад session цуцлагдана; устгах/өөрчлөх бичилтүүд step-up шаардана; TRUSTED_PROXY_CIDRS (анхдагч loopback); CONTROL_PLANE_ALLOWED_CIDRS access mode-оос салж, production-д хоосон бол хаалттай.
  • Нууц: operator TOTP secret ба OIDC signing key security.Seal-ээр (00116), rotation (cmd/oidc-signing-key); refresh token race, revoke/introspect-ийн client шалгалт, бодит auth_time.
  • 00117: workspace.audit_events DELETE-ээс хамгаалагдана, tenant тус бүрийн hash chain DB дотор + workspace.audit_chain_verify.
  • JSON body хязгаар, DB/upstream алдааны текст клиентэд хүрэхгүй (kernel/pgerr), PDF parse timeout, mock mode production-д асахгүй, PII логоос, logo_url/website зөвхөн http(s), access өөрчлөлтийн бүртгэл нэг tx-д, consent цуцлалт атомар, unlink race, жагсаалтуудад LIMIT.
  • Breaking — /oauth2/introspect: active:true зөвхөн токен олгогдсон клиентэд, эсвэл тухайн байгууллагад бүртгэлтэй confidential клиентэд (жишээ нь байгууллагын resource server өөрийн SPA-ийн токеныг шалгах). Өөр байгууллагын клиент ба public клиент active:false (үл мэдэгдэх токентой адил) авна. Гадны resource server токен эзэмшигчийн байгууллагад confidential клиентээр бүртгэгдсэн байх ёстой.
  • Client requirement — refresh token: эргүүлэлт нэг транзакцид (хуучныг цуцлах + шинийг хадгалах); серверийн алдаа хуучин refresh token-ыг хүчинтэй үлдээнэ, дахин оролдох аюулгүй. Нэг refresh token-ыг зэрэг хоёр удаа (хоёр tab) илгээвэл нэг нь replay гэж тооцогдож гэр бүл бүхэлдээ цуцлагдана — клиент нэг токенд нэг л refresh-ийг зэрэг ажиллуулна (single-flight).
  • 00117: CHECK-үүд NOT VALID, гинжийн unique индекс 00119-д CONCURRENTLY (бичилтийг хаахгүй); устгалын үл хамаарах шалгалт SECURITY DEFINER функцээр.
  • 00115: gerege_nexus_api goose-ийн хувилбарын хүснэгтэд бичих эрхгүй.
  • eID: flow зарцуулсны дараа хэрэглэгч/session үүсгэх алдаа гарвал flow буцааж нээгдэнэ (registry.eid_flow_reopen, 00118) — иргэний зөвшөөрөл шатахгүй.
  • eSign HSM: клиентийн timeout 30 s (write deadline 40 s-ээс бага); баримт нөхцөлтэйгээр SIGNED болно — давтан хүсэлт 409 ALREADY_SIGNED + баримтын id/төлөв/огноо авна, хоёр дахин гарын үсэг зурахгүй; upstream-ийн текст клиентэд хүрэхгүй.

Changed — API-ийн нэмэлт талбарууд (хуучин хэлбэр хэвээр)

  • GET /api/platform/v1/people/roster: ?cursor= + хариуны next_cursor (?offset= хэвээр ажиллана); хайлт trigram индексээр (00120, pg_trgm).
  • Жагсаалтын хязгаар 500 + truncated: /admin/devices (truncated), байгууллагын хуудасны гишүүд (members_truncated), апп-ийн түүх (truncated).
  • Алдааны code: EID_NATIONAL_ID_NOT_REGISTERED, EID_ACCOUNT_LINK_FAILED, HSM_REJECTED, NOT_SIGNABLE (error текст монголоор хэвээр).
  • pkg/nexus: ListPage[T] (шинэ endpoint-ийн жагсаалтын хэлбэр); testdata/api_changes.txt — api.txt-ийн өөрчлөлт бүр бичигдэнэ.

[1.20.0] - 2026-09-13

1.15.0-оос хойшхи бүх ажил. backend/v1.18.1 (2026-09-07) ба backend/v1.19.0 (2026-09-08) tag-ууд энэ файлд гарчиггүйгээр тавигдсан тул — release.yml тэдний тэмдэглэлийг олоогүй — тэдгээрт орсон өөрчлөлтүүд ч энд байна. Дугаар нь 1.16.0 биш: Go-ийн module proxy ба go get хамгийн өндөр semver-ийг сонгодог тул v1.19.0-оос доош tag «шинэ» хувилбар болж чадахгүй. Distribution цөмийг хэрхэн дагадгийг Distribution-оос үзнэ үү.

Changed — frontend бүхэлдээ @gerege-systems/ui дээр (#17, #18)

  • Суурь (71c20bc2): Tailwind 3 → 4 (tailwind.config.ts устаж globals.css-ийн @theme болов, @tailwindcss/postcss), @gerege-systems/ui 0.14.0 + tw-animate-css. <html data-style="nexus" data-depth="soft"> — сангийн Nexus style (6/8px радиус, 36px товч, 40px мөр). DesignSystemProvider, TooltipProvider, Toaster root-д. components/ui-ийн примитивүүд сангийнх руу adapter болов (Button, Alert, Badge, Spinner, Skeleton, Dialog, Table, ConfirmationDialog).
  • Shell ба дэлгэцүүд (ac421e4d): tenant shell (components/Layout.tsx) ба операторын консол (components/cp/Console.tsx) сангийн Sidebar/TopNav/Sheet/ DropdownMenu дээр; settings, profile, me, login/setup, store, organisation, SSO клиент, integrations, urtuu, reports, documents, esign, kiosk дэлгэцүүд сангийн форм, Card, Table, Dialog, toast-оор. globals.css 1642 → 1039 мөр — хуучин shell-ийн .gerege-* дүрмүүд устав.
  • Shell-ийн нарийвчлал (cf97cbf7, 61876f7d, ae1beb8a; #19–#21): rail-ийн дээд тэмдэг нь deployment-ийн лого бөгөөд байгууллагын сонгогч-ийг нээнэ; App Store нь аппын цэснээс гарч rail-ийн ёроолд тусдаа tile болов (aba3b4f5); идэвхтэй цэс/апп accent-soft өнгөөр; top bar ба sidebar-ийн булан 0; консолын host theme-init.js, sw.js, offline.html-ыг дамжуулдаг болов (өмнө нь 404, dark горим анивчдаг байв).
  • Багц D (#23 + дагалдах commit): components/ui, components/cp/ui, components/module/kit adapter-ууд устгагдаж дэлгэцүүд сангаас шууд импортлоно; операторын консол (app/cp/*), app/dev/ca, login ба bind, PersonRecord, landing-ийн цэсний товчны гараар бичсэн button/input/select/textarea нь сангийн Button/IconButton/Input/Textarea/Select/Checkbox болсон (/cp/apps-ийн «Түүх» ба /cp/profile/account-ийн хоёр товч дагалдах commit-оор). Landing (.gp-*) ба sign-in (.signin-*) зохион байгуулалтын CSS хэвээр. eidmongolia distribution-ий CA консолын залгаас тэр репод тусдаа шилжсэн.

Added — deployment-ийн харагдац BRAND_THEME_*-ээр (cc32726a, #20)

  • Сангийн theme.css бол токены ганц эх сурвалж; хуучин --gerege-* нэрс бүгд сангийн семантик токены alias. Цөмийн өөрийн харагдац ч ui.gecore.mn/#theme-ийн «Get code» гаралтаар бичигдсэн (globals.css, «Theme editor» блок: style nexus, depth soft, accent #0064e1). Анхдагч accent cobalt.
  • frontend/lib/brandTheme.ts: BRAND_THEME_STYLE / _DEPTH / _RADIUS / _ACCENT → <html>-ийн data-* атрибут (зөвшөөрөгдсөн утгуудаар шүүгдэнэ); BRAND_THEME_CSS_FILE / BRAND_THEME_CSS → <body>-ийн төгсгөлд nonce-тэй <style>. Distribution цөмийн код засахгүйгээр «Get code»-оо наана.
  • BRAND_THEME_COLOR өөр зүйл хэвээр: PWA manifest ба <meta name="theme-color">-ийн hex өнгө, UI-ийн токенд хүрэхгүй.
  • Тохиргоо → Харагдац дээрх accent зөвхөн хэрэглэгч сонгосон үед (accentChosen) data-accent болно — хуучин localStorage-д үлдсэн анхдагч neutral deployment-ийн өнгийг дардаг байв (ae1beb8a). «Суулгацын өнгө» сонголт буцаана.

Security — frontend аудитын багцууд (#14, #15, #16)

  • CSP nonce: proxy.ts хүсэлт бүрд nonce үүсгэж script-src 'nonce-…' 'strict-dynamic', frame-ancestors 'none', base-uri 'none' тавина; theme-init.js nonce-тэй (fb8c0441).
  • eID callback-ийг хөтөчтэй холбох: frontend зөвхөн энэ хөтчийн эхлүүлсэн session_id-г poll хийнэ; backend /auth/eid/start(-id) нь gerege_eid_flow cookie тавьж, /auth/eid/poll COMPLETE үед зөвхөн тэр хөтчид session олгоно (Sec-Fetch-Site илгээдэг хөтчид; native клиентэд өөрчлөлтгүй). Халдагчийн session-оор нэвтрүүлэх login-CSRF хаагдав (fb8c0441, 2e172ae8).
  • Нэг удаагийн токеныг (setup, impersonate, bind, callback) уншмагц history.replaceState-ээр хаягаас арилгана; Sentry request.url ба breadcrumb-ийн query-г тайрна; .dockerignore-д .env*; fast-uri 3.1.7.

Changed — аудитын UX, хүртээмж, гүйцэтгэл (#14, #15, #16)

  • lib/apiError.ts (describeError): 401/403/404/409/429/5xx/сүлжээний алдааг хэрэглэгчийн хэлээр; ~100 газрын err.message солигдов. Session дуусахад л /login?next=<зам>, бусад алдаанд shell хэвээр, дахин оролдох товчтой.
  • RTL: <html lang/dir> серверээс (cookie); physical утилитууд (pl/pr/ml/mr/left/right) logical болов (ps/pe/ms/me/start/end), 44 файл.
  • i18n route-оор: аппын толь (cp, developer, setup, documents, esign, kiosk, reports, sso_clients, urtuu, integrations) тухайн route-ийн layout.tsx-ээс бүртгэгдэнэ (lib/i18n/apps/index.ts); registry хожуу бүртгэл ба locale overlay-г дурын дарааллаар нийлүүлнэ. Landing JS 416 → 255 KB (52a7d687). Дүрмийг Орчуулга-д.
  • Токен: хоёр дахь accent (indigo), slate/red/emerald… палитр → семантик токен; 12px-ээс жижиг бичвэр хасагдав; gradient/blur/өнгөт сүүдэр хасагдав; skip link, combobox ARIA; confirm()/prompt() → dialog; kiosk идэвхгүй байдлын таймер; огноо lib/datetime-аар (Asia/Ulaanbaatar); next.config output: standalone.

Added — eID QR-д session id-тай хамт VC (31a48793)

  • QR нь sessionId=<sid>&vc=<код> (eidrp.QRPayload; vc хоосон бол зөвхөн sid). Апп консент дэлгэцээ скан хийсэн даруй зурж, attested GET /v3/mobile/session-ийн VC-тэй тулгана. Хуучин апп & хүртэлх id-г л уншдаг тул нийцтэй.

Added — цөм шинэчлэгдэхэд distribution-д дохио (12f751c6)

  • .github/workflows/notify-downstream.yml: main-ий CI ногоон болоход repository_dispatch core-updated (payload: sha) илгээнэ; CORE_SYNC_TOKEN байхгүй бол анхааруулаад алгасна. Энэ репо downstream руу юу ч push хийхгүй. Бүх механизмыг Distribution-д.

Fixed — App2App callback 403-оор гацдаг байв; хуучин commit deploy-г дардаг байв (2381fbc3, 468d20a5)

  • Callback хуудас 403, id алга, харь id, хугацаа дуусах гэсэн төгсгөл бүрийн өмнө /auth/me-ээс нэг удаа асууна: хөтөч нэвтэрсэн бол /profile руу, үгүй бол шалтгааныг харуулна; 403-ыг дахин оролдохгүй.
  • deploy.yml: зөвхөн main-ий tip автоматаар гарна — өмнө нь CI хожуу дууссан хуучин commit засварыг production-оос буцаадаг байв.

Changed — eID relying party-ийн session бичлэг DB-д (registry.eid_rp_pending, 00114)

  • kernel/eidrp: илгээснийг (challenge, нэр, interactions, callback, flowType, түвшин) хадгалах нь PendingStore интерфэйс болов — анхдагч санах ой, платформ workspace/identity/eid.StorePendingIn(db)-ээр Postgres. Өмнө нь процессын санах ойд байсан тул deploy-ийн restart эсвэл хоёр дахь instance дээр poll буухад enforce горимд тэр нэвтрэлт 502 авдаг байв.
  • Хүснэгт ямар ч ролийн grant-гүй; tenant (профайлд eID холбох) ба operator (консолын eID) ролиуд registry.eid_rp_pending_put/get SECURITY DEFINER функцээр л хүрнэ. Хугацаа дууссаныг binding sweep цэвэрлэнэ.
  • Миграцын дугаар 00113-ыг зориуд алгасав — eid-mongolia-mn distribution-ий CA консолын залгаас тэр дугаартай; numbering_test.go бүртгэж, өөр файл тэр дугаарыг авбал барина.

Fixed — App2App-аар буцаж ирэхэд «Нэвтрэх хүсэлт олдсонгүй» гардаг байв

  • /auth/eid/callback нь session id-г хаягаас уншиж даруй арилгадаг (нэг удаагийн утга). Effect нь t-ээс хамаардаг байсан бөгөөд t нь толь бичиг ачаалагдах бүрд (нэмэлт хэл, route-ийн өөрийн үгс — registryVersion) шинээр үүсдэг тул effect хоёр дахь удаа ажиллаж, цэвэрлэгдсэн хаягаас id олдохгүй → утсан дээрээ зөвшөөрсөн иргэнд «Нэвтрэх хүсэлт олдсонгүй». Одоо id-г mount дээр нэг удаа useState-д уншиж, алдааг түлхүүрээр хадгалж render үед орчуулна — effect-д i18n огт хэрэггүй. Тест tests/eid-callback.test.tsx.
  • kernel/eidrp: session бүрд илгээснийг (challenge г.м.) хадгалсан бичлэг эхний COMPLETE poll-д устдаг байсныг болиулав — нэвтрэх tab ба App2App callback хуудас хоёулаа нэг session-ийг poll хийдэг тул хоёр дахь нь «no record» → 502 → spinner дээр зогсдог байв. Бичлэг зөвхөн 15 минутын насаар арилна.

Added — eID-ийн зөвшөөрөгдсөн session-ийг криптографаар баталгаажуулах (EID_VERIFY_RESPONSE)

  • Өмнө нь kernel/eidrp нь CA-ийн endResult=="OK" + person блокт шууд итгэж session үүсгэдэг байв (тухайн үеийн core/nexus хуваалтын тэмдэглэлд байсан цоорхой; одоогийн гэрээ Distribution-д). Одоо CA-ийн SDK гэрээний (sdk/README.md ResponseValidator) дагуу: гэрчилгээ үндэсний root CA руу гинжлэгдэх (clientAuth), certificateLevel ≥ хүссэн түвшин, утасны түлхүүр ACSP_V2 payload-д (энэ платформын өөрийн rpChallenge, нэр, interactions, callback + хариуны serverRandom/userChallenge/flowType) гарын үсэг зурсан эсэх (rsassa-pss / PKCS#1 v1.5 / ecdsa-with-SHA256), person блокийн civilId нь гэрчилгээний SERIALNUMBER=PNOMN-…-тэй таарах эсэх. Identity.Verified тугтай.
  • Payload builder (kernel/eidrp/acsp.go) нь CA-ийн server/internal/crypto/acsp.go-ийн байт-ижил хуулбар, Smart-ID баримтын вектороор тесттэй. Initiate одоо interactions-ийг v3 base64 мөрөөр илгээнэ (CA тэр мөрийг яг хэвээр hash-лэдэг тул).
  • Trust store kernel/eidrp/trust/ — CA репогийн pki/chain/-аас embed: үндэсний root (anchor), MDDC/Gerege/eID Mongolia issuing CA-ууд (intermediate), 2026-07-28-ны DPoD түлхүүрүүд, үндэсний гинжээс өмнөх self-signed issuing CA-ууд (anchor — тэр үеийн иргэд нэвтэрсээр байхын тулд). EID_ROOT_CA_PEM/_FILE нэмэлт anchor.
  • EID_VERIFY_RESPONSE=enforce|warn|off, анхдагч warn: амжилтгүй баталгаажуулалтыг шалтгаантай нь логлоод зөвшөөрнө — бодит зөвшөөрлүүд embed гинжтэй таарч буйг лог дээр харсны дараа enforce болгоно. off prod-д анхааруулга. Клиент нэг session-д илгээснээ (challenge г.м.) процессын санах ойд 15 мин хадгална — өөр instance дээр эхэлсэн session-ийг enforce татгалзана, warn логлоно. (e0f0a3cd-ээс хойш Postgres-д — «eID relying party-ийн session бичлэг DB-д» хэсгийг үз.)

Fixed — /dev tenant-ийн бүрхүүл дотор давхар зурагддаг байв

  • lib/publicRoutes.ts: /dev нь /cp-ийн адил chromeless — хөгжүүлэгчийн сайт өөрийн бүрхүүлтэй (components/dev/DeveloperConsole) бөгөөд session-оо дотроо уншдаг. Өмнө нь tenant-ийн Layout түүнийг бас нэг rail/panel/topbar-аар ороож, нэг дэлгэц дээр хоёр shell (хоёр хайлт, хоёр аватар цэс) гардаг байв. Тест tests/public-routes.test.ts.
  • lib/i18n/addons/developer.ts: «Sign in with Gerege» гэж хатуу бичсэн гурван мөр {brand} болов — eidmongolia.mn дээр «Sign in with e-ID Mongolia».

Fixed — /dev нэвтрээгүй зочныг байхгүй SSO provider руу явуулдаг байв

  • Хөгжүүлэгчийн сайтын бүрхүүл (frontend/components/dev/DeveloperConsole.tsx) session-гүй зочныг шууд /api/v1/auth/sso/start руу явуулдаг байсан — энэ нь developer.gerege.mn-ий загвар (платформ нь өөрийн OIDC provider-ийн клиент). Өөрөө таних суулгац (eidmongolia.mn) дээр тэр endpoint 404 буцаадаг тул зочин {"error":"this deployment is not a client of an SSO provider"} гэсэн JSON дээр зогсдог байв. Одоо нэвтрэх дэлгэцийн адил /auth/sso/config-оос асууж: provider-тэй бол түүний start_url, үгүй бол (эсвэл тохиргоо ирэхгүй бол) энэ хостын өөрийн /login?next=… руу явуулна. Тест frontend/tests/dev-console-sign-in.test.tsx.

Added — eID провайдер сонгох: Gerege ID ба eID Mongolia (EID_PROVIDER)

  • EID_PROVIDER=gerege|eidmongolia нэг хувьсагч API-ийн дуудах CA (https://ca.gerege.mn/v3 / https://ca.eidmongolia.mn/v3), нэвтрэх дэлгэц ба мессеж бүрт харагдах нэр, CA-ийн хост, QR-аар нээгдэх аппын scheme (geregesmartid:// / eidmongolia://)-ийг хамтад нь сонгоно. Backend (internal/kernel/config/eid.go) ба frontend (lib/eidProvider.ts) хоёр нэг хувьсагч уншина. Хоосон бол gerege — өмнөх зан хэвээр.
  • EID_BASE_URL тохируулсан бол provider-ийн хаягаас давуу. Compose-ын https://ca.gerege.mn/v3 default хасагдсан — үлдсэн бол provider сонголтыг үргэлж дарах байсан.
  • Орчуулгад {eid}, {eid_ca}, {eid_host} хувьсагч нэмэгдэж, "Gerege ID" гэж хатуу бичсэн мөрүүд (7 хэлээр) тэдгээрт шилжив.
  • Хоёр CA тусдаа бүртгэл: EID_RP_UUID/EID_RP_SECRET сонгосон CA-ийн олгосон утга байх ёстой.

Added — гурван түвшний App Store (console/workspace/person)

App Store гурав болов: консол (/cp/apps), байгууллага (/apps), одоо хүн өөрөө (/me/apps). Каталогийн manifest "surfaces": [...]-ээр аль хэлбэрт нь өөрийгөө санал болгохоо хэлнэ (console/workspace/person, эсвэл хослол); хуучин operator/tenant нэрс alias хэвээрээ ажиллана, хоосон утга урьдын адил workspace гэсэн үг.

  • Шинэ workspace.person_app_installations (миграц 00112), user_id-аар тусгаарлагдсан (tenant_id байхгүй) — хүн байгууллагад орсон ч, ороогүй ч адилхан ажилладаг өөрийн хувийн суулгацууд. /api/v1/person/apps* (жагсаах, суулгах, шинэчлэх, идэвхжүүлэх/идэвхгүй болгох, устгах, түүх) — нэвтэрсэн хэрэглэгч бүрт нээлттэй, RequireAdmin шаардахгүй: энэ бол тухайн хүний өөрийнх нь шийдвэр.
  • Байгууллагын App Store одоо зөвхөн workspace гадаргуутай аппуудыг л санал болгоно (HasSurface(catalog.SurfaceWorkspace)) — хувийн апп байгууллагын дэлгэцэд гарахгүй, эсрэгээр ч мөн адил.
  • Олон гадаргуутай апп (жишээ нь зэрэг workspace ба person) аль дэлгэцэд цэсэндээ гарахыг RBAC шийднэ, суулгацаар биш: nexus.AccessPolicy.MenuPermission() ба HandleMenus-ийн цэсний эрхийн шүүлтүүр (appReadPermission) — байгууллагын дуудагч эрхтэй л бол харна, хувийн дуудагч ямар ч байгууллагын эрх шаарддаггүй person-аппуудыг л харна.
  • Мөн энэ явцад: GetTenantMenus-д байсан алдаа засав — байгууллагын ажлын мужид built-in «Профайл» ба «Байгууллагын профайл» хоёул зэрэг гардаг байсныг tenantID-аар ялгаж, хувийн дуудагч зөвхөн Профайл, байгууллагын дуудагч зөвхөн Байгууллагын профайл авдаг болгов.

Дэлгэрэнгүй: docs/MODULES.md.

Changed — байгууллагын ажлын муж апп-уудаас бүрдэнэ

  • Байгууллагын шинэ built-in «Байгууллагын профайл» апп: консолын Профайл апп-ын загвараар хянах самбар (/organisation/dashboard, GET /api/v1/tenant/profile/dashboard), байгууллагын мэдээлэл, нийтэлсэн үйлчилгээ, App Store, суулгасан аппууд. «Профайл» апп нь Данс бүлэгтээ «Харагдац»-ыг авлаа.
  • «Хандалт» ба «Төхөөрөмж» нь тусдаа core апп болов (internal/workspace/coreapps): App Store-д гарна, tenant бүрд анхнаасаа суусан, идэвхгүй болгож/устгаж болно, платформын хувилбартай хамт явна.
  • Бүрхүүлийн хатуу бичсэн «Платформ» хажуугийн цэс ба rail-ийн Платформ tile устав; chrome апп-ын дэлгэцүүд Байгууллагын профайлын цэсэнд нэгдэнэ.

Changed — консол апп-уудаас бүрдэнэ, App Store карт болов

Операторын консолын App Store одоо байгууллагынхтай ижил дизайнтай: карт, хайлт, шүүлт, түүх. Платформ, Байгууллагууд, Ops нь суулгах боломжтой цөмийн апп болов (миграц 00110 тэднийг суусан гэж seed хийнэ); зөвхөн Профайл нь built-in — авч хаяшгүй, суулгах товчгүй. Профайл апп одоо самбартай (/cp/profile, GET /me/activity) ба дахин зохион бүтээсэн дансны дэлгэцтэй. Байгууллагатай ч, хувийн ч бай — workspace бүр built-in Профайл tile-тай ирнэ.

Changed — элсэх хүсэлтийг менежер ч шийднэ; SSO-оор ирсэн хүн өөрийн гэртэй

/admin/access/join-requests хоёулаа RequireAdmin-ы ард байсан. Одоо join_requests.manage эрхийн ард (шинэ RequirePermission middleware): менежерт default-оор байдаг %.manage тул шинэ байгууллагад өөрөө очно, одоо байгаа байгууллагуудад миграц 00109 admin ба manager-т өгөв. Shell дээр менежер «Хандалт» дэлгэцээс зөвхөн хүсэлтийн табыг харна.

SSO client горимд SSO_CLIENT_TENANT хоосон байхад баталгаажсан танихгүй хүн татгалзагддаг байсан. Одоо платформ public бол eID-ийн 00085-тай адил өөрийн home workspace-тэй үүсч, /me-ээс байгууллагад элсэх хүсэлт илгээнэ. Private хэвээр бол урьдын адил татгалзана. Эхний хэрэглэгч: App Store — Gerege Nexus-ээр нэвтэрч, gerege-д элсэх хүсэлт өгч, Gerege Systems-ийн админ, менежер зөвшөөрнө.

Added — операторын консол апп-уудаас бүрдэнэ

Нэг store, хоёр гадаргуу. Manifest surfaces: ["operator"] зарласан модуль (nexus.ConsoleApp) консолын App Store-оос (/cp/apps) платформд суугаад rail-д tile болно (operator.console_apps, миграц 00108; /api/platform/v1/apps*). Цөмийн дэлгэцүүд built-in апп болов: Профайл (шинэ — данс, нууц үг, eID), Платформ, Байгууллагууд, Ops. Console.tsx-д хатуу tile үлдсэнгүй. Аппаар дамжсан бичих хүсэлт бүрийг gate operator.app.write action-аар аудитлана, X-Reason header нь шалтгаан болно.

Removed — Gerege CA-ийн админ консол цөмөөс гарав

/cp/ca/* (app/cp/ca/, components/cp/ca.tsx, lib/ca.ts) ба backend/internal/operator/ca/ (/api/platform/v1/ca/* дамжуулагч) нь зөвхөн eID Gerege түгээлтийнх тул цөмд биш, eid-gerege-nexus-ийн core/ дотор амьдарна. Доорх «Added» бичлэг тэр өдрөө юу орсныг хэлсээр байна.

Added — операторын консолд Gerege ID (eID) холболт ба QR нэвтрэлт

Оператор нэвтэрсэн консолоосоо eID-ээ холбож/салгаж, дараа нь QR уншуулаад нэвтэрнэ (TOTP давхар асуухгүй). Миграц 00107, /session/eid*, /me/eid. eID-ийн хаяг бүр ca.gerege.mn (eID Gerege); UI-д «Gerege ID».

Added — Gerege CA-ийн админ консол операторын консол дотор

  • /cp/ca/* (app/cp/ca/, хүрээ components/cp/ca.tsx, клиент lib/ca.ts): CA-ийн хуучин Next.js админ консолын 16 дэлгэц — иргэд, төхөөрөмж, сертификат, PKI, байгууллага, байгууллагын CA (X-Road CSR), RP, RP-ийн өргөдөл, 4-eyes зөвшөөрөл, гадаад бүртгэл, audit (CSV), админ, систем, профайл — консолын өөрийн дизайнаар. Rail дээр «Gerege CA» tile.
  • Backend internal/operator/ca: /api/platform/v1/ca/{session,session/mfa,api/*} — CA-ийн /v3/admin/* руу дамжуулагч (CA_ADMIN_URL, хоосон бол EID_BASE_URL). CA-ийн session токен cp_ca_session httpOnly cookie-д (зам /api/platform/v1/ca); path allowlist; CA-ийн 5xx нь хөтөчид харагдахгүй; CA 401 → ca_session_required; бичилт бүр операторын audit-д (ca.session.begin/confirm/end, ca.write + X-Reason).
  • lib/cp.ts: RequestFailed (status + code) — дэлгэц кодоор нь ялгах боломжтой.

Added — ажиглалт нь OpenTelemetry дээр, өөрийн домэйнтэй

Хэмжүүрүүд client_golang дээр гараар бичигдсэн, нэр нь өөрсдийн зохиосон байсан. Одоо OpenTelemetry-ийн metrics SDK-аар дамжиж, Prometheus exporter-ээр хуучин /metrics дээрээ гарна. HTTP-ийн хоёр хэмжүүр semantic convention руу шилжив — http_requests_total ба http_request_duration_seconds нь http_server_request_duration_seconds болж нэгдсэн, хүсэлтийн тоо нь тэр гистограммын _count цуврал. Repo доторх бүх alert дүрэм, dashboard панел, консолын query хамт шилжсэн.

Гурван зүйл дайвар байдлаар зассан. net/http ямар ч token-ыг method болгож хүлээж авдаг тул хэн ч зохиомол verb илгээгээд хязгааргүй тооны цуврал үүсгэж чадах байсан — танихгүй method одоо _OTHER. resilience_in_flight_requests нь Set()-ээр бичигддэг байсныг UpDownCounter болгов: зэрэг дуусах хоёр хүсэлт агшны зургаа дурын дарааллаар бичиж, гарсан тоо хуучирдаг байв. Exemplar нь хоёр талаасаа тасарсан байсныг залгав — латенси графикийн удаан цэг дээр дарахад тэр хүсэлт ямар SQL хүлээснийг харуулна.

monitor.nexus.gerege.mn — Grafana, Alertmanager. Grafana нь платформын өөрийн OIDC-ээр нэвтэрнэ: roles scope дээр platform_admin claim гарах ба тэр нь эхний байгууллагын админд — тохиргооны шидтэн үүсгэсэн байгууллагын админд — л үнэн. Claim нь хүний тухай, токен аль workspace-д зориулагдсанаас хамаарахгүй.

Долоон dashboard: API тойм, Гадаад системүүд, Инфраструктур, Тэсвэрлэлт, Логууд, Аюулгүй байдал, Мониторингийн эрүүл мэнд. Самбар бүр лог руу орох цэстэй.

Trace асав. Tempo хоёр долоо хоног ажиллаж юу ч хүлээж аваагүй байсан — кодод бүх зүйл бэлэн байсан, .env рүү хувьсагч хүргэх зам байгаагүй.

Added — нөөцлөлт, эцэст нь

Энэ платформ дээр нөөцлөлт огт байгаагүй: cron суугаагүй, скрипт хостод байхгүй, platform_backups хүснэгт хоосон. Консолын Нөөцлөлт дэлгэц хоосон байсныг хэн ч анзаараагүй — хоосон дэлгэц нь «юу ч буруудаагүй»-тэй яг адилхан харагддаг.

backup.sh одоо үр дүнгээ хоёр газар бичнэ: platform_backups (консол уншина) ба node_exporter-ийн textfile (Prometheus уншина). Дөрвөн дохио, тэр дундаа NexusBackupNeverSeen — «нөөцлөлт унасан» биш, «нөөцлөлт байгаа эсэхийг хэн ч хэмжихгүй байна».

backups.nexus.gerege.mn — S3-той нийцэх сан. Dump нь хостыг орхихоосоо өмнө age-ээр шифрлэгдэнэ; эх хостод зөвхөн нийтийн түлхүүр байдаг тул эвдэрсэн платформ өөрийн илгээсэн зүйлээ уншиж чадахгүй. Bucket нь хувилбартай ба суулгацын түлхүүр устгах эрхгүй: гараас нь атгасан хост нэмж чадна, арилгаж чадахгүй. Сэргээлтийг хаях зориулалттай санд ажиллуулж баталгаажуулсан.

Added — баримт бичгийн сайт MkDocs дээр

docs.nexus.gerege.mn — MkDocs + Material for MkDocs, docs.gerege.mn-тэй яг ижил хэрэгсэл, ижил брэнд. Хуудасны жагсаалт нь docs/site/pages.mjs-ээс уншигдана: хоёр жагсаалт байвал салж, салсан нь нь хэн ч харахгүй байгаа нь болно.

Changed — эхний бүртгэл нь хүн биш

Тохиргооны шидтэн Gerege Core-оос хүнийг хайж, түүний нэрийг эхний бүртгэлд тавьдаг байсан. Эхний бүртгэл бол хүн биш — байгууллага үүсгэж, ажиллах хүмүүсээ урих хаалга. Нэр нь одоо тогтмол Super Admin, хүний хайлт шидтэнээс хасагдав. И-мэйл хэвээр: нууц үг сэргээх мессеж хэн нэгэнд хүрэх ёстой.

Fixed — Authenticator дээрх нэр брэндээ дагана

Нэг хост дээр ажиллаж буй бүх бүтээгдэхүүний консол утсан дээр яг ижил бичлэг үүсгэдэг байв — issuer кодод бэхлэгдсэн байсан. Одоо config.BrandName()-ыг дагана. Мөн url.Values.Encode хоосон зайг + гэж бичдэг тул утсан дээр Gerege+Nexus+Control+Plane гэж гардаг байсныг %20 болгов.

Fixed — засварын горим хүнийг байгууллага дотор нь хоръё гэж байв

Байгууллагыг засварын горимд оруулахад тэр байгууллагын гишүүн бүр — түүнийг асаасан админ ч мөн адил — өөр байгууллага руугаа шилжиж чадахгүй болдог байв. Шилжих нь POST, зөвхөн-унших хаалт бүх POST-ыг татгалздаг; гарах цорын ганц зам нь бүрмөсөн гарах байлаа.

Хаалтын өөрийнх нь тайлбар "гарах гэсэн хүн үргэлж гарч чаддаг байх ёстой, хүнийг хорьдог засварын горимыг хэн ч дахин асаахгүй" гэж бичсэн байсан ба гарахыг чөлөөлсөн — шилжихийг мартсан. Одоо хоёулаа чөлөөтэй: шилжилт нь засварлагдаж буй байгууллагын дотор юу ч бичдэггүй, зөвхөн session-ы мөрийг өөрчилдөг.

Мөн байгууллага солих цонх алдааны шалтгааныг харуулдаг боллоо: өмнө нь юу болсноос үл хамааран «дахин оролдоно уу» гэдэг байсан нь засварын горим ба гишүүнчлэлээ алдсан хоёрын аль алинд буруу зөвлөгөө.

Fixed — Google-ээр анх нэвтрэх нь нэвтрэх дэлгэц рүүгээ буцдаг байв

Google-ээр анх удаа нэвтрэхэд backend бүх зүйлээ зөв хийж байсан: хаягийг баталгаажуулж, домэйныг шалгаж, таних мэдээллийг парклаад /login/bind руу шилжүүлдэг. Гэтэл ажлын мужийн бүрхүүл тэр хаягийг нэвтэрсэн хүний дэлгэц гэж үздэг байв — PUBLIC_ROUTES нь яг таарах шалгалттай бөгөөд /login/bind түүнд ороогүй. Бүрхүүл /api/v1/me асууж, session байхгүй хүнээс 401 аваад /login руу буцаан түлхэнэ.

Гаднаас нь харахад: Google дарахад юу ч болохгүй. Алдаа ч алга — юу ч уначхаагүй, зүгээр л хүргэх гэсэн дэлгэц нь зурагдаагүй. eID-ээр нэвтэрсэн хүн профайлаас Google холбоход ажилладаг байсан нь (session байгаа тул бүрхүүл түлхэхгүй) энэ асуудлыг "Google login ажиллахгүй байна" биш "eID хийсний дараа л ажилладаг" мэт харагдуулж байв.

Ижил шалтгаанаар хоёр зам мөн эвдэрсэн байсан: урилга ба нууц үг сэргээх (/login/set-password — файл нь өөрөө "public route by necessity" гэж бичсэн), ба консолын impersonate handover (/impersonate — session нь тэр хуудсан дээр л үүсдэг).

Дүрэм нь одоо lib/publicRoutes.ts-д, нэвтрэх муж нь угтвараар: /login/** бүхэлдээ нээлттэй. Тодорхойлолтоороо зөв — тэнд байгаа дэлгэц бүр нэвтрээгүй хүнд зориулагдсан — бөгөөд ирээдүйд нэмэгдэх дэлгэц ижил алдаанд дахин орохгүй. /impersonate жагсаалтад нэмэгдэв.

Fixed — "бүртгэл алга" гэж хэлдэг байсан нь "дахин оролдоно уу" болов

binding_failed нь no_account нэрээр явж байсан. Хоёр нь эсрэг зөвлөгөө: нэг нь админ ажиллах ёстой гэнэ, нөгөө нь дахин оролдохыг хэлнэ. Бүртгэл нь байхгүй нь зөв — тэр яг одоо үүсэх гэж байсан бөгөөд зөвхөн баталгаажуулалтын дэлгэц рүү хүрч чадаагүй.

Added — frontend-ийн тестийг бодит болгов

Хорин долоон мянган мөр frontend дээр хоёр зуун есөн мөр тест байв — бүгд нь цэвэр функц ба эх кодын инвариант, өөрөөр хэлбэл компонентын дотор амьдардаг дүрэм бүр шалгагдахгүй: Node нь TSX-ийг хөрвүүлж чадахгүй тул тэдгээрийг шалгах арга байгаагүй. Дээрх нэвтрэлтийн алдаа хоёр долоо хоног амьдарсан нь яг үүний үр дагавар.

node --test → vitest + Testing Library (jsdom). Байсан файлууд хэвээрээ, импортоо л сольсон. Нийт 144 тест: консолын арван зургаан дэлгэц рендерлэгдэж, эрхийн ялгаа (auditor бүгдийг уншаад юу ч дарж чадахгүй, operator байгууллага түдгэлзүүлж чадах ч устгал хүсэж чадахгүй, support дотор нь харж чадах ч амьдралын мөчлөгт хүрэхгүй), өөрийгөө идэвхгүй болгож чадахгүй, нэг удаагийн handover яг нэг удаа, step-up код асуугаад тасалсан үйлдлээ үргэлжлүүлэх — бүгд шалгагдана.

Гурван инвариант тест эх кодыг өөрийг нь уншина: Go-гийн capabilities хүснэгт ба консолын хуулбар зөрвөл унана; серверийн илгээдэг sso_error шалтгаан бүр эсвэл өгүүлбэртэй, эсвэл "зориуд ерөнхий" гэж нэрлэгдсэн байх ёстой; бүрхүүлийн нээлттэй замын дүрэм нь дээрх дөрвөн дэлгэцийг session-гүй зурдаг байх ёстой.

Мониторингийн дэлгэцүүдээс шалгагдаж буй гол дүрэм: тоо байхгүй нь тэг биш. Prometheus-гүй суулгацад гурван хэмжүүр зураас харуулна — 0.00% гэж бичих нь эрүүл суулгацтай яг адилхан харагддаг.

E2E — Playwright. jsdom-оор асууж боломгүй хоёр дүрэм: консол өөрийн хостоороо л үйлчилнэ (/login → 404), ба /cp бүрхүүл нь layout тул маршрутууд дамжин амьд үлдэнэ — гурван дэлгэц дамжсаны дараа ч cp.me() нэг л удаа дуудагдсан байхыг тоолж шалгана. API нь хөтөч дотор stub хийгддэг тул backend ч, өгөгдлийн сан ч хэрэггүй.

CI-д npm test (vitest) хэвээр, build-ын дараа npx playwright test нэмэгдэв; унасан ажиллагааны trace артефакт болж үлдэнэ. Docker болон CI-д PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 — npm ci гурван хөтөч татахгүй.

Fixed — консолын зөвхөн дүрстэй товчнууд нэргүй байв

/cp/config-ийн флаг унтраах/асаах, флаг устгах, credential цэвэрлэх, тохиргооны түүх — дөрвүүлээ дүрс эсвэл ✕ агуулсан, aria-label-гүй товч байв. Дэлгэц уншигчид "button" гэж л дуудна, тестээс нэрээр нь олдохгүй. Дөрвүүлд нь одоо байгаа орчуулгын түлхүүрээс aria-label ба title өглөө.

Changed — нийтэлсэн үйлчилгээ өөрийн дэлгэцтэй боллоо

Байгууллагын хуудасны хамгийн доод хэсэгт наасан карт байсныг /organisation/services болгож, хажуугийн цэсэнд байгууллагынхаа доор оруулав. Хүн олж харах ёстой зүйл доод талын карт болж наагдсан бол хэн ч түүнийг шийдээгүй гэсэн үг: үйлчилгээ нийтлэх нь гадагш өгсөн амлалт — танихгүй хүн лавлахаас олж хандана — тул дээрхтэйгээ адил зэрэглэлтэй байх ёстой.

Fixed — «Дотор нь орж харах» нь хэнийг гэдгийг өөрөө сонгодог байв

Байгууллагын хуудасны үйлдлийн мөрөн дэх тэр товч members[0] —жагсаалт юугаар эхэлж таарсан тэр хүн— рүү ордог байсан. Оператор өөр хүний нэрийг шалтгаандаа бичээд огт өөр хүн болж ордог боломж; impersonation-ы бүх утга нь хэн болох дээр байтал.

Одоо тэр товч гишүүн бүрийн мөрөнд байна: хэнийг гэдгийг сонгож, шалтгаанаа бичнэ. Үйлдлийн мөрөөс хасав.

Fixed — хэмжигдээгүй систем ногоон гэж харагддаг байв

Гадаад системүүдийн самбар зургаан системийг «0.00% алдаа, ногоон» гэж харуулдаг байсан нь тэдгээрийг сайн ажиллаж байна гэсэн үг биш: Prometheus-д тэдгээрийн цуваа огт байхгүй байсан. instantQuery нь хоосон хариултыг тэг гэж уншдаг — тоолуурын хувьд зөв (хэн ч нэвтрээгүй бол нэвтрэлт үнэхээр тэг), гадаад системийн хувьд буруу: «алдааны хувь алга» гэдэг нь «алдаагүй» гэсэн үг биш.

Одоо instantSample нь дээж байсан эсэхийг хэлнэ. Дээжгүй систем ба хэмжүүр unknown төлөвтэй, measured: false-тай ирж, дэлгэц дээр «хэмжигдээгүй» гэж саарлаар гарна — жагсаалтаас алга болохгүй: eID хэмжигдээгүй байгаа нь eID байхгүй гэсэн үг биш, ажиллаж байгаа гэсэн үгээс бол огт өөр.

Мөн /cp/ops дэлгэц backend-ийн төлөвийг ok/warn гэж уншиж байсныг зассан — backend green/amber/red гэж хэлдэг тул мөр бүр улаанаар будагдаж, дотор нь "green" гэсэн үг англиар бичигдэж байлаа.

Fixed — админ болгох нь нэг товшилт байв

Хандалтын дэлгэцийн гишүүн бүрийн хажууд эрхийн чипүүд зэрэгцэн сууж, тэдгээрийн хамгийн хүчтэй нь бусадтайгаа адил амархан дарагддаг байв. nexus.gerege.mn дээр хоёр хүн — хүсэлт нь зөвшөөрөгдсөнөөс хойш хорин секундын дараа — дөнгөж орсон байгууллагынхаа админ болов; дэлгэц тэгэх гэсэн үү гэж асуугаагүй.

Одоо admin эрх өгөхийн өмнө хэнийг, юу болгож байгааг нэрлэсэн асуулт гарна. Бусад эрх өмнөх шигээ нэг товшилт; админыг хасах нь сүүлчийнх бол backend өмнөхөөрөө татгалзана.

Зөвшөөрлийн зам өөрөө зөв байсан — миграц 00008-ийн trigger шинэ гишүүнчлэлд зөвхөн user эрх өгдөг — гэхдээ энэ нь харагдахгүй тул тестээр бэхлэв: TestAnApprovedRequestGrantsOnlyTheSmallestRole.

Changed — оператор нэмэхэд нэрийг нь бүртгэлээс

Байгууллага нээхэд аль хэдийн хийсэн зүйлийг операторын цонхонд ч: регистрийн дугаараар Gerege Core-оос хайж (GET /api/platform/v1/directory/person) нэр, байвал и-мэйлийг нь татна. Бүртгэл нэрийг хэрхэн бичдэгээр нь бичнэ; гараар бичсэн нэр бол хэн нэгний галиглал.

Хайлт заавал биш: бүртгэлд байхгүй эсвэл и-мэйлгүй хүнийг өмнөх шигээ гараар бөглөнө.

Added — консол дээр «Хэрэглэгч» бүлэг: платформын бүх хүн

Дэмжлэгийн дэлгэц гурван тэмдэгт бичихийг шаардаж, нэг хүнийг олдог. Тэр нь "хэн нэг нь залгасан" гэдэгт зөв хэлбэр, харин хүн амын тухай асуулт бүрд буруу: хэдэн бүртгэл байна, хэд нь үнэхээр нэвтэрч чадах вэ, хэн нь ямар ч байгууллагад харьяалагдахгүй байна вэ. Эдгээрийг бүхэлд нь асуухаас өөр аргагүй.

/cp/people — бүх бүртгэл: дөрвөн тоо (нийт, eID-ээр баталгаажсан, нээлттэй session-той, байгууллагагүй), хайлт, шүүлтүүр, хуудаслалт. Тоонууд нь хуудасных биш, хүн амынх: хуудас гүйлгэхэд өөрчлөгддөг "баталгаажсан" тоо байхгүй тооноос дор.

/cp/people/[id] — нэг хүний бүх зүйл: нэвтрэх аргууд (eID ба холбогдсон провайдерууд, тус бүр хэзээ холбогдсон, сүүлд хэзээ харагдсан), харьяалагдах байгууллагууд эрхийнх нь хамт, яг одоо нээлттэй session-ууд, ба түүний нэрээр хэн, хэзээ, ямар шалтгаанаар орсон түүх.

Зөвхөн унших. Түгжээ тайлах, session таслах, буцаж орох холбоос илгээх нь дэмжлэгийн дэлгэцэд хэвээр — тэнд нэг хүнд, нэг шалтгаантай, нэг үйлдэл болж хийгддэг. Хоёулаа support.act эрхийн ард: §2.2-ын шугам хэвээр, оператор эрх хүмүүсийн мэдээлэл харахгүй.

Миграц 00100 нь консолын role-д registry.user_sso_identities дээр зөвхөн SELECT өгнө (00099 нь eID-ийнхийг өгсөн).

Added — байгууллагад админаас өөр хүн нэмэх

Байгууллагын хуудсан дээрх «Хүмүүс» хэсэгт Хүн нэмэх. Эхний админтай ижил жагсаалтаас — eID-ээр нэвтэрч баталгаажсан хүмүүсээс — сонгоно, шалтгаан ба хоёр дахь хүчин зүйл шаардана, audit-д мөр үлдэнэ.

Тэр хүн платформын хамгийн бага эрхтэйгээр («Хэрэглэгч») орно, тэр эрхийг энэ дэлгэц биш, схем өөрөө өгдөг: миграц 00008-ийн trigger нь шинэ гишүүнчлэл бүрт user эрхийг олгодог. Консол эрхээ сонгодог байсан бол схемийн аль хэдийн хариулсан асуултад хоёр дахь хариулт болох байсан бөгөөд тэр хоёр зөрөх өдөр консолынх нь чимээгүй ялах байв. Түүнээс дээшхи эрхийг байгууллагын өөрийнх нь админ, өөрийнхөө хандалтын дэлгэцээс өгнө.

Хатуу хэрэгжилттэй хэрэглэгчийн хязгаарт хүрсэн байгууллагад нэмэхээс татгалзана: консол хязгаарыг нэг дэлгэцэд харуулаад нөгөө дэлгэцээрээ түүнийг тойрч гарах арга байж болохгүй. Зөөлөн хэрэгжилт өмнөх шигээ анхааруулна.

Changed — байгууллага нээхэд дэлгэрэнгүйг нь бүртгэлээс, админыг нь eID-ээс

Хоёулаа өмнө нь бичигддэг байв. Формоос бичсэн нэр бол бараг зөв нэр — дутуу ХХК, өөр галиг — тэр "бараг" нь нэг компанийн хоёр бичлэг болдог. Бичсэн и-мэйл хаяг бол тэр хаяг руу явсан урилга л болохоос, тэр хүн мөн эсэх нь батлагдаагүй.

Одоо: регистрийн дугаараар Gerege Core бүртгэлээс хайж, нэр, албан ёсны нэр, богино нэрийг татна (GET /api/platform/v1/directory/organisation). Эхний админыг eID-ээр нэвтэрч баталгаажсан хүмүүсийн жагсаалтаас сонгоно (GET /api/platform/v1/directory/people) — нэр, и-мэйл, регистр, аль хэдийн хэдэн байгууллагад байгаа, хамгийн сүүлд хэзээ орсныг харуулна.

Сонгосон хүнд урилга илгээхээ болив: тэр хүнд бүртгэл ба орох арга нь аль хэдийн байгаа тул урилга бол эхнийхийнх нь өмнө зогсох хоёр дахь хаалга. admin_user_id өгөгдөөгүй үед хуучин урилгын зам хэвээр.

eID-ээр баталгаажаагүй хүний id өгвөл татгалзана — сонгох гэдгийн учир нь яг тэр. Миграц 00099 нь консолын role-д registry.user_eid_identities дээр зөвхөн SELECT өгнө: иргэн ба бүртгэлийн холбоосыг нэвтрэлт нь бичдэг, консол хэзээ ч биш.

Added — консол дээр Tenant удирдлага апп

Rail-ын гурав дахь хавтас. «Байгууллага» бүлэг бүтнээрээ консолын аппаас энэ рүү нүүв — Байгууллагууд, Дэмжлэг, Хүлээгдэж буй зөвшөөрөл — учир нь оператор өдрөө тэнд өнгөрөөдөг, консолын бусад бүлгүүд нь очиж үздэг зүйл.

Хоёр шинэ дэлгэц, хоёулаа нэг байгууллагын хуудас хариулж чаддаггүй асуултад:

  • /cp/quotas — аль байгууллагад ямар хязгаар тавигдсан, хэдэн хүнтэй нь зэрэгцүүлээд, хэрэгжилт нь зөөлөн үү хатуу юу. Хязгааргүй байгууллагууд дээр нь тоологдоно; хязгаар тавих нь байгууллагынхаа хуудсанд, шалтгаантай хэвээр.
  • /cp/installations — аль апп аль байгууллагад, ямар хувилбартай. Каталогийн дэлгэц "гурав нь 1.2.0, нэг нь 1.1.0" гэж тоолдог; энэ нь тэр нэг нь хэн болохыг хэлнэ, мөн аппыг хасах санал гарсан өглөө "хэн үүнийг ашиглаж байна вэ"-д хариулна.

Fixed — операторын тест бусад тестийн бүртгэлийг унтраадаг байв

TestTheLastSuperadminIsKept нь сүүлийн ерөнхий админы хамгаалалтыг батлахын тулд бусад бүх ерөнхий админыг үнэхээр идэвхгүй болгодог байсан — тэр сан нь бүх багцын тесттэй хуваалцсан тул тэдний бүртгэлийг ч хамт унтраана. Одоо буцаагддаг транзакц дотор хийгдэж, гадаад ертөнцөд хүрэхээ болив.

Fixed — дахин үүсгэсэн өгөгдлийн сан эрхээ буруу role дээр авдаг байв

Role нь кластерынх, өгөгдлийн сан нь тийм биш. 00079 нь gerege_nexus_app → gerege_nexus_tenant нэршилтийг зөвхөн tenant role байхгүй үед хийдэг тул нэг кластерт хоёр дахь сан үүсгэхэд 00079-өөс өмнөх бүх grant ба RLS policy хуучин role дээр буудаг; платформ нь gerege_nexus_tenant-аар SET ROLE хийдэг. Нэвтрэлт болно, дараагийн дэлгэц бүр 500: permission denied for table users.

Онолын зүйл биш: nexus.gerege.mn-ийг 2026-08-29-нд шинэ суулгацын урсгал турших зорилгоор хоосноос босгоход яг ийм болов — хуучин role дээр 220 эрх, шинэ дээр 11.

Миграц 00098 нь хуучин role-ийн барьж байгаа бүхнийг (хүснэгт, схем, sequence, RLS policy, default privilege) шинэ рүү зөөж, дараа нь хуучин role-ыг кластераас гаргана — тэгснээр дараагийн сан "эхнийх" болно. Мөн:

  • search_path дахин баталгаажуулав. 00084 үүнийг өгөгдлийн санд тавьдаг ч pg_dump нь ALTER DATABASE-ийн тохиргоог дампдаа авчирдаггүй; дампаас сэргээсэн суулгац бүх хүснэгттэй, search_path-гүй үлдэж, схем нэрлээгүй асуулга (хүнд гэр үүсгэх зам дахь roles) relation "roles" does not exist гэж унадаг байв.
  • Консолын хувилбарын самбарт grant өглөө. public.goose_db_version дээр operator role-д эрх огт байгаагүй тул нүүр хуудас бүр permission denied for table goose_db_version гэж чимээгүй анхааруулж, схемийн дугаарын оронд хоосон харуулж байв.

Гурван guardrail тест нэмэв: tenant role нь бүрхүүлийн асуудаг хүснэгтүүдийг уншиж чадах, өгөгдлийн сан search_path-аа барьж байгаа, консол миграцын хувилбарыг уншиж чадах.

Fixed — GEREGE_CORE_TOKEN deploy-д огт дамждаггүй байв

Регистрийн дугаараар байгууллага, хүн хайх нь энэ токеноор ажилладаг ба credentials.keys-д, клиент код, шидтэний тайлбар дотор бүгд нэрлэгдсэн байсан ч байрлуулалтын гурван газрын нэгд нь ч байгаагүй: compose нь backend рүү дамжуулдаггүй, deploy нь .env-д бичдэггүй, жишээ файлд ч алга. Гараар .env-д нэмсэн ч compose-оор дамжихгүй, дараагийн deploy-д арчигдана — өөрөөр хэлбэл шидтэн production дээр регистрээр хайх боломжгүй, шалтгаан нь харагдахгүй.

Гурвуулаа зассан: compose-ийн passthrough, deploy-ийн env/envs/.env heredoc, ба deploy/.env.prod.example дахь тайлбартай мөр.

Added — консолоос оператор нэмэх (CP-2)

Хоёр дахь операторт хүрэх цорын ганц зам нь production хост дээрх shell — cmd/operator-bootstrap, өгөгдлийн сангийн эзний эрхээр — байв. Эхний account-д зөв хяналт, дөрөв дэх нь буруу: платформ ба түүнд нэгдэх хүн бүрийн хооронд серверийн shell зогсдог, ийм шалтгаанаар өгсөн shell буцаж цуглуулагддаггүй.

/cp/operators дэлгэц: оператор нэмэх, эрх солих, идэвхгүй болгох/эргүүлэх, ба өөрийн нууц үгээ солих. Нэмэлт бүр ерөнхий админ (operator.write), хоёр дахь хүчин зүйл, шалтгаан шаардана, audit-д мөр үлдээнэ.

Шинэ account нь нэвтэрч чадахгүй төлөвт үүснэ: консол нууц үг ба authenticator-ийн түлхүүрийг нэг удаа харуулж, шинэ оператор кодоо оруулж баталгаажуулах хүртэл totp_confirmed_at хоосон. Хулгайлагдсан консолын session account үүсгэж чадах ч, тэр account нэвтэрч чадахгүй.

Миграц 00097 нь консолын DB role-д INSERT өгнө — 00049 үүнийг "CP-2-д хэрэгтэй болно" гэж тэмдэглэн зориуд түдгэлзүүлсэн байсан. DELETE хэвээр өгөгдөөгүй: явах ёстой операторыг идэвхгүй болгоно, audit-ийн мөрүүд оршсоор буй мөр рүү заасаар байна.

Хамгаалалт: сүүлийн (нэвтэрч чадах) ерөнхий админыг идэвхгүй болгож ч, эрхийг нь бууруулж ч болохгүй; хэн ч өөрийгөө идэвхгүй болгож, эрхээ өөрчилж чадахгүй.

Fixed — консол цэс дарах бүрд бүхэлдээ дахин зурагддаг байв

<Console> хүрээ хуудас бүрийн дотор байсан тул /cp дотор шилжих бүрд бүхэл бүрхүүл дахин mount хийгдэж: ачаалалтын төлөв анивчиж, cp.me() дэлгэц тутамд дуудагдаж, толгой/rail/самбарын төлөв (нээлттэй бүлгүүд ч) тэглэгдэж байв.

Хүрээ нь одоо app/cp/layout.tsx дотор. Layout нь доорх маршрутуудын хооронд шилжихэд амьд үлддэг тул зөвхөн ажлын муж дахин зурагдана — Next-ийн App Router энэ зорилгоор layout-тай. Арван долоон хуудас өөрсдийн агуулгаа л буцаадаг боллоо.

Added — консол дээр System Operations апп

Консолын rail хоёр дахь хавтантай боллоо. Эхнийх нь платформ дээр юу байгааг удирддаг (байгууллага, тохиргоо, аудит); хоёр дахь нь байрлуулалтыг өөрийг нь ажиллуулна — 3 цагт асуудаг гурван асуулт: ажиллаж байна уу, юу үйлдвэрлэгдэж байна вэ, юу хадгалагдаж байна вэ.

Хяналт — /cp/ops (API-ийн гурван тоо, дэд бүтцийн хэмжүүрүүд, гадаад системүүд), /cp/ops/alerts (асаж буй сэрэмжлүүлэг ба тохиргооны анхааруулга — хоёр өөр зүйл, нэг мөчид уншигддаг), /cp/ops/jobs (өөрөө ажиллах ёстой ажлууд ба давтан алддаг байгууллагууд).

Тайлан — /cp/ops/usage (бүх байгууллагын энэ сарын хэрэглээ; тоолуур бүр өөрийн утгаараа: тоологддог нь нийлбэр, хадгалалт нь сүүлийн заалт, идэвхтэй хэрэглэгч нь сарын оргил — өдрийн идэвхтэй хэрэглэгчийг нэмбэл нэг хүнийг гуч удаа тоолно), /cp/ops/schedules (аль хуваарь унасныг нэрлэнэ; нүүр хуудас тоог нь л хэлдэг).

Нөөцлөлт — /cp/ops/backups (нөөц ба сэргээлтийн туршилтын түүх, гараар сэргээлт бүртгэх). Нүүр хуудас сүүлийнхийг л харуулдаг нь "өнөө шөнө болсон уу" гэдэгт хариулдаг; түүх нь "унаж байсан уу" гэдэгт хариулна.

Fixed — консол товлосон тайлангуудыг огт уншиж чаддаггүй байв

observability.backgroundJobs нь workspace.report_schedules-ыг асуудаг ч консолын DB role-д тэр хүснэгт дээр эрх байгаагүй. Алдаа нь warning болж залгигдаж, нүүр хуудсанд эрүүл харагддаг самбар үлдэж байв — яг тэр самбарын барихаар бүтээгдсэн уналт. Миграц 00096 нь SELECT эрх ба operator policy өгнө.

Fixed — хуваалцсан тест сангийн улмаас CI санамсаргүй улаан болдог байв

TestARequestUsesCachedControlDecisionsWhenTheirTablesAreUnavailable нь долоон хүснэгт дээр ACCESS EXCLUSIVE авдаг — тэр нь тестийн санаа. Гэвч go test ./... багцуудыг зэрэг ажиллуулж, бүгд нэг Postgres руу заадаг тул өөр багц registry.tenants-ийн мөр барьж байхад deadlock үүсдэг, Postgres хоёрын нэгийг алдаг — 2026-08-29-нд SQL огт өөрчлөөгүй хоёр салбар дээр яг үүнээс болж CI улаан болов (нэг нь main дээр deploy-г зогсоов).

Түгжээг lock_timeout-той болгож, алдвал транзакцаа дахин эхлүүлнэ (таван оролдлого). Тестийн баталгаа хэвээр: хүснэгтүүд үнэхээр хүрэшгүй болсныг шалгасаар байна.

Changed — AI туслах ба и-мэйл баталгаажуулалт консол руу нүүв

Хоёулаа платформын хөрөнгө байсаар атлаа ажлын муж дотор, байгууллага тус бүрд харагдаж байв. Туслахын нийтлэг prompt ба мэдлэгийн сан нь бүх байгууллагад үйлчилдэг — нэг tenant админ түүнийг засах нь бусад бүх байгууллагын хариултыг засаж байсан гэсэн үг. Баталгаажуулалтын үйлчилгээ нь нэг түлхүүр, нэг провайдертай тул "ажиллаж байна уу", "хэнд бичсэн бэ" гэсэн хоёр асуулт нь платформынх; tenant админ хариултын дөрөвний нэгийг л хардаг байв.

Одоо консол дээр: /cp/assistant (нийтлэг prompt, мэдлэгийн сан — бичилт бүр шалтгаантай, audit-д), /cp/email-verification (бүх байгууллагын бүртгэл, үйлчилгээний төлөв — зөвхөн унших). Ажлын мужаас /settings/ai, /settings/email-verification хуудсууд ба /api/v1/admin/ai/*, /api/v1/admin/email-verification/overview маршрутууд устав.

Миграц 00095 нь консолын DB role-д тухайн хүснэгтүүд дээр эрх ба RLS policy өгнө: prompt/мэдлэгийн хувьд зөвхөн tenant_id IS NULL мөрүүд (тиймээс оператор нэг байгууллагын өөрийн prompt-ыг уншиж ч, бичиж ч чадахгүй), бүртгэлийн хувьд зөвхөн SELECT. Мөн нийтлэг prompt түлхүүр бүрт нэг мөр байхыг баталгаажуулсан partial unique index нэмэв — UNIQUE (tenant_id, prompt_key) нь NULL-ыг ялгаатай гэж үздэг тул хоёр "глобал scope" зэрэгцэн орж болдог байсан.

Changed — толгойн нэр бүтэн, аватар хоёр үсэгтэй

UserMenu-ийн дугуй тэмдэг нэг үсгээс хоёр болов (хоёр үгтэй нэрээс үсэг тус бүр, нэг үгтэй нэрээс эхний хоёр үсэг), нэр нь 9rem дээр таслагдахаа больж бүтнээрээ харагдана. Ажлын муж ба консол хоёулаа ижил цэс хэрэглэдэг тул хоёуланд нь хүрнэ.

Changed — консолын толгойд бүтээгдэхүүний өөрийнх нь бүртгэлийн цэс

"Гарах" нь бар дээр ганцаараа зогсох товч байхаа болиод, ажлын мужийн UserMenu-ийн дотор оров: нэр, и-мэйл, доор нь эрх, дараа нь хэл ба загвар (гэрэл / харанхуй / систем), хамгийн доор нь Гарах. Операторт өмнө нь консол дотроос хэл, өнгөө солих арга огт байгаагүй.

UserMenu нь гурван нэмэлт заавал бус prop авна — showTenants, links, subtitle. Консол нь showTenants={false} (байгууллагын session байхгүй тул жагсаалт ч, түүний API дуудлага ч утгагүй), links={[]} (консолд /profile, /settings хуудас байхгүй), subtitle-ээр эрхээ өгнө. Анхдагч утгууд нь ажлын мужийн өмнөх зан төлөв яг хэвээр.

Changed — операторын консол admin.nexus.gerege.mn дээр нүүв

cp. гэдэг товчлолыг хүн уншиж ойлгодоггүй: консолын хаяг өөрөө юу болохоо хэлэх ёстой. Хост admin.nexus.gerege.mn болов — vhost, гэрчилгээ, CONTROL_PLANE_HOST репо хувьсагч, prod .env гурвуулаа шинэ нэр дээр.

cp.nexus.gerege.mn нь гэрчилгээгээ хадгалж, зөвхөн 301-ээр шинэ нэр рүү заана — хуучин bookmark ажиллах боловч консол тэр хостоос үйлчлэхээ болив. API-ийн HostGate нь хуучин нэрээр ирсэн хүсэлтэд 404 өгнө, өөрчлөлт шаардлагагүй: тэр хаяг ердөө CONTROL_PLANE_HOST-той таарахаа больсон.

Changed — консол ажлын мужийн бүрхүүлийг яг хуулж авав

Толгой хэсэг мөр мөрөөрөө ижил боллоо: брэндийн нүд, контекст, цэсний товч + бүх бүлгийг хумих, session цэг дээр операторын нэр, хайлт, баруун талд эрх ба гарах. Зүүн цэс ажлын муж шиг хоёр хуваалттай — 4rem-ийн апп rail ба 14rem-ийн модулийн самбар, хумигддаг бүлгүүдтэй, гар утсан дээр drawer болдог. Ижил class-ууд тул design-ий өөрчлөлт хоёуланд нь хүрнэ.

Layout.tsx-ийг хуваалцаагүй: тэр нь mount дээр /api/v1/me асуудаг — операторт байхгүй session. Хуваалцаж буй зүйл нь stylesheet.

Fixed — модулийн байнгын ажил миграцынхаа өмнө эхэлдэг байв

StartBackgroundJobs нь v1.15.0-д бүртгэгдсэн модуль бүрийн StartHousekeeping-ийг дууддаг болсон боловч жагсаалтын эхэнд байрлаж байв — модулийн миграцыг ажиллуулдаг ApplyCatalogToInstallations нь тэр функцийн төгсгөлд. Хүснэгтээ энэ хувилбараар авсан модуль эхний шүүрдэлтээ байхгүй схем дээр хийнэ.

client.gerege.mn-ий v1.15.0 rollout дээр Өртөөгийн суваг эхний дамжлагадаа relation "urtuu_peers" does not exist гэж зургаан удаа бичив, каталогийн шүүрдэлтээс 600мс өмнө. Өөрөө эдгэрдэг — дараагийн тик ажиллана — гэхдээ логоос харахад deploy унасантай ялгагдахгүй, ба энэ нь суулгац бүрийн эхний ачаалалт дээр давтагдана.

Дараалал зассан; TestAModulesHousekeepingStartsAfterItsMigrations нь өөрийн миграцтай модуль бүртгээд StartHousekeeping дотроосоо хүснэгтээ асуудаг.


[1.15.0] - 2026-08-27

Нэг сэдэв, хоёр хагас: нэршил ба хил. Эхнийх нь кодоос хоёр үгийг гаргав — platform, tenant — хоёр дахь нь цөмөөс хоёр бүтээгдэхүүнийг. Хооронд нь хүний хувийн муж («гэр») суув: гишүүнчлэлгүй хүн нэвтэрч, байгууллагаас хүсэлтийнхээ төлөвийг уншиж, өөрөө нэгдэх хүсэлт гаргадаг болов.

⚠️ v1 дотор эвдэрсэн өөрчлөлт. pkg/nexus-ийн экспортолсон гадаргуугаас нэрс хасагдаж, дахин нэрлэгдсэн. Semver-ээр бол энэ нь major боловч Go нь v2+ дээр модулийн замыг .../backend/v2 болгохыг шаарддаг — тэр нь найман distribution бүрийн бүх import-ыг өөрчилнө. Тиймээс v1 дотор гаргаж, distribution бүр go.mod-ын нэг мөр ба нэршлийн mechanical rename хийнэ. Wire format (tenant_id гэх JSON тэмдэглэгээ) өөрчлөгдөөгүй: web shell, дөрвөн native клиент, гадны OAuth2 клиент бүр хуучин шигээ уншина.

Changed — ⚠️ нэршил: platform ба tenant гэдэг үг кодоос гарлаа

platform гэдэг нэр гурван өөр зүйлийг нэрлэж байв — операторын урсгал, өгөгдлийн сангийн schema, процессыг угсардаг host. tenant нь байршлын үг бөгөөд байгууллагыг заах домэйний үгийн байрыг эзэлж байв (ADR 0001). Хоёулаа одоо зөвхөн прозод үлдэв:

Юу Байсан Болсон
Операторын урсгал internal/platform internal/operator
Процесс угсрагч pkg/platform pkg/host
Байгууллагын урсгал internal/tenant internal/workspace
DB schema platform registry + operator (27 хүснэгтийг хоёр эх сурвалжаар хуваав)
DB schema tenant workspace
SDK TenantID, RequireTenant, AllowedTenants, TenantOf … WorkspaceID, RequireWorkspace, AllowedWorkspaces, WorkspaceOf …

api.txt-ийн 554 мөрөөс 65 нь дахин бичигдсэн, нэг ч тэмдэглэгээ нэмэгдээгүй, хасагдаагүй. Нэрлэлтийг gopls rename-ээр хийсэн тул TenantID нэртэй боловч SDK-гийнх биш дотоод бүтцүүд хөндөгдөөгүй. pkg/platform нь эхлээд дамжуулагчаар үлдэж, дараа нь хасагдав.

Distribution-д хийх ажил: go.mod-ыг энэ хувилбар руу зөөж, nexus.RequireTenant → nexus.RequireWorkspace маягийн mechanical rename. client-gerege-nexus дээр 72 дуудалт байв.

Added — хүн өөрийн мужтай болов («гэр»)

Өмнө нь гишүүнчлэлгүй хүн нэвтэрч чаддаггүй байв: схем нь хүнийг байгууллагаас хамааралгүй гэж үздэг атал нэвтрэлтийн код хориглодог — схемийн зөвшөөрснийг код хориглож буй цорын ганц цэг. EID_JIT_TENANT_SLUG гэсэн тойрч гарах зам нь иргэнийг хэн ч шийдээгүй байгууллагад гишүүн болгодог байв.

  • Гэр нь гуравдахь plane биш, муж мөр (ADR 0006). Шинэ schema, шинэ role, шинэ GUC байхгүй: app.current_tenant иргэний session дээр ҮРГЭЛЖ тавигдана.
  • person_items — проекц, агуулга биш (00086). Нийлүүлэгч даалгаврын төлөвөө иргэний гэрт нийтэлнэ (nexus.PersonFeed); баримт, нотолгоо, хувийн мэдээлэл нийлүүлэгчийн мужид үлдэнэ. Байгууллага дамнасан уншилт огт үүсэхгүй.
  • /me/items, /me дэлгэцүүд. Хоёр дахь бүрхүүл биш — гэрт байгаа хүнд байгууллагын дэлгэцүүд харагдахаа болино.
  • Нэгдэх хүсэлт (00089): хүн байгууллагад өөрөө хүсэлт гаргана, цөм person_items рельсээ өөрөө ашигладаг болов — «half a capability is none»-ыг давтахгүйн тулд.
  • Үйлчилгээгээр нь байгууллагыг олох лавлах: иргэн «жолооны үнэмлэх сунгуулах» гэдгээ мэднэ, хэн хийдгийг нь мэдэхгүй.
  • Кодын хувьд internal/person домэйн (route, схем, зан төлөв хөдлөөгүй).

Removed — ⚠️ Өртөө ба гадаад холбогч цөмөөс бүрмөсөн гарлаа

Хоёр бүтээгдэхүүн, нэг шалтгаан. Аль аль нь өмнө нь хагасаараа гарсан байв: дэлгэц, амьдралын мөчлөг нь client-gerege-nexus руу явж, «рельс» гэж нэрлэгдсэн хэсэг нь цөмд үлдсэн. Тэр заагийг барьсан үндэслэл нь CORE_BOUNDARY_PLAN.md §4.1-ийн шалгуур: «нэг суулгац дотор үүнээс хоёр зэрэг оршиж чадах уу». Суваг ч, шифр ч чадахгүй — тиймээс рельс.

Шалгуур зөв, гэхдээ дутуу байсан. «Хоёр байж болохгүй» гэдэг нь «цөмд байх ёстой» гэсэн үг биш: хэрэглэгч нь ганц апп бол «ганц байх» гэдэг нь тэр аппын дотор ганц байна гэсэн үг. Гурван сарын дотор nexus.Link ба nexus.PeerDirectory-г Өртөөгийн самбараас өөр юу ч дуудсангүй, экспортыг esign рельсээс өөр юу ч дуудсангүй, nexus.MeetingBooker-ыг юу ч дуудсангүй. Тиймээс шалгуурт хоёр дахь асуулт нэмэгдэв: хоёр өөр бүтээгдэхүүн үүнийг өнөөдөр дуудаж байна уу?

Өртөө (00087) — тээвэр (internal/workspace/urtuu), дугтуйн гэрээ (pkg/urtuu), nexus.Link, nexus.PeerDirectory болон сувгийн зургаан хүснэгт бүгд явлаа. Цөмд Өртөөгийн юу ч үлдсэнгүй: маршрут ч, /.well-known/urtuu.json ч, URTUU_*/RING_* хувьсагч ч, операторын консолын «Өртөө» гэрэл ч. Аппын хувилбар: io.gerege.nexus.urtuu 1.1.0.

Гадаад системийн интеграц ба webhook (00088) — менежер, провайдерын бүртгэл, OAuth солилцоо, илгээх давталт, гурван хүснэгт бүгд явлаа. nexus.MeetingBooker гэрээ устлаа. Аппын хувилбар: io.gerege.nexus.integrations 1.1.0.

Added — nexus.SecretSealer

Цөмд үлдсэн ганц зүйл нь шифр: INTEGRATION_ENCRYPTION_KEY ба түүний AES-GCM нь internal/kernel/security-д хэвээр бөгөөд гадагш энэ гэрээгээр гарна. Энэ нь шалгуурын хоёр асуултыг хоёуланг нь давсан цорын ганц хэсэг: нэг суулгацад хоёр шифр байж болохгүй, бөгөөд итгэмжлэл хадгалдаг аль ч модуль түүнийг дуудна. Хувьсагчийн нэр өөрчлөгдөөгүй — тэр нь ижил түлхүүр.

Мөн модуль өөрийн байнгын ажлаа зарлах зам нэмэгдэв: бүртгэгдсэн модуль StartHousekeeping(context.Context) метод агуулбал платформ түүнийг эхлүүлнэ. Өмнө нь apps.Bootstrap-ийн жагсаалт л энэ боломжтой байсан бөгөөд тэр нь цөмийн дотоод — Өртөөгийн солилцооны давталт гадаад репод очиход энэ нь зайлшгүй болов.

Changed — ⚠️ зан төлөв өөрчлөгдсөн гурван зүйл

  1. Гарын үсэг зурсан баримт гадагш автоматаар илгээгдэхээ болив. POST /api/v1/esign/documents/{id}/export алга болов. Экспортын код нь холбогчийн аппын дотор байгаа тул хэрэгтэй өдөр нэг дуудлагын зайд байна.
  2. Апп суулгаагүй суулгац Өртөөгийн дугтуй хүлээж авахаа болив. Өмнө нь аппгүй ч дугтуй ирж, шалгагдаж, хадгалагдаж, апп суулгагдмагц уншигдаж байсан.
  3. Демо суулгацын integrations апп жагсаалтаас хасагдав (pkg/host/seed.go).

Changed — CI нь downstream repo-г clone хийхээ болив

Платформ өөрийн эзэмшдэггүй репог шинэчилж push хийх хүртэл merge хийж чадахгүй байсан — хамаарлын чиглэл урвуу. Гэрээний эвдрэлийг api.txt ба testdata/canary (тусдаа Go модуль тул go build ./... түүнд хүрэхгүй, CI л хүрнэ) хоёр барина.

Migration — амьд суулгац дээр юу хийх вэ

Энэ хувилбар нь арван нэгэн миграц (00079–00089) агуулна, тэдгээрийн хоёр нь бүх хүснэгтийг schema хооронд зөөнө (platform → registry+operator, tenant → workspace) ба нэг нь DB role-ыг gerege_nexus_app → gerege_nexus_tenant болгож нэрлэнэ. Аль аль нь ажиллаж буй суулгац дээр шалгагдсан (nexus.gerege.mn, 2026-08-27).

Модулийн миграц бичдэг distribution бүр анхаараарай: role-ыг нэрээр нь бичсэн бол цэвэр өгөгдлийн сан дээр role "gerege_nexus_app" does not exist гэж унана. pg_roles-оос хайж, gerege_nexus_tenant-ыг эхэнд тавь.

00087 ба 00088 нь есөн хүснэгтийг CASCADE-аар хаяна. 2026-08-27-нд гурван суулгац дээр тоолоход бүгд 0 мөртэй байв (nexus.gerege.mn дээрх хоёр холбогч нь INACTIVE, хэзээ ч холбогдож байгаагүй). Өгөгдөлтэй хост дээр appstore-ийн store_* хүснэгтүүдэд ажилласан алхмыг давт: миграцаас өмнө CREATE TABLE <нэр>_keep AS SELECT * FROM workspace.<нэр>, rollout, дараа нь гадаад түлхүүрийн дарааллаар буцааж INSERT. Модулийн миграц нь хүснэгтүүдийг IF NOT EXISTS-ээр дахин зарлах бөгөөд платформ суулгацын үед бус ачаалахдаа модулийн схемийг шүүрддэг (v1.10.1-ээс хойш), тиймээс гар засвар шаардлагагүй.

Distribution-ууд: pkg/urtuu, pkg/nexus.Link, PeerDirectory, MeetingBooker дөрөв алга болсон нь эвдэрсэн өөрчлөлт. Эдгээрийг хэрэглэдэг цорын ганц repo нь client-gerege-nexus бөгөөд тэр нь энэ өөрчлөлттэй хамт шинэчлэгдсэн.


[1.14.1] - 2026-08-26

Fixed — өөр байгууллагын клиентэд өгөх анхны зөвшөөрөл бүтдэггүй байв

Зөвшөөрлийн дэлгэцийн ард байх /api/v1/oauth2/consent нь клиентээ дуудагчийн байгууллагад уягдсан холболтоор уншиж байсан тул мөрийн түвшний хамгаалалт өөр байгууллагын бүртгэсэн клиентийг нуудаг байв. Үр дүнд нь unauthorized_client: unknown or disabled client буцаж, байгууллага хоорондын анхны нэвтрэлт хэзээ ч гүйцэддэггүй: клиентээ бүртгэсэн байгууллагын гишүүд ороод, бусад нь орж чаддаггүй.

Клиент нэг байгууллагынх, нэвтэрч буй хүн өөр байгууллагынх байх нь зохиомжийн санаа — issueAuthCode нь кодод клиентийнх биш хэрэглэгчийн байгууллагыг хадгалдаг нь яг үүний төлөө. Нэвтрэлтгүй ажилладаг /oauth2/auth аль хэдийн платформын замаар уншдаг байсан; одоо consent хоёр ч мөн адил уншина.

TestConsentPromptFindsAnotherTenantsClient нь dbguard-тай усан сан дээр хоёр байгууллага үүсгэж, засваргүйгээр production-ий яг тэр 400-г давтдаг.


[1.14.0] - 2026-08-25

Changed — ⚠️ PlatformVersion-ийг стампдах -X зам өөрчлөгдөв

internal/platform/server.go-д байсан PlatformVersion нь v1.13.0-ийн дараа internal/kernel/config-т нүүсэн (5471576). Distribution-ий Dockerfile-ууд хуучин замаар стампддаг бол юу ч болохгүй: Go нь байхгүй тэмдэгт рүү чиглэсэн -X-ийг чимээгүй үл тоомсорлодог тул образ өөрийгөө 1.1.0 гэж хэлээд, өөрийнхөө каталогийн manifest бүрийг татгалзаж, асахаа болино.

-X .../backend/internal/platform.PlatformVersion=${version}        # үхмэл
-X .../backend/internal/kernel/config.PlatformVersion=${version}   # зөв

v1.14.0 бол энэ нь мэдрэгдэх эхний хувилбар. Бумп хийхийн өмнө deploy/Dockerfile-аа засах хэрэгтэй — appstore-gerege-nexus, sso-gerege-nexus хоёр 2026-08-25-нд яг үүнийг хийсэн.

Removed — SSO клиентийн апп цөмөөс гарч, App Store-д очлоо

internal/apps/sso_clients нь энэ репогийн сүүлчийн апп байв. Одоо appstore-gerege-nexus-д modules/ssoclients болж, каталогоор дамжин аль ч distribution түүнийг апп стороос татаж суулгана — эхнийх нь sso-gerege-nexus.

Үүний дараа catalog/apps.json ба internal/apps хоёулаа хоосон. Энэ нь дутуу байдал биш: ECOSYSTEM_GIT_STRATEGY-ийн тавьсан "бизнес апп огт байхгүйгээр асдаг платформ" шалгуур ингэж биеллээ. Аппын дэлгэц, migration history нь shell-д үлдсэн бөгөөд module-гүйгээ амьгүй (§2.3-ын дүрэм, contacts-тай адил) — иймд цөмийн frontend image-ийг хуваалцдаг distribution бүр дэлгэцээ хэвээр авна.

Рельс нь цөмийнх хэвээр. OAuth2 сервер — код олгох, токен солих, id_token гарын үсэглэх, клиент хадгалах, redirect шалгах — бүгд internal/tenant/ssoprovider-т үлдэв. Апп нь тэр рельс рүү хорин экспортолсон нэрээр биш, шинэ гэрээгээр хүрнэ.

Added — nexus.SSOClientRegistry

Модулийн SDK-д OAuth2 клиентийн бүртгэлийн гэрээ нэмэгдэв: SSOClientRegistry interface ба SSOClient, SSOScope, SSOClientActivity, SSOConsent, SSOSigningKey төрлүүд, ErrSSOClientNotFound sentinel.

Цөм нь nexus.Provide[nexus.SSOClientRegistry](https://github.com/gerege-systems/private-gerege-nexus/blob/main/ssoprovider.AsClientRegistry(...)) гэж нийтэлнэ. Ямар ч distribution nexus.Capability[nexus.SSOClientRegistry]() гэж асуугаад өөрийн суулгацын провайдерыг администрацлах апп агуулж чадна.

Нууц үгийн digest гэрээгээр гардаггүй: CreateClient/RotateClientSecret нь задгай нууцыг авч, hash-ыг цөм өөрөө хийнэ — модуль тухайн суулгацын hash функцийг мэдэх шаардлагагүй.

Changed — Консолыг хаягаар хязгаарлах нь зөвхөн платформ ХААЛТТАЙ үед

Консол нь nginx-ийн deny all allowlist-ын ард байсан бөгөөд тэр хаалт нь платформын өөрийнх нь тохиргоог харж чаддаггүй: нээлттэй (public) горимд ажиллаж буй, хэн ч бүртгүүлж болдог суулгац операторуудаа хаанаас нэвтрэхийг нь зааж, өөрчлөх бүрд secret + deploy + nginx reload шаарддаг байв.

Одоо шийдвэрийг платформ өөрөө, хүсэлт бүр дээр гаргана:

хаалттай (private) → хаяг жагсаалтад байх ёстой (жагсаалт өгсөн бол)
нээлттэй (public)  → хаягаар огт хязгаарлахгүй

Үнэ нь ил: нээлттэй суулгац дээр танихгүй хүн консолын нэвтрэх дэлгэц хүртэл хүрнэ. Ард нь хэвээрээ — хостоос гараар үүсгэсэн оператор бүртгэл, нууц үг, баталгаажсан хоёр дахь хүчин зүйл, өөрчлөлт бүрд дахин асуух step-up. Хаягийн жагсаалт бол таван давхаргын хамгийн гадна талынх бөгөөд платформын өөрийнх нь «энэ суулгац хэнийх вэ» гэсэн хариулттай зөрчилддөг цорын ганц нь байлаа.

nginx-ийн snippet хэвээр — хүсвэл ирмэг дээр давхар хаалт болно. CONTROL_PLANE_ALLOWED_CIDRS-д open гэж бичвэл түүнийг өргөнө. 0.0.0.0/0 гэж бичих боломжгүй хэвээр: буруу бичсэн prefix консолыг чимээгүй нээхээс сэргийлнэ, харин open гэдэг үгийг санамсаргүй бичихгүй.

Changed — open-gerege-core-оос бүрмөсөн салав

Гурван багц үлдсэнийг nexus дотроо бичив:

Хуучин Шинэ Юу
pkg/eid internal/kernel/eidrp eID-гийн RP клиент (QR/push нэвтрэлт, poll, төлөөлөл)
pkg/gemini internal/kernel/gemini generateContent + WAV туслах
core/business/usecases/sign internal/kernel/eidsign PDF/digest-д PIN2 гарын үсэг, PAdES

go.mod-оос github.com/gerege-systems/open-gerege-core бүрэн хасагдав. pdfcpu, digitorus/pdf, digitorus/pdfsign гурав indirect байснаа шууд хамаарал болов — өөрчлөлт нь тэднийг ХЭН эзэмшиж байгаад л, аль хувилбар ажиллаж байгаад биш.

Юуг нь авчирсангүй: gemini-гийн embedding клиент, streaming, model fallback — энэ репод хэн ч дуудаагүй. eID-гийн гарын үсэг зурагчид, PKI самбар, төлөөллийн бичилт — өөр бүтээгдэхүүнийх.

Wire protocol гурвуулаа өөрчлөгдөөгүй. Гарын үсгийн багц дээр нэг зүйл сайжрав: өмнө нь алдааны утгыг («represent» гэсэн үг агуулж буй эсэх) хайж HTTP статус шийддэг байсныг одоо errors.Is-ээр яг таарууллаа — үг солиход статус өөрчлөгддөггүй болов.

Гарын үсгийн урсгалын хамгаалалтууд хэвээр бөгөөд одоо тестээр барьцаалагдав: өөр иргэний session «олдсонгүй» гэж хариулна (IDOR), хэрэглэгчийн өгсөн зургийн URL дотоод сүлжээ рүү хүрэхгүй (dial түвшний SSRF шалгалт, redirect дагахгүй), production-д түр зуурын Document-Signer хориотой, displayText кирилл тэмдэгтээр таслагдана.

Changed — eID-ийн RP клиентийг nexus дотроо бичив, person блокийн шинэ нэршилд оруулав

eID нь person блокоо core.gerege.mn-ий нэршилд оруулж, birthDate, gender, familyName, geID нэмсэн. Гурван нэр нь гурван ӨӨР ойлголт бөгөөд одоо тэрийгээ хэлдэг болов:

firstName  = нэр            (өөрийн нэр)
lastName   = ЭЦГИЙН нэр     (сертификатын subject-д ордог)
familyName = УРГИЙН овог    (сертификатад ОРОХГҮЙ)

Өмнө нь givenName/surname хоёрыг уншиж байсан бөгөөд «surname» гэдгийг эцгийн нэр рүү тулгадаг байсан нь англиар зөв уншигдаж, монголоор ил буруу байдаг төрлийн алдаа.

open-gerege-core/pkg/eid-ийг ашиглахаа больж, RP клиентээ internal/kernel/eidrp-д бичив. Хоёр шалтгаан, хоёр дахь нь чухал:

  • Тэр сан өөр репогоос, өөрийн хуваарьаар гардаг. eID geID нэмэхэд дугаар нь энэ платформын доод давхаргад тасарч, хийх зүйл нь хувилбар гартал хүлээх л байв. Нэвтрэлт гэдэг version bump-ын цаана байх ёсгүй.
  • Тэр сангийн гадаргуугаас нexus ердөө ДӨРВИЙГ л ашигладаг байсан (хоёр initiate, poll, төлөөлөл). Үлдсэн нь — гарын үсэг зурагчид, PKI самбар, төлөөллийн бичилт — өөр бүтээгдэхүүний хэсэг, дагаж хөрвөгдөж явдаг байлаа.

Wire protocol өөрчлөгдөөгүй: ижил зам, ижил ACSP_V2 body, ижил Bearer. Хуучин givenName/surname талбарыг fallback болгож уншина — нэг тал шинэчлэгдээд нөгөө нь хоцорсон өдөр иргэн бүрийн нэр алга болохоос сэргийлнэ.

Changed — eID-ээр нэвтэрсэн иргэний хаяг утгатай болов, geID нь доош дамжина

eID-гийн session хариу COMPLETE+OK үед person.geID — иргэний core.gerege.mn дахь дугаар — буцаадаг болсон. Түүнээс өмнө энэ платформ eID-ээр анх нэвтэрсэн иргэнд eid+3854e516490de4f6184ac3af2d8cc8b8@identity.invalid гэсэн зохиомол хаяг үүсгэдэг байв: давхцахгүй, тогтвортой, гэхдээ хүн харахад утгагүй — тэр дундаа энд холбогдсон RP-үүд түүнийг жинхэнэ хүний email claim болгож хүлээж авдаг байсан.

Одоо geID мэдэгдэж байвал хаяг нь 10000263@gemail.com. Хуучин хаягтай бүртгэлүүд дараагийн нэвтрэлтдээ шинэчлэгдэнэ — тэр хаягаар хэн ч нэвтэрч байгаагүй тул алдах зүйл алга. Өөрийн жинхэнэ хаягаар бүртгүүлээд дараа нь eID-ээ холбосон хүний хаягийг хөндөхгүй: нэвтрэх арга нэмэх нь нэр солих шалтгаан биш.

geID нь platform.users.ge_id баганад тусад нь хадгалагдана (NULL зөвшөөрнө — eID-д заавал байх талбар биш) бөгөөд нэг дугаар = нэг хүн гэдгийг unique index барина. Доод урсгал руу хоёуланг нь өгнө: email ба ge_id нь id_token, userinfo хоёуланд гарч, discovery-гийн claims_supported-д зарлагдав. Хаягнаас нь дугаарыг нь салгаж авдаг RP байх ёсгүй — тэр нь хаягийн хэлбэр өөрчлөгдөх өдрийг хүртэл ажилладаг код юм.

Тайлбар: open-gerege-core/pkg/eid-ийн RP клиент нь person блокоос geID-г задлахгүй тул тэр замаар ирэхгүй байна. Түүнийг зөв уншдаг болтол дугаарыг Gerege Core бүртгэлээс регистрээр нь авна — geID гэдэг нь тодорхойлолтоороо core.gerege.mn-ий users.id тул үр дүн нь ижил. Токен байхгүй, бүртгэл хүрэхгүй, иргэн бүртгэлд байхгүй — гурвуулаа адилхан «geID алга» гэж дуусах ба аль нь ч eID зөвшөөрсөн нэвтрэлтийг унагаах шалтгаан биш.

Fixed — Консолын нүүр хуудас цагаан дэлгэц болдог байсныг зассан

cp.<домэйн>/cp нь «This page couldn't load» гэж унадаг байв. Шалтгаан нь нэг мөрөнд: observability.New(...) нь Warnings callback-ыг огт дамжуулаагүй тул warnings талбар JSON-д null болж, нүүр хуудас түүнийг .map хийхэд бүх дэлгэц уначихдаг байлаа. Go-д nil slice нь null болдог бөгөөд жагсаалт гэж зарласан талбарт null ирэх нь «богино жагсаалт» биш, өөр төрөл.

Хоёуланг нь зассан: callback-ыг холбож (нүүр хуудас платформын өөрийн анхааруулгуудыг дахин харуулна), мөн Overview буцахдаа бүх жагсаалтаа хоосон жагсаалт болгодог болов — найман талбарыг найман газарт санахын оронд нэг дор.

Added — Тохиргоог консолоос: нууц бус утга нэг дэлгэцэд, түлхүүрүүд нөгөөд

.env засаад контейнер дахин асаах гэдэг нь тохиргоо өөрчлөх ганц зам байв. Одоо /cp/config дээр:

  • Тохиргоо — ai.tts_model, brand.name, observability.prometheus_url, observability.alertmanager_url, observability.grafana_url нэмэгдэв. Хуучин таван түлхүүрийн адил: утга хаанаас ирснийг харуулна, өөрчлөлт бүр шалтгаантай, түүхтэй, буцаагдана. Env хувьсагч нь fallback хэвээр.
  • Түлхүүрүүд — ai.gemini_api_key, core.api_token, reports.smtp_url.

Түлхүүрүүд нь тохиргооны хүснэгтэд ОРООГҮЙ бөгөөд орох ч ёсгүй: internal/kernel/settings нь нууц Kind байхгүй, нууц мэт нэрийг panic-аар татгалздаг гэдгээрээ тэр хилээ кодоор барьдаг. Оронд нь тусдаа хүснэгт, тусдаа дүрэмтэйгээр:

  • утга нь баганад хүрэхээсээ өмнө AES-256-GCM-ээр битүүмжлэгдэнэ (INTEGRATION_ENCRYPTION_KEY — холбогчийн эрхийг битүүмжилдэг яг тэр түлхүүр, тиймээс шифр нь хоёр plane-ийн доор kernel/security-д нүүв);
  • буцааж уншигдахгүй — утга буцаадаг route байхгүй, дэлгэц зөвхөн эх сурвалж, сүүлийн дөрвөн тэмдэгтийг харуулна;
  • бичих бүрд хоёр дахь хүчин зүйл дахин шаардана;
  • audit мөр нь юу өөрчлөгдсөнийг бичнэ, утгыг нь БИШ — хуучин утгыг агуулсан түүх гэдэг бол «энэ суулгацын хуучин түлхүүрүүдийн жагсаалт» гэсэн үг.

Хэрэглэгч талдаа: copilot болон дуут боломжийн клиент нь түлхүүр солигдвол дахин баригддаг болов (өмнө нь зөвхөн загвар солигдоход), тайлангийн SMTP relay нь илгээх үедээ уншигддаг болов — өмнө нь ачаалах үед л шийддэг байсан тул консолоос тавьсан утга дахин ачаалтал үйлчлэхгүй байх байлаа.

Added — Байгууллагын мэдээллийг регистрээс нь шинэчлэх

/organisation дээр «Core-оос шинэчлэх» товч. Регистрийн дугаараар Gerege Core-оос албан ёсны нэр, хаяг, утас, и-мэйлийг татна. Регистрд байхгүй талбарыг хөндөхгүй — байхгүй утгаар дарж хоослох нь огт ажиллаагүйгээс дор.

Товч болохоос товлосон ажил биш: буцаж ирсэн утга нь админы нүдний өмнө байгаа талбаруудыг дарж бичдэг тул хүнгүйгээр хийвэл Мягмар гаригт хийсэн засвар Лхагва гаригт чимээгүй алга болно.

Added — Анхны тохиргооны шидтэн: байгууллагаа регистрээр нь татаж авдаг

Тушаалын мөр бүх хүнд тохирохгүй. Шинэ deployment дээр одоо /setup гэсэн гурван алхамт дэлгэц гарна: байгууллага, админ, нууц үг. Байгууллагын болон хүний регистрийн дугаараар Gerege Core-оос нэр, албан ёсны нэр, и-мэйлийг татна — суулгасан хүний гараар бичсэн нэр нэхэмжлэх дээрхтэй таарахгүй байдгийн эсрэг.

Вэб дээрх first-run хуудсыг аюулгүй болгодог хэсэг нь ихэвчлэн орхигддог: нээлттэй /setup бол хамгийн түрүүнд хүрсэн хүнийх. Тиймээс энэ нь токеноор зэвсэглэдэг — 256 бит, зөвхөн байгууллагагүй үед, зөвхөн санах ойд, ачаалах үед лог руу нэг удаа бичигдэж, байгууллага үүссэн даруйд устана. Токенгүй үед эндпойнтууд 401 биш 404 буцаана: "буруу токен" гэж хэлэх нь таах токен байгааг хэлж байгаа хэрэг.

tenant-bootstrap команд хэвээрээ — терминалтай хүнд, хөтөчгүй сервер дээр.

Added — Шинэ deployment-д эхний байгууллагаа нээх команд

Шинэ deployment дээр орох зам байсангүй: бүртгүүлэх дэлгэц байхгүй, demo бүртгэл production дээр үүсэхгүй, консол нь өөрийн vhost, оператор бүртгэл, TOTP шаарддаг. Өмнөх хоёр бүтээгдэхүүн гараар бичсэн SQL-ээр орсон бөгөөд тэр SQL нь илэрхий биш гурван зүйлийг зөв хийх шаардлагатай — admin эрхийг tenant мөрийн trigger үүсгэдэг, админ эрх platform.users.is_admin дээр биш гишүүнчлэлийн эрх дээр тогтдог, гишүүнчлэлд эрхийн мөр нь тусад нь хэрэгтэй — бөгөөд эхний оролдлогод гурвуулаа буруу хийгдсэн.

Одоо tenant-bootstrap команд образ дотор явна, консолын operator-bootstrap- тай яг ижил зарчмаар: DATABASE_URL эзэмшдэг хүн ажиллуулна, нууц үгийг TTY-ээс асууна, ард нь юу ч үлдээхгүй. Байгууллага аль хэдийн байвал татгалзана — анхдагч нууц үг ч биш, үүрд үлддэг env хувьсагч ч биш, ачаалахаас хойш онгорхой зогсох first-run вэб хуудас ч биш.

Асах үед байгууллагагүй deployment-ийг лог нь хэлж, яг ажиллуулах командыг зааж өгдөг болов — өмнө нь энэ төлөв чимээгүй байсан тул хоёр удаа "нууц үг буруу" гэсэн шинжээр дебаг хийгдсэн.

Fixed — Суулгаагүй аппын цэс хажуугийн самбарт үлддэг байсныг зассан

«AI тохиргоо» ба «Интеграцууд» хоёрыг бүрхүүл өөрөө бичдэг байв. Тэр хоёр өнөөдөр апп болсон тул тэдгээрийг суулгаагүй байгууллага ч гэсэн цэсэндээ харсаар, дарахад л 404 авдаг байв.

Одоо модуль өөрсдөө зарлана (Group: settings), тэгэхээр:

  • апп суулгаагүй бол цэс байхгүй — цэсний API суулгасан аппын бичлэгийг л буцаадаг;
  • эрхгүй хүнд ч харагдахгүй: хоёулаа MenuPermission нь бичих эрх (ai.manage, integrations.manage) — prompt бичих нь байгууллагын бүх гишүүний ярьдаг туслахыг тодорхойлно, холбогчийн хаяг нь сервэрийг хэн нэгний бичсэн хаяг руу хүсэлт явуулдаг болгоно.

Бүрхүүл мөн бүх chrome аппын цэсийг зурдаг болов, эхнийхийг нь биш. Гурав нь тийм тугтай (байгууллага, туслах, холбогч) бөгөөд өмнө нь эхнийх нь л зурагддаг байсан тул нөгөө хоёрын дэлгэц утсан дээр хаанаас ч хүрэхгүй байв — доод таб мөр зөвхөн rail аппуудыг жагсаадаг.

Fixed — Суулгасан ч бинарь үүрдэггүй апп чимээгүй алга болдог байсныг хэлдэг болов

Апп өөр репод нүүхэд суулгацын мөр нь үлддэг. Тэр байгууллагын хувьд апп нь маршрут нь 404, цэсний API-д байхгүй — өөрөөр хэлбэл хаанаас ч харагдахгүй болно. Утсан дээр аппуудын хооронд явах цорын ганц зам нь доод таб мөр тул энэ нь «цэс эвдэрлээ» гэж уншигдана.

Суулгац буруу байгаа хэрэг биш — аппын явалтыг давж шинэчилсэн оператор тэр байдалд хууль ёсоор орно — харин юу ч дуугардаггүй байсан нь алдаа. Одоо асалт бүрт нэрсийг нь бичнэ:

catalog: organisations have apps installed that this binary does not carry;
their routes answer 404 and they appear in no sidebar.
Either deploy a distribution that compiles them, or uninstall them
apps=[io.gerege.nexus.documents io.gerege.nexus.egov io.gerege.nexus.organisation]

Гадаад апп үүнд ороогүй: тэдгээр нь өөр газар ажилладаг бөгөөд энэ бинарь тэднийг үүрэх ёсгүй.

Removed — Цэсний blueprint пакет устав; модуль өөрийн цэсээ бүрэн зарлана

internal/platform/menu/blueprints.go нь аппын id-гаар түлхүүрлэгдсэн хүснэгт байв: «энэ апп цаашид ийм дэлгэцүүдтэй болно» гэсэн жагсаалт, зөвхөн энэ репогийн аппууд өөрсдийгөө нэмж чадах. Тэр хүснэгт устлаа.

nexus.MenuDefinition.Group нэмэгдэв (MenuGroupModules, MenuGroupSettings). Аппын хоёр толгойн алинд нь дэлгэц харьяалагдахыг модуль хэлдэг болов — эцэг нь платформынх хэвээр, тэгэхгүй бол апп өөр аппын бүлэг рүү бичлэг оруулж чадна. Талбар нэмэх нь эвдэх өөрчлөлт биш (RELEASING §1).

sso_clients нь blueprint-ийн таван бичлэгээ өөрөө зарлав, замууд нь өөрчлөгдөөгүй (/module/sso-clients/<id>).

Хоёр шалгуур модулиудынхаа хажууд нүүлээ: цэсний бичлэг бүр жинхэнэ хуудастай байх, шошго бүр долоон хэлээ хамрах. Аль аль нь одоо nexus.List()-ийн оронд internal/apps-ийн модулийн хүснэгтээс асууна.

Removed — Тайлангийн апп цөмөөс гарав

internal/apps/reports ба domain/reports нь client-gerege-nexus руу нүүлээ. Гарах боломжтой болсон нь v1.12.0-ийн дөрвөн гэрээ (ReportEngine-ийн Form/RunConsolidated, ReportSchedules, ReportGrants) ба v1.13.0-ийн InstalledApps.

Хөдөлгүүр цөмд үлдэв — тайлангийн SQL ажиллуулах, экспорт бичих, гурван цагт шуудан илгээх, өөр байгууллагын мөрийг зөвшөөрлөөр унших. report_schedules ба report_grants хүснэгтүүд ч мөн адил: платформ өөрөө бичдэг тул аппын хамт явсангүй — Өртөөгийнхөөс ялгаатай тал нь энэ.

Тэгэхээр суулгац тайлангаа шуудангаар авсаар байна, дэлгэц нь байхгүй ч гэсэн. Хуваарь платформын мөр, sweep платформын давталт.

/api/v1/reports/* арван хоёр маршрут энэ бинариас алга. Хүснэгт устгасан миграц байхгүй.

Энэ бол цөмөөс гарсан зургаа дахь апп бөгөөд internal/apps дотор суулгацын өөрийн бүтээгдэхүүн үлдсэнгүй: одоо тэнд байгаа дөрөв — туслах, холбогч, ээлж солих PIN, SSO клиентийн бүртгэл — бүгд платформын рельсийн дэлгэц.


[1.13.0] - 2026-08-23

Added — nexus.InstalledApps: аль апп суусныг distribution ч асууж чадна

Тэр чадамж нь internal/apps.InstalledApps нэрээр нийтлэгддэг байв — repo-гийн гаднах модуль тэр төрлийг нэрлэж чадахгүй тул асууж ч чадахгүй. Бүртгэл нь төрлөөр түлхүүрлэдэг гэдгийг санавал энэ хоёр өөр түлхүүр байсан гэсэн үг.

Гэрээ экспортлогдож, nexus.AppsOf(ctx, tenantID) туслах нэмэгдэв. Энэ бол PDF-ийн рельс өнөөдөр давсан яг тэр алдаа, яг адилаар илэрсэн: цөмөөс гарсан апп платформын өгч байгаа чадамжийг авч чадахгүй.


[1.12.0] - 2026-08-23

Note — ReportEngine таван метод авсан нь v1 доторх шийдвэр

RELEASING.md §1 нь экспортолсон interface-д метод нэмэхийг эвдэх өөрчлөлт гэж нэрлэдэг: гаднаас түүнийг хэрэгжүүлж байсан бүх төрөл компиллогдохоо болино. Энэ хувилбар ReportEngine-д таван метод нэмж байгаа тул журмаараа major байх ёстой.

Minor-оор гаргаж байгаа шалтгаан, ил тод: ReportEngine бол платформ өгдөг чадвар — distribution нь дуудагч тал, хэрэгжүүлэгч нь энэ репогийн адаптер. Экосистемд өөр хэрэгжүүлэгч мэдэгдэхгүй байна. Хэрэв танай distribution nexus.ReportEngine-ийг өөрөө хэрэгжүүлдэг бол энэ хувилбар дээр компайл унана — тэр тохиолдолд бидэнд хэлээрэй, дараагийн major дээр гэрээг хуваана.

Added — Тайлангийн гэрээ бүрдэв: ReportSchedules, ReportGrants, Form, RunConsolidated

pkg/nexus/reportengine.go гурван цоорхойгоо нэрлээд «эдгээр буутал reports апп internal/platform/reporting-ийн импортоо хадгалж, энэ репод үлдэнэ» гэж бичсэн байсан. Гурвуулаа буув.

  • nexus.ReportSchedules, nexus.ReportGrants — хуваарь ба хуваалцах гэрээний бичлэгүүд. Engine дээр наагаагүй, тусдаа хоёр гэрээ: тэдгээрийг гурван цагт шуудангаар илгээдэг нь платформын sweep, өөр байгууллагын мөр уншихыг зөвшөөрдөг нь платформын consolidated run.
  • ReportEngine.Form — параметрийн маягт, dropdown нь дүүргэгдсэн. Dropdown нь тайлангийн зарласан SQL тул түүнийг ажиллуулах нь engine-ийнх; апп үүний тулд өөрийн өгөгдлийн сангийн бариул барьдаг байв.
  • ReportEngine.RunConsolidated — Run-аас тусдаа: нэг нь аюулгүй, нөгөө нь өөр байгууллагын мөрийг уншина, хоёуланг нэг дуудлага болгох нь тэр ялгааг арилгана.
  • ReportEngine.ValidateCron, NormalizeFormat — дэлгэц талбар бөглөх бүрд шалгадаг тул бүтэн хуваарь зохиох шаардлагагүй.

nexus.ErrReportScheduleNotFound, ErrReportGrantExists, ErrReportGrantNotPending, ErrReportGrantNotFound, ErrOrganisationNotFound нэмэгдэв — апп бүрийг өөр хариулт болгодог тул sentinel.

Changed — Тайлангийн sweep платформынх болов

Гурван цагт тайлан илгээдэг давталтыг reports апп эхлүүлдэг байсан нь дэлгэцийг суулгацын аж ахуйн ажилд хариуцлагатай болгож байв: аппыг устгасан байгууллага хуваариа чимээгүй хүлээж авахаа болино. Одоо платформ эхлүүлнэ.

Мөн internal/apps/reports нь internal/platform-ийн нэг ч импортгүй болов — domain/reports/postgres устаж, түүний бичилтүүд платформын адаптерт орлоо.

Removed — Өртөөгийн даалгаврын самбар цөмөөс гарав

internal/apps/urtuu ба domain/urtuu нь client-gerege-nexus руу нүүлээ. Гарах боломжтой болсон нь v1.11.0-ийн nexus.PeerDirectory: апп сувгийн хүснэгтийг арван дөрвөн газар шууд уншдаг байсныг гэрээ болгосон (ADR 0004-ийн Саад 2).

Суваг цөмд үлдэв — холбоос, гарын үсэг, дугтуйны дараалал, дахин оролдлого, кодын sync ба тэдгээрийн таван хүснэгт. Суулгац Өртөөний сүлжээнд байхын тулд даалгаврын самбар суулгасан байх шаардлагагүй: дугтуй ирж, шалгагдаж, хадгалагдаж, хүлээн авсан гэж хариулагдаад, тэр төрлийг уншдаг модуль компайллагдах өдрийг хүлээнэ — модулийн анхны тайлбар үүнийг бичсэн байсан.

  • /api/v1/urtuu/tasks/* арван маршрут энэ бинариас алга. /api/v1/urtuu/{peers,codes,exchange} ба /.well-known/urtuu.json хэвээр.
  • Миграц 00078 нь urtuu_tasks, urtuu_task_events, urtuu_numbers гурвыг устгана. Өгөгдөл устана; схем нь аппын хамт явж modules/urtuu/migrations/00001_urtuu.sql болов. Энэ хостын таван суулгац дээр гурвуулаа хоосон байсныг устгахын өмнө тоолж шалгасан.
  • Demo seed urtuu биш ai суулгана — явсан аппын slug юуг ч нэрлэхгүй.

[1.11.0] - 2026-08-23

Changed — Туслах апп болов

Copilot, ярианаас бичвэр, бичвэрээс яриа, орчуулга, prompt ба мэдлэгийн удирдлага — арван маршрут server.go-гоос гарч internal/apps/ai болов. Өмнө нь суулгац бүр туслахыг үүрдэг, аль нь ч устгаж чаддаггүй байсан; тэр бол энэ архитектурт апп гэдгийн тодорхойлолт (CORE_BOUNDARY_PLAN §4.2).

Суулгацад:

  • Маршрутын хаяг өөрчлөгдөөгүй (/api/v1/ai/*, /api/v1/admin/ai/*), харин одоо аппын хаалганы ард: тухайн аппыг суулгаагүй байгууллага 404 авна. Каталогт io.gerege.nexus.ai нэрээр байгаа тул дэлгүүрээс суулгана.
  • Эрх нь хоёр: ai.read (асуух, уншуулах, орчуулах) ба ai.manage (prompt ба мэдлэг бичих). Өмнө нь эхнийх нь эрхгүй, хоёрдугаарх нь requireAdmin байв — одоо ai.manage-ыг админаас өөр рольд ч өгч болно.
  • Аппын самбарт хавтан гарахгүй: манифест нь chrome бөгөөд туслах нь бүрхүүлийн чат хэсгээс нээгддэг, өөрийн дэлгэцгүй.

Added — nexus.PeerDirectory: Өртөөгийн сувгийг уншдаг гэрээ

Даалгаврын самбар нь сувгийн хүснэгтүүдийг арван дөрвөн газар шууд уншдаг байв: urtuu_peers-ыг ес удаа JOIN хийж (даалгавар, үйл явдал, дугтуй, гурван тайлан), urtuu_request_codes, urtuu_peer_codes, urtuu_deliveries-ыг тус бүр. Хажууд нь «энэ хоёр пакет бол давхаргаар хуваагдсан нэг бүтээгдэхүүн, нэг схем хуваалцдаг» гэсэн тайлбар бичээстэй байсан — тэр нь хоёул нэг репод байх хүртэл үнэн бөгөөд ADR 0004 (docs/adr/0004-a-pilot-that-did-not-ship.md) энэ аппыг цөмөөс гаргах гол саад гэж нэрлэсэн.

Одоо дөрвөн метод:

Peers(ctx, tenantID) ([]Peer, error)                        // хэн нөгөө талд байна
RequestCode(ctx, tenantID, code) (RequestCode, bool, error) // код юу гэсэн үг
CodeOpenOn(ctx, tenantID, peerID, code) (bool, error)       // тэр холбоос дээр зарлагдсан уу
DeliveryLoad(ctx, tenantID, from, to) ([]PeerLoad, error)   // тухайн хугацаанд юу явсан

nexus.Directory-гийн хэв маягаар: хуудсанд нэг уншилт, ID-г нэр болгох нь санах ойд. Таван зуун даалгаврын самбар нь давтагддаг хариултын төлөө таван зуун JOIN байсан.

Тайлангуудын бүлэглэлт нэрээр биш ID-гаар болов: нэг нэртэй хоёр холбоос хоёр мөр хэвээр үлдэнэ, өмнө нь нийлдэг байсан.

Changed — Ээлж солих PIN апп болов, нэвтрэлт нь цөмд үлдэв

Кассын дэлгэц дээр ажилтан солигдоход бичдэг богино нууц үг — хүснэгт, түгжигдэлт, хэлбэрийн дүрэм, PIN тавих дэлгэц — internal/apps/staffpin болов. Дэлгүүргүй суулгац эдгээрийг үүрэх шалтгаангүй.

Нэвтрэлт өөрөө гараагүй, гарч ч болохгүй. POST /api/v1/devices/staff/pin нь платформын session нээдэг бөгөөд session нээх нь аппын эрх биш — SDK зориудаар ийм зам санал болгодоггүй. Тэр маршрут одоо nexus.StaffCredential-ыг хэрэгжүүлсэн аппаас «энэ хэн бэ» гэж асууна.

  • Ийм апп байхгүй суулгац 404 хариулна («энэ суулгац төхөөрөмж дээр хэнийг ч танихгүй»), PIN буруу гэж хэлэхгүй.
  • Аппыг суулгаагүй байгууллага дээр PIN ажиллахгүй, буруу PIN-ээс ялгагдахгүй хариу өгнө. Энэ шалгалт нь аппын дотор: тэр маршрут төхөөрөмжийн токентой, session-гүй ирдэг тул аппын хаалга урд нь зогсож чадахгүй.
  • Уншиж чадаагүй тохиолдол нь татгалзал биш: 503, 401 биш. Өгөгдлийн сангийн доголдлыг «PIN буруу» гэж хэлбэл хүн зөв PIN-ээ дахин дахин бичнэ.
  • PUT /api/v1/admin/devices/staff-pin нь аппынх болов, staff_pin.manage (AdminOnly) эрхтэй.

Added — nexus.StaffCredential

Хуваалцсан төхөөрөмж дээрх богино нууц үгийн гэрээ. Нууц үг нь юу байх — PIN, тэмдэг, карт — бүтээгдэхүүний шийдвэр; session нээхийг зөвхөн платформ хийнэ гэдэг нь тийм биш. ErrStaffCredentialRejected нь буруу нууц үг, түгжигдсэн бүртгэл, аппгүй байгууллага гурвуулангийн хариулт — гурвыг нь ялгаж чаддаг дуудагч кассын өмнө зогсох хэн бүхэнд гурвыг нь хэлж чадна.

Changed — Холбогчийн удирдлага апп болов

Zoom, Teams, Google, Dropbox холболт бүртгэх дэлгэц, OAuth-ийн урсгал, илгээлтийн бүртгэл — server.go-гийн ес маршрут internal/apps/integrations болов. Хурлын өрөө захиалдаггүй, гадагш юу ч файлддаггүй суулгац тэднийг үүрэх шалтгаангүй.

  • Хаяг өөрчлөгдөөгүй (/api/v1/integrations/*), эрх нь integrations.manage бөгөөд AdminOnly — өмнөх requireAdmin-тэй ижил утгатай, гэхдээ одоо тенантын админ өөр рольд гараар өгч болно.
  • Суулгаагүй байгууллага 404 авна; каталогт io.gerege.nexus.integrations.
  • OAuth-ийн буцах хаяг хаалганаас гадуур: провайдер хэрэглэгчийн хөтчийг манай session-гүйгээр буцаадаг тул тэнд аппын хаалга тавих нь холболт бүрийг сүүлийн алхам дээр нь унагаана.

Рельс нь цөмд үлдэв — internal/platform/integration: менежер, провайдерын бүртгэл, OAuth солилцоо, нууцлалын түлхүүр, илгээх давталт. Хоёр зүйл түүнээс хамаарна: PDF гарын үсгийн рельс баримтаа түүгээр илгээдэг, nexus.MeetingBooker нь түүний адаптер.

Changed — httpx.DecodeLimited

Хүсэлтийн биеийг хэмжээтэйгээр уншдаг туслах нь ai_handlers.go дотор амьдардаг байсан бөгөөд платформын есөн handler түүнийг хэрэглэдэг байв. Гурав дахь хуулбар үүсэхийн өмнө httpx рүү гарлаа — энэ пакет нь яг ийм хүсэлтийн үгсийн санд зориулагдсан.

Added — nexus.QuotaGate ба nexus.Quota

Сарын квотыг модуль нэрээр нь гуйдаг болов:

rr.Use(nexus.QuotaGate("ai"))

Хязгаарыг control plane зардаг тул модуль өөрөө тоог нь мэдэх ёсгүй. Квот хэмждэггүй суулгац, эсвэл хэмждэггүй төрөл дээр бүх хүсэлтийг нэвтрүүлдэг middleware буцна — RateLimit-ийн адил, дутуу тоолуур маршрутыг унагаах ёсгүй.

Fixed — nexus.RateLimit анх удаа үнэхээр хуваалцсан төсөв болов

SDK нь модульд «суулгац даяар хуваалцсан төсөвтэй хязгаарлагч» амлаж байсан бөгөөд өнөөдрийг хүртэл нэг репликийн IP хувин буцаадаг байв: нэг gateway-ийн ард гурван реплик гурван өөр төсөв барьж, суулгац нийтдээ юу ч барихгүй. Платформын өөрийн үнэтэй маршрутууд — нэвтрэлт, poll, туслах — Redis тоолуурыг server.go-д гараар хажууд нь холбож байсан, яг тэр нь модулийн хийж чадахгүй зүйл. Одоо чадамж нь хоёуланг нь өгнө; Redis-гүй суулгац хуучин локал хувиндаа үлдэнэ.


[1.10.1] - 2026-08-23

Fixed — Модулийн схем суулгацын дараа ирвэл хэзээ ч ажиллахгүй байв

Модулийн өөрийн миграц нь ганцхан газраас — суулгах замаас — дуудагддаг байсан. Тиймээс аль хэдийн суусан аппын схем нь дараа нь модуль руу нүүвэл тэр миграц хэзээ ч ажиллахгүй: app_installations дээр «installed», маршрут нь mount хийгдсэн, харин хүсэлт бүр relation ... does not exist гэж хариулна.

Онолын биш: v1.10.0-д гурван апп энэ репогоос гарч схемээ авч явахад 00077 нь тэдний үлдээсэн хүснэгтийг устгасан, тэдгээрийг өмнө нь суулгасан суулгац дээр модулийн миграц ажиллах зам байгаагүй. Стек эрүүл асаж, бүрхүүл зурагдаж, нэвтрэлт ажиллана — зөвхөн тэр аппын дэлгэц л 500 өгнө.

Одоо applyCatalogToInstallations бүрт (асалт дээр нэг удаа, дараа нь каталогийн синк бүрт) бинарь дотор компайллагдсан модуль бүрийн схем ажиллана. Суулгацаар биш модулиар: модулийн хүснэгт бол тенантынх биш суулгацынх — runModuleMigrations анхнаасаа гүйлгээнээс гадуур байгаа шалтгаан яг тэр. Хэрэглэгдэхгүй схем бол ямар ч суулгац өөрийн үүрдэггүй апп бүрийн хувьд байдаг төлөв.

goose-ийн ачаар идемпотент; нэг модулийн алдаа нөгөөг нь нуухгүй; өгөгдлийн сан хараахан босоогүй байхад асалтыг зогсоохгүй — дараагийн синк дээр дахин оролдоно.


[1.10.0] - 2026-08-23

Removed — Цахим засаг, баримт бичиг, байгууллага цөмөөс гарав

Гурван апп — io.gerege.nexus.egov, io.gerege.nexus.documents, io.gerege.nexus.organisation — нэг өдөрт client-gerege-nexus руу нүүлээ. Цөмд үлдсэн нь тэдний ашигладаг рельсүүд: internal/platform/esign (PDF гарын үсэг), gerege (ХУР-ын клиент), eid, dan, directory. Гарах боломжтой болсон нь өмнөх PR-ууд: nexus.StateRegistry/AuditReader (#184), SigningRails/EIDSigner/DANAuthenticator (#185), ReportEngine (#186), Directory (#187).

Схем нь аппуудтайгаа хамт явав — nexus.Migrations (Үе 3) — ба 00077 нь цөмөөс арван нэгэн хүснэгтийг устгав: document_* ес, departments, organisation_people.

Суулгацад юу өөрчлөгдөх вэ:

  • /api/v1/core/departments ба /api/v1/core/people-ийн redirect устав. Тэдний очих газар энэ бинарид байхгүй болсон, харин redirect нь 308 — метод ба биеийг хадгалдаг тул PUT нь Location-ыг дагаад 404 руу орж бичилтээ алддаг, дээрээс нь үхсэн хаягийг үүрд кэшлэ гэж хэлдэг байв. Аппыг үүрсэн distribution хосыг нь өөрийн маршрутын хажууд дахин бүртгэж болно. /api/v1/core/organisation ба /me/preferences нь платформын маршрут руу заасаар байна.
  • Анхдагч апп нь distribution-ийн шийдвэр болов — platform.Options.DefaultApps. appinstaller.DefaultApps нь энэ репогийн аппуудыг нэрлэсэн литерал байсан бөгөөд сүүлчийнх нь гармагц утгагүй болсон: суулгац тенант бүрт өгөх ёстой апптай, түүнийгээ хэлэх аргагүй болно. Одоо хоосон, хоосон нь энгийн хариулт — өөрийн апп байхгүй платформ юу ч суулгахгүй. Хоосон байхад каталогийн хуучралтын шалгалт (verifyCatalogVersions) бас унтарна гэдгийг анзаараарай.
  • Demo seed-ийн аппууд documents/egov биш, reports/urtuu болов. Хуучин slug-ууд юуг ч нэрлэхээ больсон тул хоёр demo тенант хоосон sidebar-тай үлдэх байв.

Changed — Distribution-ийн модуль чадамжуудын дараа баригддаг болов

platform.Options.ExtraModules-ийн callback нь NewServer бүх чадамжаа Provide хийсний дараа дуудагдана — тэр дотор nexus.SigningRails, Link, Directory, ReportEngine, StateRegistry, AuditReader, EIDSigner, DANAuthenticator, Signer, RateLimiter, MeetingBooker. Distribution-ийн модуль конструктор дотроо тэдгээрийг гуйж чадна гэсэн үг.

PDF-ийн рельс одоо ганц түлхүүртэй — экспортолсон nexus.SigningRails. Хажууд нь *esign.Rails-ыг нийтэлж байсан нь internal/-ийн төрлөөр арын үйлчилгээний давталтыг түлхүүрлэдэг байсан: distribution нэрлэж ч, солиж ч чадахгүй, санамсаргүй устгасан Provide нь цэвэр эхэлж таван минутын дараа nil дээр panic хийдэг байв.

Added — Чадвар нэмэх нь SDK-гийн засвар байхаа болив

nexus.Provide[T](https://github.com/gerege-systems/private-gerege-nexus/blob/main/impl) ба nexus.Capability[T](). Чадвар нь өөрийнхөө төрлөөр түлхүүрлэгдсэн нэг бүртгэлд амьдардаг болсон тул distribution энэ репо сонсож ч байгаагүй чадвар нийтэлж чадна:

nexus.Provide[mydist.Pricing](https://github.com/gerege-systems/private-gerege-nexus/blob/main/myPricing{db})   // main()-д
p, err := nexus.Capability[mydist.Pricing]()   // модульд

nexus.Meetings() нэмэгдэв. MeetingBooker гэрээ 2026-08-15-нд зарлагдсан, адаптер нь тэр өдрөө бичигдсэн, авах арга нь хэзээ ч нэмэгдээгүй — зургаан хоног хэрэглэгдэх боломжгүй байсан.

nexus.ReportSink нь нэрлэгдсэн төрөл болов (өмнө нь нэргүй func(Report)). Чадвар төрлөөрөө хайгддаг тул нэргүй төрөл нь ижил гарын үсэгтэй бүхнийг нэг түлхүүр дээр буулгана.

internal/apps.Bootstrap нь есөн параметрийн оронд nexus.Platform нэгийг авдаг болов. Энэ нь internal/ тул экспортолсон гадаргууд өөрчлөлт биш, гэхдээ дээрх бүртгэл яагаад байгааг тайлбарладаг: тэр гарын үсэг 2026-08-09-өөс 08-20-ны хооронд 4-өөс 9 болж долоон удаа өөрчлөгдсөн.

Deprecated — Дөрвөн Use*

nexus.UseLink, nexus.UseDocumentFiler, nexus.UseAuditSink, nexus.UseReportSink нь Provide[T]-ийн нимгэн бүрхүүл болов. Зан төлөв яг хэвээр, Use*(nil) нь чадварыг хураах нь ч хэвээр.

Тус бүрийг nexus.Provide[<төрөл>](https://github.com/gerege-systems/private-gerege-nexus/blob/main/impl) -ээр солино:

Хуучин Шинэ
nexus.UseLink(l) nexus.Provide[nexus.Link](https://github.com/gerege-systems/private-gerege-nexus/blob/main/l)
nexus.UseDocumentFiler(f) nexus.Provide[nexus.DocumentFiler](https://github.com/gerege-systems/private-gerege-nexus/blob/main/f)
nexus.UseAuditSink(s) nexus.Provide[nexus.AuditSink](https://github.com/gerege-systems/private-gerege-nexus/blob/main/s)
nexus.UseReportSink(s) nexus.Provide[nexus.ReportSink](https://github.com/gerege-systems/private-gerege-nexus/blob/main/s)

Нэг major цикл үлдэнэ, дараагийн major дээр устана (docs/RELEASING.md §1). Ring(), Documents(), Audit(), RegisterReport() нь deprecated биш — дотроо Capability-ээр уншдаг болсон, гарын үсэг нь хэвээр.

Added — Баримт гарын үсэг зурагдах зүйлээ өөртөө авч явдаг болов

documents апп өнөөг хүртэл агуулгагүй байсан — гарчиг, төрөл, төлөв — тиймээс түүний «гарын үсэг» нь агуулгад холбогдоогүй зөвшөөрөл байв (ADR 0002). Одоо баримт файл авч явна, гарын үсэг нь түүнийг хамарна.

Гурван хэлбэр, ямар файл авч явж байгаагаас нь шалтгаална: PDF-д pades (гарын үсэг баримтын дотор ордог, энэ платформгүйгээр шалгагддаг), бусад файлд detached (SHA-256 дээрх баталгаажсан гарын үсэг), хавсралтгүй баримтад approval — 0002-ын тодорхойлсон зүйл, одоо гуравны нэг. Хэлбэрийг баримт шийднэ, дуудагч биш: сонголт өгвөл PDF-ээ сулаар зуруулах болно.

POST /documents/{id}/file (оруулах, documents.manage), GET /documents/{id}/file (татах, documents.read). Төрлийг байтууд шийднэ; зурагдсаны дараа файл хөлддөг; татахдаа digest шалгагдана.

Ёслол дуусахад юу зурагдсаныг шалгана: зам нь илгээснээс өөр digest баталгаажуулбал татгалзана. Бүртгэлд format ба covered_digest нэмэгдэв (миграц 00070, 00071).

ADR 0002-ын 3-р үе шат (eid.StartSignature устгах) цуцлагдав: хавсралтгүй баримтын зөвшөөрөл нь нэвтрэлтийн ёслол бөгөөд нэвтрэлтийн клиентийнх. Хоёр зам зэрэгцэн үлдэнэ.

Шийдвэр: docs/adr/0003-a-document-carries-what-is-signed.md.

Added — Гарын үсгийн зам нэг болж, SDK-д нийтлэгдэв

nexus.Signer — суурилуулалтын баталгаажсан гарын үсгийн зам, модуль харах хэлбэрээр. eidmongolia дээр хэрэгжсэн.

Шалтгаан нь энэ репод «гарын үсэг» гэдэг үг хоёр өөр зүйлийг нэрлэж байсан: internal/platform/esign нь агуулгын дээрх гарын үсэг үйлдвэрлэдэг бол internal/apps/documents нь иргэний баталгаажсан зөвшөөрлийг бүртгэж, signature_hash талбарт сессийн дугаар хадгалдаг байв. Хоёр дахь нь утгатай бичлэг ч баримтын агуулгад холбогдоогүй — зөвшөөрлийн дараа баримт өөрчлөгдвөл хадгалагдсан ямар ч утга түүнийг үгүйсгэхгүй.

Гэрээ нь нарийхан: digest дээрх гарын үсэг, төлөв, юу гарын үсэг зурагдсаныг шалгах. ДАН энэ гэрээний ард байхгүй — тэр нь зөвшөөрлийн зам бөгөөд апп нь аль нь болохыг хэлэх ёстой.

Баримтын бичлэг үнэн болов. documents апп нь баримтын агуулга хадгалдаггүй (мөрөнд гарчиг, төрөл, төлөв — PDF нь esign-ы өөр хүснэгтэд, холбоосгүй), тиймээс агуулгын гарын үсэг үйлдвэрлэж чадахгүй. API одоо proof талбар нийтэлж, бичлэг нь баталгаажсан зөвшөөрөл гэдгийг хэлнэ; signature_hash нэр хэвээр (клиентүүдийг эвдэхгүй) ч миграц 00069 нь баганад тайлбар бичив. Дэлгэц батламжийн сурвалжийг үргэлж харуулдаг болов — өмнө нь гэрчилгээ байвал түүнийг нуудаг байсан нь гэрчилгээг агуулгын гарын үсэг мэт уншуулж байв.

Ажиллахгүй суваг санал болгохоо болив. Гарын үсгийн цонх E-ID ба ДАН хоёрыг үргэлж санал болгодог байв — байгууллагын бодлого ч, суурилуулалт тухайн сувагтай эсэх ч дэлгэц дээр хүрдэггүй байсан. ДАН дээр тэр нь бүх deployment: амьд клиент байхгүй тул сонгоход л 503. Одоо GET /documents/{id}/signing-rails хоёр баримтыг хамтад нь хариулж, цонх ажиллахгүй сувгийг идэвхгүй болгож шалтгааныг нь хэлнэ — «байгууллага зөвшөөрөөгүй» (админ засна) ба «суурилуулалтад тохируулаагүй» (оператор засна) хоёр өөр асуудал, өөр хүнд.

Шийдвэр ба үе шатууд: docs/adr/0002-one-signing-rail.md.

Changed — Үлдсэн аппууд ч дүрмээ платформоос салгав

egov — ХУР клиент ба төрийн холболтууд одоо порт: лавлагаа, холболтын жагсаалт, асуултын түүх нь ХУР-гүйгээр, өгөгдлийн сангүйгээр ажиллана.

sso_clients — OAuth2 клиент бүртгэлийн шалгалтууд (фрагмент, wildcard, userinfo, loopback биш plain HTTP, нууц үгийн бодлого) домэйн болов. Authorization server бүтнээрээ платформд үлдэв. Энэ аппад урьд нь тест байгаагүй; татгалзал бүрийн өгүүлбэр одоо хүснэгтэд.

documents — зөвшөөрлийн гинжний дүрмүүд домэйн болов: нэг иргэн нэг удаа гарын үсэг зурдгаас урган гарах бүхэн, регистрийн дугаарыг Go-д том үсэг болгож руне-ээр тоолох шийдвэр, гарын үсгийн бодлого. Гарын үсгийн төмөр зам, PDF, retention нь байрандаа.

urtuu — Өртөө самбарын шийдвэрүүд домэйн болов: бүртгэлийн дугаарын хэлбэр, гарчиг ба хугацааг хэнээс тоолох, аль шугам өргөдөл эзэнтэй байхыг шаардах, мөчлөгийг таних, доод байгууллагын хоцорсон мэдээ дууссан ажлыг ухраахгүй байх. Суваг (холбоос, гарын үсэг, дараалал) платформд, гүйлгээт SQL адаптерт үлдэв. Энэ аппын дүрмүүдийг ажиглахад урьд нь хоёр суурилуулалт, түлхүүр, миграцлагдсан схем хэрэгтэй байсан.

Changed — Тайлангийн апп ба түүний хөдөлгүүр хоёр өөр зүйл болов

backend/domain/reports үүсэв: байгууллага аль тайланг үзэж чадах, хуваарь хадгалагдахаасаа өмнө юуг хангах, өөр байгууллагад тайлан үзүүлэхийг хэн зөвшөөрөх — эдгээр дүрэм HTTP handler-аас гарч, PostgreSQL ч, тайлангийн хөдөлгүүр ч байхгүйгээр ажилладаг боллоо. Хөдөлгүүр (internal/platform/reporting) байрандаа: тайлан ажиллуулах, экспортлох, товлон илгээх нь бүх модулийн хуваалцдаг платформ.

Апп нь өмнө нь нэг ч тестгүй байсан тул эхний алхам нь route-уудын зан төлөвийг барих гурван тест байв; тэдгээр өөрчлөгдөөгүйгээр ногоон.

Хоёр хариулт зориудаар засагдав: суулгасан аппын жагсаалт уншигдахгүй үед хоосон 200 биш 500 болов, мөн хуваалцах хүсэлт бүртгэхэд гарсан ямар ч алдааг «аль хэдийн хүсэлт байна» (409) гэж хэлэхээ болив — зөвхөн жинхэнэ давхардал 409.

Changed — Аппын дүрмүүд платформоо мэдэхээ болив

backend/domain/organisation үүсэв: сүүлчийн администратор үлдэх, нэгж өөрийн удмынхаа доор орохгүй байх, архивлагдсан эцэгтэй нэгж сэргэхгүй байх — эдгээр өгүүлбэрүүд HTTP handler дотроос гарч, pkg/nexus ч, chi ч, pgx ч импортлодоггүй пакет болов. Тэдгээрийг ажиглахын тулд өмнө нь миграцлагдсан PostgreSQL, HTTP хүсэлт хэрэгтэй байсан; одоо go test ./domain/... секундын дотор хариулна.

internal/apps/organisation нь адаптер болов: handler бүр декодлож, домэйнийг дуудаж, домэйний хариуг статус болгоно. Route, JSON талбар, HTTP статус, алдааны текст, эрхийн код, module ID — нэг ч байт өөрчлөгдөөгүй, аппын зургаан интеграцийн тест өөрчлөгдөөгүйгээр ногоон.

esign нь internal/platform/esign болов (Rails, Mount). Тэр нь 00058-аас хойш апп байгаагүй бөгөөд internal/apps/boundaries_test.go-д ганц онцгой тохиолдол шаардаж байсан шалтгаан нь зөвхөн хавтасны байрлал байв. Онцгой тохиолдлын хүснэгт одоо хоосон.

Шийдвэр ба түүний хил: docs/adr/0001-domain-first.md.

[1.9.1] - 2026-08-17

/api/v1/oauth2/consent sent already_granted: null when the user had granted that client nothing, because a Go nil slice is null and the handler set the field straight from a lookup that finds no row. The consent screen asks that list whether it includes each requested scope, so the page threw before it rendered: a relying party's very first authorization — the only kind a new client ever sees — ended at "This page couldn't load", with the grant itself never offered.

The handler sends [] now, through the same list helper the client writes already use, and the screen tolerates a null besides: a shell talks to deployments of several ages, and it should not be the half that breaks when one of them answers with an older shape.

[1.8.0] - 2026-08-17

Added — The installation ring and the platform's clock are SDK capabilities

Two things a module could only reach by living in this repository are published now, and the Өртөө app — the first caller of both — imports nothing from internal/ as a result.

nexus.Link is the Өртөө ring: enqueue a signed message for another installation, in the caller's transaction, and register a reader for what arrives. The transport stays in the platform for the same reason document filing does — one signing key, one outbox, one retry schedule and one set of peers per installation, not per app. Two apps growing their own would stop the ring being one ring the first time they disagreed about retries. What is deliberately absent: peering. Who this deployment is linked to is an operator's decision, and a module that could add a peer could arrange its own audience.

nexus.Location, with Now, Today, TimezoneName and DefaultTimezone, moved out of internal/platform/config, which now delegates to them. A module makes calendar decisions of its own — a register number carries a year, a report covers days, a schedule fires at an hour — and one reaching for time.Now() in the process's zone would put a Mongolian office's Monday morning on Sunday.

The Өртөө app took *urtuu.Service, a platform type, and used five of its methods; that was the whole of what stood between it and a repository of its own. It takes nexus.Link now.

Added — The core's shape is a test now, not a paragraph

Four apps have left this repository, and every one of those decisions was made in prose. The paragraphs are right and they were also the only thing holding the line: nothing failed when an app reached into another app's package, or when the platform reached into an app's. The split just got quietly more expensive, and the price was paid by whoever tried it next.

internal/apps/boundaries_test.go asserts the two properties that make a split possible — no app imports another app, and no platform package imports an app — and it found a real violation of each on the first run:

  • internal/platform/server.go imported the e-Government app to name egov.Rail, the shape it fills in with what this deployment is wired to. The comment said "egov names the shape, the platform answers it", which reads well and points the wrong way. The type moved to internal/platform/staterail, beside the clients that fill it in; the app imports it the way it already imports them.
  • The default-app installer test built the real organisation module. It is a test about the installer, so it registers a stub answering for that id instead.

One deliberate exception is recorded in the test with its reason: documents imports esign, because the PDF rails moved inside that app when the two store cards became one. Adding another entry is a decision, and it should feel like making one.

Changed — The commerce distribution is Gerege Business

commerce-gerege-nexus is business-gerege-nexus, the product is Gerege Business, and it answers at business.gerege.mn. The repository, the Go module path, the deployment names and the domain all moved together; the module ids did not, because an id is what an installation row, a manifest and a menu key are all keyed by, and a product changing its name is no reason for a database to lose track of an app.

Two names were deliberately left as they were, both for the same reason — they are not labels for the product, they are records of what has already happened:

  • the Postgres volume, still commerce-nexus_…, now pinned external in the compose file. Compose derives a volume name from the project, so renaming the project without pinning brings Postgres up on an empty data directory: healthy container, initdb, migrations, no tenants — a deployment that looks like it lost everything, because it has;
  • the goose_db_version_commerce table. It records what has run on that database; renaming it would show goose an empty history and have it apply the product's first migration a second time.

Nothing in the platform depends on either name. This entry is here because the next distribution to be renamed will face both.

Added — A deployment can supply its own app icon

BRAND_ICON_URL and BRAND_MASKABLE_ICON_URL join the brand: the tab favicon, the Apple touch icon and the manifest's icon all follow the first, and the second is published only when a deployment has artwork drawn for cropping.

When the brand work landed, icons were the one thing deliberately left in the image — "the logo is an address, the icons are files". That was half true. The files cannot be handed to a built image; the addresses can, exactly as the logo's is, and a deployment serving its own logo already has somewhere to put one. Nothing is resized or generated here: the platform points at what it is given.

  • Two variables, not one, because a maskable icon is not a resize. Android crops an adaptive icon to a circle, so artwork that does not bleed to the edge with its subject inside a safe zone comes out with its corners missing. Unset publishes no maskable entry at all, and the launcher crops the ordinary icon itself — visibly a crop rather than quietly wrong.
  • A supplied icon is declared sizes: "any". The deployment's file is whatever size it is and nothing here can measure it; claiming 512×512 over a 144-pixel picture is a claim the browser believes and then draws blurred.
  • The favicon moved from app/favicon.ico to public/. As a file convention Next emits a <link rel="icon"> for it unconditionally, so a deployment that named its own ended up with two icon links and which one the browser drew was a coin toss. In public/ it is an ordinary file at the same address, named in the metadata when nothing overrides it — exactly one link either way.

Fixed — Гарын үсэг зурагдсан мөр наносекундаа алдаж байсныг зассан

Гарах дугтуйн created_at нь TIMESTAMPTZ баганад хадгалагдаж, илгээхээр уншихад дахин дүрслэгддэг байв. Go-гийн time.Now() Linux дээр НАНОсекунд өгдөг, Postgres МИКРОсекунд хүртэл хадгалдаг тул буцаж гарсан мөр нь гарын үсэг зурагдсан мөрөөс өөр болж, дугтуй бүр нөгөө талд унаж байсан.

Хөгжүүлэгчийн macOS дээр цаг ихэвчлэн микросекундээр зогсдог тул харагдалгүй, CI (Linux) дээр Өртөөгийн бүх тест улаан болж илэрсэн. Хамгийн муу хэлбэр: бүх зүйл зөв харагдаж, сувагт юу ч хүрэхгүй.

payload-ыг яагаад JSONB биш TEXT болгосон шалтгаан яг энэ байсан бөгөөд created_at дээр хийгдээгүй байжээ. Одоо гарын үсэг зурагдсан мөрийг яг тэр чигээр нь хадгална (миграци 00067) — дахин дүрслэгдэхгүй тул дахин зөрөх боломжгүй. Тэр багана SQL-д ашиглагддаггүй байсан: цорын ганц уншигч нь дугтуйг эргүүлэн угсрах код.

Тест нь цагаас хамаарахгүй: наносекундтай тогтмол агшин бичиж, хадгалаад буцааж уншаад гарын үсгийг нь шалгана.

Fixed — Платформын календарь Улаанбаатарын цагаар

Агшинг өдөр болгож бууруулах бүх газар нэг цагийн бүсээр: Asia/Ulaanbaatar (PLATFORM_TIMEZONE-оор дарж болно). Хадгалалт өөрчлөгдөөгүй — багана бүр timestamptz, агшин хадгалсаар байна; энэ нь зөвхөн түүнийг ХЭРХЭН УНШИХЫГ шийднэ.

Илэрсэн алдаа. Хэрэглээний тоолуур өдрийг Go талд Format("2006-01-02")-оор гаргаж, нэгтгэлээ created_at::date буюу өгөгдлийн сангийн бүсээр хийж байв. UTC+8 машин дээр шөнө дундаас өглөөний 8 хүртэл хоёр нь зөрж, бүх тоо тэг гарч, шалтгааныг хэлэх зүйл байхгүй байсан. Консол дээр "энэ байгууллага юу ч хийгээгүй" гэж харагдана — квота мөн адил тэр тоог уншина.

Засвар нэг л газар. dbguard.Install connection бүрт цагийн бүсийг өгдөг болов. Тэр нь энэ репо дахь ЦОРЫН ГАНЦ бүх pool тохируулагддаг газар — production болон өгөгдлийн сантай тест бүр — тул хоёр дахь дуудлага мартагдах боломжгүй. Үүнээс хойш ::date, CURRENT_DATE, date_trunc бүгд платформын цагаар уншина.

Go талд: тоолуурын хуваарь (шөнө дунд), тайлангийн огнооны муж (хэрэглэгчийн бичсэн «2026-08-16»), товлосон тайлангийн cron цаг, Өртөөгийн бүртгэлийн дугаарын он, баримтын загварын {date} — бүгд config.Location()-оор.

Нэг нарийн зүйл тэмдэглэв: $1::date гэж бичвэл Postgres параметрийг шууд date гэж таамаглах тул драйвер Go-гийн бүсээр огноог бууруулж, алдаа зүгээр л нүүдэг. $1::timestamptz::date нь параметрийг агшин байхад хүргэж, бууралт нь session-ий бүсэд явна. Тестээр батлав: алдааг буцаахад унана.

time/tzdata компайлд шингээв — суурь image-д tzdata байхгүй бол LoadLocation унаж, чимээгүйгээр UTC болох байсан.

Added — «Өртөө»: платформ хоорондын үйлчилгээний хүсэлт, даалгаврын суваг

Nexus дээр суурилсан платформууд дээд/доод холбоосоор хэлхэгдэж, урьдчилан бүртгэгдсэн хүсэлтийн кодоор даалгавар доошоо урсаж, биелэлт нь дээшээ буцна. Яамнаас гарсан «хагас жилийн тооллого» агентлагт ирж, аймгуудад задарч, сумдад буугаад, биелэлт нь шат шатандаа хуримтлагдан яаманд харагдана. Их Монгол Улсын өртөө шуудангийн зарчим: захиаг өртөөнөөс өртөөнд дамжуулж, хүрсэн эсэхийг нь буцааж мэдэгддэг.

Дизайны санал docs/URTUU_PROPOSAL.md, ажиллагааны заавар docs/URTUU.md.

Гурван давхарга, зааг нь тодорхой. pkg/urtuu — гэрээ (дугтуй, гарын үсэг, статусын машин), distribution бүр ижил ойлголттой байхын тулд pkg-д. internal/platform/urtuu — тээвэр (холбоос, дараалал, retry), платформын үйлчилгээ. internal/apps/urtuu — апп (io.gerege.nexus.urtuu), тенант өөрөө суулгана. Аппыг устгасан ч замд яваа ажил алга болохгүй: суваг нь доор нь үлдэнэ.

Доод тал л холбогдоно. Дээд тал доод руу хэзээ ч гарахгүй — доод суулгац галт ханын цаана, хувийн сүлжээнд, түр унтраатай байж болно. Доод тал long-poll-оор татаж, биелэлтээ түлхэнэ. Каталог sync яг ижил шалтгаанаар pull сонгосон.

Token нь "хэн ярьж байна", гарын үсэг нь "хэн бичсэн". Хоёр өөр асуулт, дугтуй бүр дээр хоёулаа асуугдана. Гарын үсэг нь pkg/catalog-ийн signed баримттай яг ижил хэлбэртэй (created_at + '\n' + түүхий payload), golden тесттэй. Дугтуйн payload нь өгөгдлийн санд JSONB биш TEXT-ээр хадгалагдана: JSONB нь түлхүүрийн дарааллыг эмхэлдэг тул хадгалаад буцааж уншихад гарын үсэг батлагдахаа болих байв.

Хоёр талын зөвшөөрөл, устгах биш цуцлах. Дээд тал урина (24 цаг, нэг удаагийн код) → доод тал буулгаж түлхүүр солилцоно → дээд тал баталгаажуулна. report_grants-ийн урсгал. Цуцлагдсан холбоос 401 буцаана, баталгаажаагүй нь 403 — цуцлагдсан тал нь өөрийгөө "хэзээ ч байгаагүй"-ээс ялгаж чадах ёсгүй.

Кодын бүртгэлийн стандартыг бид тогтоов. ring.dgov.mn-ий утасны форматыг хүлээхийн оронд түүнийг санал болгож бичив: docs/RING_STANDARD.md — гарын үсэгтэй баримт, ETag, Ed25519. Гарын үсгийн вход нь аппын каталог болон Өртөөгийн дугтуйтай ЯГ ИЖИЛ (generated_at + '\n' + түүхий байт): нэг дүрэм гурван газар, нэгийг нь уншсан хүн гурвуулангийнхыг ойлгоно. Хэрэгжилт нь бүрэн — нөхцөлт татах, гарын үсэг шалгах, хэмжээний хязгаар, нэг муу бичлэг бүх импортыг унагаахгүй.

Гурван зүйлийг ЗОРИУДААР хийгээгүй: диск кэш (импортлогдсон кодууд өгөгдлийн санд сууна — тэр нь өөрөө кэш), хуваарьт синк (код нь хугацааны нормтой ирдэг, норм нь ажилтны хэмжигдэх зүйлийг өөрчилдөг тул авах эсэх нь байгууллагын шийдвэр), гарын үсэггүй горим (RING_PUBLIC_KEY байхгүй бол импорт огт байхгүй — шалгагдаагүй бүртгэл нь улсын бүх суулгац дээрх нормыг өөрчлөх эрхтэй баримт).

Ring нь ЭРСДЭЛ БИШ, НЭМЭЛТ: RING_BASE_URL тохируулахгүй бол платформ бүрэн ажиллана — байгууллага local. кодоо зохиож, холбоос бүрд зарлана.

Даалгавар чөлөөт текстээр үүсэхгүй. Урьдчилан бүртгэгдсэн кодоор үүснэ: код нь юу бөглөхийг (JSON Schema) болон хэдий хугацаанд хийхийг (SLA) өөрөө хэлнэ. Эх сурвалж нь ring.dgov.mn — импортлогч интерфэйсийн цаана, бодит формат тохирогдоогүй тул parsing нь TODO хэвээр (таамгаар бичсэн parser өөрийн тестээ давж, бодит цэг дээр буруу байна). Дээд тал холбоос бүрд аль кодыг нээхээ шийдэж зарлана; локал код заавал local. угтвартай.

Мөчлөгөөс хамгаална. Даалгавар бүр дамжсан суулгацынхаа ID-г origin_chain-д авч явна. А→Б→А холбоос өөрөө хууль ёсны (хоёр яам өөр өөр төрлийн ажлаар бие биенийхээ дээд байж болно) — хамгаалагдах ёстой нь холбоос биш, даалгавар. Гинжинд өөрийн ID байвал шалтгаантай буцаана.

Хоёр шугам, хоёр амлалт. Суваг нь хоёр төрлийн ажил зөөнө. Үйлчилгээ: иргэн, байгууллагаас ирсэн хүсэлт доошоо явж, ХАРИУ нь заавал буцна — хүсэгч платформын гадна байгаа тул хариугүй хаагдсан хүсэлт гэдэг нь тэр хүний асуултыг зүгээр л алга болгосон хэрэг. Албан даалгавар: дээд байгууллага доод байгууллагадаа ажил өгнө, хүсэгч гэж байхгүй.

Хоёр дахь мөр нь Go-гийн шалгалт биш, СХЕМИЙН шалгалт: үйлчилгээний хүсэлт хариугүйгээр COMPLETED болж чадахгүй (urtuu_tasks_service_has_answer). Handler дээрх шалгалт нь хүнд ойлгомжтой өгүүлбэр буцаахын тулд; CHECK нь мартагдахгүйн тулд.

Шугам нь КОДООС ирнэ, үүсгэгч хүнээс биш: ring.dgov.mn-ээс импортлогдсон код бол төрийн үйлчилгээ, дотоод код бол даалгавар. Үүсгэгч сонгодог байсан бол нэг код хоёр өөр амлалттай хоёр газар хэрэглэгдэх байв. Задаргаа хийсэн хүсэлтийн хариу нь салбар бүрийн хариуг байгууллагын нэрээр нь нэрлэн эх мөр дээр цуглана.

Албан бичиг. Даалгаварт eID гарын үсэгтэй бичиг дагалдана. Дамждаг зүйл нь ЛАВЛАГАА — баримт нь бүртгэсэн байгууллагын Баримт бичиг апп дотор үлдэж, тэндээ гарын үсэг зурагдана; холбоосоор "ийм бичиг байгаа, ингэж нэрлэгдсэн, гарын үсэг хэд зурагдсан" гурав л явна.

Бүртгэлийн дугаар. Мөр бүр хүн уншихуйц дугаартай: Д2026-00412 (даалгавар), Ү2026-01875 (үйлчилгээ). Эхний тэмдэгт нь шугам — дугаарыг харангуут хариу заавал буцах эсэхийг мэднэ. Дараалал нь суулгац, шугам, он тус бүрд. Шат бүр ирсэн ажлыг өөрийн бүртгэлд дугаарлаж, илгээгчийн дугаарыг иш татна — цаасан бичиг яг ингэж ажилладаг.

Дугаарт платформын угтвар ЗОРИУДААР байхгүй: холбоосын мөр аль хэдийн name талбартай ("Ховд аймаг"), админ handshake хийхдээ өгсөн. Суулгац тусад нь богино код зарлавал нэг зүйлийн хоёр дахь, хэн ч баталгаажуулдаггүй нэр үүсэх байв. Дугаарыг нөгөө талын нэртэй нь хамт харуулна: Ховд аймаг · Д2026-0087.

Хяналт. urtuu_tasks_total{status}, urtuu_deliveries_total{result}, urtuu_peer_last_seen_seconds{role} — тенантын label аль нь ч дээр байхгүй. Операторын консол дээр «Өртөө» гэрэл: хүргэгдээгүй дугтуй, дуугүй холбоос. Оператор агуулгыг харахгүй — миграци 00064 нь зөвхөн urtuu_peers, urtuu_deliveries-ийг нээнэ.

Тайлан гурав: даалгаврын биелэлт, хугацаа хэтрэлт, сувгийн ачаалал. reports.schedule-тэй ажиллана. Гурвуулаа ScopeFull зарлана, ScopeCounterparty зарлахгүй: даалгаврын схемд counterparty_ref байхгүй бөгөөд холбоосыг түүн мэт үзвэл нэг байгууллагад өгсөн зөвшөөрөл чимээгүйгээр бүх доод платформын харагдац болох байв.

Миграцууд 00061–00067. Шинэ орчны хувьсагчид: URTUU_SIGNING_KEY (байхгүй бол Өртөө унтраалттай, платформ хэвийн асна), URTUU_ALLOW_INSECURE_PEERS, RING_BASE_URL, RING_API_KEY.

[1.7.0] - 2026-08-15

Changed — Contacts left for the commerce distribution, and the sidebar was rearranged

Three changes with one shape: putting each thing where it belongs.

The contact register is a product's, not a platform's. Migration 00059 folded Contacts into the Directory this morning, on the argument that who an organisation is made of and who it deals with are one subject. That was half right, and the wrong half is the half that decides where code lives: departments and staff are something every organisation has; customers are something a business has. So io.gerege.nexus.contacts is an app again — built and shipped by commerce-gerege-nexus — and io.gerege.nexus.organisation is the organisation again at 3.0.0, under the name it had before the merge.

  • The version does not go back to 1.0.0. 2.0.0 was published and deployments installed it; a catalogue offering 1.0.0 to them would be offering a downgrade nothing knows how to apply.
  • The contacts table stays, for the reason applied migrations always stay: 00003 has run on every deployment in the field, and the module reading it is the same code at a different import path.
  • The grants stay. 00059 gave organisation.read/manage to every role that held contacts.read/manage; taking them back would remove a permission from roles that may have been edited since. An administrator can drop one they do not want and cannot restore one nobody told them had gone. Migration 00060 fixes the only thing that was actually wrong — the two descriptions 00059 widened to mention contacts.
  • The screens stay in the shell and moved back to /module/contacts/*, where the module now points its own menu entries. A blueprint is keyed by app id inside the platform, so a distribution cannot add to one — which is right: the platform should not carry a list of screens for products it does not ship.

The sidebar says where things are. Installed apps sat under Modules while its page is at /settings/apps, which asked somebody to hold two answers for where the same screen lives; it is under Settings now. The organisation moved the other way, up into Modules, because it is a thing you look at and edit rather than a switch that changes how the platform behaves — and its two screens, departments and people, are indented beneath it instead of listed beside the App Store as though the three were unrelated destinations.

[1.6.0] - 2026-08-15

Added — A distribution can read its own catalogue

catalog.LoadFile and the pieces under it — LoadEntries, Assemble, LoadManifest, LoadChronicleFile, ReleaseNotesFor — moved from internal/platform/appcatalog into the contract package.

The App Store found this the way these things are always found. A test wanting to check its bundled catalogue against the modules compiled beside it could not read the catalogue: the loader was in internal/, which is the rule that makes distributions possible, working exactly as designed and against the product it was designed for. The alternative was a second implementation of "how a catalogue is read" — a second answer to whether a manifest is valid, agreeing with the first until the day a deployment and the store it publishes to disagreed about an app nobody had touched.

The platform's own loader is now three lines over the public one, keeping the one thing that is genuinely this repository's: the deprecated app-id renames, which no distribution should inherit.

[1.5.0] - 2026-08-15

Added — Apps can be published to named platforms instead of to all of them

An app now declares a visibility: public, which every platform may be offered, or private, which only the platforms the registry names may be. Empty means public, so every manifest written before this marshals to the bytes it always did — the signed catalogue stays byte-reproducible across the two repositories that build it.

It is enforced by the registry, not by the platform, and that is the whole design rather than a shortcut. A private app is kept from a platform by not being in the catalogue that platform is served. The alternative — ship every platform the same document and ask each to hide what it should not see — leaks the names of private apps to everyone holding the catalogue, and asks the party with the motive to look to be the party that decides.

  • The declaration travels. visibility moved onto the manifest. cmd/publish-catalog submits exactly that struct, so a visibility that lived only on the catalogue entry was a declaration the publisher made and nobody downstream ever received. The entry keeps its field; a private declaration in either half wins, because an app hidden by mistake is a support question and an app published by mistake is not recallable.
  • An unknown value is refused. Not read as public, which would publish an app on a typo — Private, internal, restricted — and not read as private, which would hide one for a reason nobody could see.
  • A deployment can now identify itself to the registry: APP_CATALOG_TOKEN, sent as a bearer token on the catalogue request. A signature says who wrote a document; it says nothing about who should be reading it, and both matter once an app is published to named platforms. A request without it is anonymous and can only be answered with what everybody may see. Empty stays the normal case — a deployment granted no private apps has nothing to prove — and the token is never put on the query string and never written to the catalogue cache.
  • The store card says Private on an app that arrived by arrangement. Nothing else on it distinguishes such an app from one anybody can install.

The other half of this lives in appstore-gerege-nexus: which platform may see which private app, and GET /catalog answering per deployment. One trap to carry over — the ETag has to vary per deployment too, or a platform whose entitlement is withdrawn keeps its old catalogue on a 304. See docs/ECOSYSTEM_GIT_STRATEGY.md §3.1.

Fixed — Installed apps listed four that this binary cannot run

The settings screen read straight from app_installations, so it showed nine rows under its own banner saying the catalogue has five. State Services, Products, Inventory and Billing were all listed as installed and active — months after their code left for gov-gerege-nexus and commerce-gerege-nexus — each with a button offering to disable an app that was not running. Nothing about them worked: no routes mounted, no menu entry, and the compile-time check refuses them at install. The row was all that was left, and a row that says "Active" about an app with no code is worse than no row.

The store already had this rule (runnableHere, added when State Services left); the list of what a tenant has never got it. Both screens ask the same question now: if the catalogue knows the app, it has to be runnable here; if the catalogue has never heard of it, a compiled module is enough — a distribution's own module is real from the moment the binary starts and may reach a catalogue minutes later or never.

The rows stay in the database. The apps went to distributions this deployment may yet run, and deleting an installation because a screen cannot render it is the wrong way round.

Changed — Two cards became one app: Contacts moved inside the Directory

"Organisation & People" and "Contacts" were one subject cut in half — who this organisation is made of, and who it deals with. An administrator who installed one and not the other had half a directory, and nothing in the store said which half was missing. So io.gerege.nexus.contacts is gone and its register is part of io.gerege.nexus.organisation, now called Directory (mn: Бүртгэл) at version 2.0.0.

The contacts table, the API path /api/v1/contacts and the screen at /contacts are all unchanged. What moved is the app the register belongs to.

  • The contact routes assert their own permission now, and this is the part worth reading twice. They were gated by the platform, from the registered module's route prefix — and a module that mounts another package's routes lends it its own gate. The Directory declares no prefix, because it checks each of its routes explicitly. Mounting the register behind it without saying so would have turned "contacts.manage required" into "any member of the tenant", silently, in a diff about menus.
  • One permission namespace. contacts.read/manage → organisation.read/manage, one to one, carried by migration 00059 before the old codes are dropped.
  • The register arrives everywhere. The Directory is a default app, so every tenant has it — including tenants that never installed Contacts. That is the thing being fixed rather than a side effect: a directory with no outside half was the half-product this merge exists to end.
  • Migration 00059 was run against a database in the pre-merge shape — a role holding only the two contacts codes, a tenant with Contacts and no Directory row at all — and taken down and up again. The role came out holding exactly the two organisation codes; the tenant came out with the Directory installed and enabled.
  • catalog/chronicle/contacts.json is kept and marked retired, as esign.json was.

Changed — Two cards became one app: PDF E-Sign moved inside Documents

The store carried "Digital Documents & Signatures" and "PDF E-Sign" side by side. They answered one question — where are my documents and who has signed them — and nobody adopts a signature on its own; they adopt it because something has to be signed. So io.gerege.nexus.esign is gone and its rails are part of io.gerege.nexus.documents, now called Documents (mn: Баримт бичиг) at version 2.0.0.

Nothing about how a document is signed changed. The eID and HSM rails, the signature log, batch signing, stamp placement, the HSM connection and the two reports are the same code reading the same tables.

  • esign is no longer a module. It registers nothing with nexus; documents.New builds it and documents.RegisterRoutes mounts it. The classification test in internal/apps grew a third category for exactly this — a package that stops being a module has to say so somewhere, or its absence from the table reads as an oversight.
  • One permission namespace. esign.read/sign/manage → documents.read/sign/manage, one to one. Migration 00058 carries every existing grant across before the old codes are dropped, so no administrator is asked to reconstruct anything. An app with two namespaces would have made someone grant the right to sign twice, in two places, with no way to tell from the Access control screen which one the button obeys.
  • documents.sign now reaches the default manager and user roles, which esign.sign did and record signing did not. The argument was always the same one: the authority to sign is the citizen's own — PIN2 on their own phone, or a certificate proved to the HSM — and an approval chain only counts a signature from somebody it names. Withholding it only stopped people signing their own documents.
  • The API keeps /api/v1/esign. A path is a contract with every client already written against it, including the reference signing view the eID rail mirrors. What merged is the product, not the wiring. The screen did move: /esign answers 308 to /module/documents/pdf, because one app has one slug and every screen hangs off it.

The forwarding note is served by proxy.ts rather than by a page calling redirect(), and the difference is not academic: the root layout is rendered per request and streams, so a nested page asking for a redirect gets a 200 carrying a client-side instruction — fine for a browser, useless to a crawler or anything reading the status code, which is most of what a permanent move is announced to. This was shipped the wrong way round first and found by curling production. - The menu gained the PDF entries and lost a collision: two screens were called "Signature policies". One is which channel a document type may be signed through; the other, now "PDF signing rails", is which of the two PDF machines is switched on. - Migration 00058 was run against a database holding the pre-merge shape — a tenant with the PDF app and not the documents app, a manager role holding all three old codes, a report schedule naming an old key — and then taken down and up again. What the down cannot restore is which tenants had the PDF app separately; that left with the deleted rows, so a rollback gives it back to everyone holding Documents. The safe direction: an app to switch off rather than work nobody can reach. - catalog/chronicle/esign.json is kept, marked retired rather than deleted. Two versions really shipped, and removing them buys a tidier directory at the price of a record that is no longer true.

Changed — The image no longer knows its own name

lib/apiBase.ts took the deployment's address out of the build. This takes its identity out, which is the second half of the same argument: one image, a different .env, a hundred deployments is only true when neither is baked in.

Five values are read from the environment per request — BRAND_NAME, BRAND_SHORT_NAME, BRAND_DESCRIPTION, BRAND_LOGO_URL, BRAND_THEME_COLOR — and all of them are optional. Unset is Gerege Nexus, so nothing about nexus.gerege.mn changes. They reach the document title, the PWA manifest (which is what an installed copy keeps under its icon), the header, the sign-in and consent screens, the workarea footer, the operator console and the landing page's chrome.

  • The product's name became a variable in the dictionary. It was written out in nineteen entries and their overlays in five more languages, which made a rebrand a translation job in seven. t() now substitutes {brand} into every string without being asked: a sentence that names the product is ordinary prose, and requiring every call site to pass the value would mean the one that forgot renders {brand} at somebody.
  • The shell's HTML is rendered on demand rather than prebuilt. That is the price and it is the point — HTML produced by next build is a name travelling inside the image. Little was lost: every screen under this layout is either behind a session or already rendered per request, so what was being cached was the empty frame around them. The root layout is a server component now and the providers moved to app/providers.tsx; a comment there once called that split larger than metadata warranted, which it was, for metadata.
  • The mark is an address, not an import. It used to arrive as a static import with a hashed, permanently cacheable URL, which is the same thing as belonging to the build. BRAND_LOGO_URL is validated as a path on this host or an absolute http(s) URL and ignored otherwise — the value ends up in an img tag, and a mistyped environment file should cost a logo rather than anything else.
  • The API reads BRAND_NAME too, for the only two places it names the product to a person: the message shown when a verified eID identity cannot be linked, and the relying-party name eID Mongolia puts in front of a citizen approving a request. Set it in both containers or in neither.
  • Not made configurable, deliberately: the icon set, which is files rather than an address; the accent palette, which is a per-person, per-device preference in Appearance and not a deployment's to seize; and the native shells, which are signed bundles carrying their own name — the export script resolves {brand} from BRAND_NAME so no placeholder reaches them.
  • The offline page lost the product's name instead of gaining a variable. It is precached and served with no network, so nothing can tell it what this deployment is called; it now says "this app", which is true everywhere and is addressed to somebody staring at an icon they installed themselves.
  • The head tags moved from hand-written elements to Next's metadata API, which a server component can use, and one of them nearly did not survive the move: appleWebApp.capable emits only the unprefixed mobile-web-app-capable, and Safari on iOS honours nothing but Apple's spelling for a standalone launch. Written by hand alongside it, as it was before. An installed copy that came back inside Safari's chrome would have been a strange thing to trace to a refactor of the title.

See docs/ECOSYSTEM_GIT_STRATEGY.md §2.3 and §6.

Removed — State Services left, and is a product of its own

The second distribution split. apps/gov_services now lives in gov-gerege-nexus.

No composition image, and nexus.gerege.mn simply stops offering the app. That was not the plan an hour before it shipped; the plan was a repository whose only job is to build core-plus-verticals into one image for this deployment. Production answered the question instead. It carries two tenants; the app is installed on one of them and disabled there; gov_services, gov_applications and gov_appointments hold zero rows between them. A repository, a pipeline and their maintenance, to keep showing an app nobody has switched on. The ecosystem strategy tells distributions to choose the lower level when in doubt (§1), and the rule reads the same when it is pointed at us.

The distribution exists and is green, so the day somebody wants State Services it is a deployment rather than a project.

  • Gone from here: the module package, catalog/manifests/gov-services.json, its chronicle, its entry in catalog/apps.json, and its menu blueprint.
  • Migrations 00006 and 00007 stay, for the reason 00038 stayed when the App Store left: they have run on every deployment in the field, and removing an applied migration buys a tidier directory at the price of a history that no longer describes the database.
  • The frontend pages stay — app/module/gov-services/* and components/gov/*. The shell is one image serving every deployment, so it carries the union of first-party screens; without the module behind them the pages are inert, unlisted in the menu because menus are built from registered modules, and refused by the API. See docs/ECOSYSTEM_GIT_STRATEGY.md §2.3.
  • The stale app_installations row on that one tenant is harmless: the compile-time check runs on install, not at boot, so a row without code mounts no routes and lists no menu.

[1.4.0] - 2026-08-15

Removed — Commerce left, and is a product of its own

The third and last of the planned vertical splits. apps/products, apps/inventory and apps/billing now live in commerce-gerege-nexus.

As with State Services, no composition image: production carries two tenants, and products, stock_levels and billing_invoices hold zero rows between them. A deployment that wants commerce runs the distribution.

  • Migrations 00003 and 00004 stay, and the distribution re-declares the five tables rather than moving them. It could not move them: 00003 creates contacts — which the platform keeps — in the same file, and 00004 creates sessions and oauth2_clients beside the invoices. This entanglement, not the foreign keys the plan blamed, is why commerce was last.
  • Every index in the distribution's copy is IF NOT EXISTS. The originals were not: CREATE INDEX idx_products_tenant with no guard is harmless when a migration runs once against an empty database, and fatal in a history that runs against databases already carrying the platform's copy of these tables.
  • invoices_created_total registers tolerantly. During a split the module is compiled twice — once inside the platform it depends on, once in the distribution — and MustRegister turns that into a panic at init, before any logging, nowhere near the cause. A module should not stop a binary from booting because an older copy of itself was compiled alongside.
  • The classification guard now reads the directory. It compared two hand-written constants, which would have passed while these three modules were deleted, because both numbers get edited in the same breath. It now fails in both directions and both were demonstrated.
  • The frontend pages stay, for the reason they stayed for State Services: the shell is one image serving every deployment.

[1.3.0] - 2026-08-15

Added — the reporting contract and the document capability, both so a module elsewhere can use them

  • nexus.Report and the shapes around it — Querier, Rows, ParamSpec, ColumnSpec, Params, Result, plus RegisterReport and UseReportSink. A module needs the authoring contract, not the engine; parameter binding, totals, exports, schedules and cross-organisation grants stay inside the platform where they can change without moving the ecosystem's floor. internal/platform/reporting is now aliases onto these, so there is one set of types rather than two kept in step by hand.

The sink buffers, unlike UseAuditSink which drops. A dropped audit line is a gap in a log; a dropped report is a feature that silently does not exist, indistinguishable from one nobody wrote. Buffering makes the order of a distribution's main() stop mattering.

  • nexus.DocumentFiler — filing a document and following what becomes of it, available to every module including those compiled elsewhere. There is deliberately no Sign: a signature is an interactive ceremony performed by a person in front of a screen, and a module that could sign would be a module that could sign as somebody else. How many signatures a document needs is the tenant's policy, not the caller's, so it is not a field on the draft.

Documents and esign stay in the platform. The dependency profile settles it: esign reaches into the eID rail, the DAN rail, the HSM, the registry client, quota and the async runner. It is not an app that happens to live here; it is a platform capability wearing an app's clothes, the same shape as reporting.

Changed

  • invoices_created_total is registered by the billing module, not by the platform, under the same name the dashboards already use. A platform that ships a counter named after somebody else's domain has to be edited every time that domain moves. A deployment without billing does not export the series at all, which is truer than a zero that never moves.
  • products, inventory and billing no longer import anything under internal/. That is what makes the commerce split possible.

[1.2.0] - 2026-08-15

Added — a module can now state its own access policy, and book a meeting

Both are the same discovery from two directions: the platform held things a module should have held, and a module could not be moved until it did.

  • nexus.AccessPolicy — MenuPermission() and RoutePermissionPrefix(), optional, empty being a real answer rather than an omission. The platform used to hold this in two switch statements keyed by app ID, listing every app by name. A module in another repository cannot add itself to a switch in this one, and the failure would not have been a compile error: an extracted app keeps working, keeps appearing in the sidebar, and stops being gated. products, inventory and billing were in both switches, so a commerce split done before this would have removed route permission checks entirely. The switches are now assertions in internal/apps/access_policy_test.go.
  • nexus.MeetingBooker, MeetingConnector, Meeting — booking a conferencing link, as a module sees it. gov_services declared its own interface and inverted the dependency correctly, but the interface spoke in *integration.Connector and *integration.Meeting, which are under internal/. A dependency's type travels as far as the dependency does, so that module could not be compiled outside this repository — and the connector it could not do without is a fifteen-field storage record of which it reads one field. integration.AsMeetingBooker adapts the manager to the contract.
  • Both additions are additive; a module compiled against 1.1.0 keeps compiling.

gov_services now depends on pkg/nexus and nothing else from this repository, which is what makes the next split possible.

Removed — the App Store left, and is a product of its own

The first distribution split. apps/appstore_registry, apps/publisher_studio and apps/store_review now live in appstore-gerege-nexus, which takes this platform as a dependency by tag and adds nothing but three modules and the line that registers them. Every other deployment stopped carrying them as dead weight the day they left.

  • Gone from here: the three module packages, catalog/profiles/appstore, cmd/appstore-import (an operational tool for those tables), and the App Store's entries in the public-route list. That last one matters: a name on that list is a permission, and leaving /api/v1/registry/* behind would have blessed the next core route that happened to be mounted under it.
  • Migration 00038_appstore_registry.sql stays. It has already run on every deployment in the field, and removing an applied migration from the sequence buys a tidier directory at the price of a history that no longer describes the database. The tables sit unused where the App Store is not installed. New store migrations belong to the distribution, in its own goose table.
  • The distribution needs a route-policy guard of its own. It has public routes — the signed catalogue and the keys that verify it — and nothing there is checking them yet.

[1.1.0] - 2026-08-14

Added — pkg/platform, so a distribution can start the platform it compiles against

The SDK let somebody write a module and gave them nowhere to run it: booting is internal/platform.NewServer, which the language closes to every other repository. docs/ECOSYSTEM_GIT_STRATEGY.md §2.5 sketches a distribution's main.go as a call to platform.Run(); this is that function.

  • backend/pkg/platform.Run(Options) is the whole boot — configuration, tracing, the pool with the isolation guard installed on it, the invalidation bus, the server, the background sweeps, the listener, a graceful stop. It is cmd/api/main.go moved, not reimplemented, and cmd/api is now three lines calling it. That is deliberate: the platform's own binary is built the way every distribution's is, so a boot path that works here works there.
  • Options.Modules registers a distribution's own modules with the same nexus.Platform the built-in ones get, at the same moment — after the pool exists and before any route is mounted. NewServer takes it as a variadic option, so every existing caller and test kept compiling.
  • A dead listener now unwinds through shutdown. It used to call os.Exit(1) from inside the goroutine, which skipped every deferred close on the way out — the pool, the Redis client, the tracer's flush. It now hands the error back and leaves by the same door a signal does.
  • cmd/migrate accepts MIGRATIONS_DIR and MIGRATIONS_TABLE. A distribution has its own schema, and goose keeps one row per applied version in one table, so its 00001 and the platform's 00001 were the same row. Each history needs its own table. Defaults unchanged.

Added — pkg/catalog, the app-store contract, and a clean appstore boundary

Preparation for the first distribution split (gerege-appstore). The three store modules — the registry, the publisher studio and the review queue — were measured against the rest of the platform first, and the boundary turned out to be almost clean already: nothing in the core imports them, and the only thing holding them here was the catalogue schema, which lived in internal/platform/appcatalog where no other repository can reach it.

  • New public package backend/pkg/catalog carrying the schema and its validation: Manifest, CatalogApp, Chronicle, ReleaseNote, Person, ValidateManifest, ValidateChronicle, ValidateCatalog, IsNewerVersion, and the slug rule. docs/ECOSYSTEM_GIT_STRATEGY.md §2.4 names this one of three contracts that outlive the core's release cycle; it is separate from pkg/nexus because its audience is different — a registry operator or a third-party publisher, not somebody writing a Go module.
  • What stays in internal/ is anything that knows where a catalogue lives on a particular deployment: the bundled file, the disk cache, the signed fetch from a registry, and the rename table.
  • IsValidSlug moved with the schema. What counts as a slug is part of the app-store contract — a slug is a store URL segment and a manifest filename — rather than of this deployment's hardening. security.IsValidSlug forwards, so the two can never disagree about the one app that could be published and not installed.
  • Four unused interfaces deleted (CatalogRepository, PackageStorage, PackageVerifier, Installer), left over from a sketch of "the future marketplace boundary". Nothing implemented or called them, and carrying them into a public package would have frozen four shapes nobody uses into the semver promise.
  • The same import-graph guard as pkg/nexus: the catalogue contract may not reach internal/.

The three store modules now import pkg/nexus, pkg/catalog and each other, and nothing else from this repository. What still ties them here is three lines in internal/apps/runtime.go — the lines that become a distribution's main.go.

[1.0.0] - 2026-08-14

Эхний тогтвортой хувилбар: backend/pkg/nexus нь semver амлалттай нийтийн API болж, экосистемийн салгалтын 0-р (нэршил) ба 1-р (SDK) алхам дуусав. Энэ хувилбараас эхлэн distribution repo нь цөмийг fork хийхгүйгээр dependency болгон авч чадна — хувилбар гаргах журам (docs/RELEASING.md).

Added — a release process, and the tests that make its promise checkable

Step 2 of the ecosystem split. A tag is the only way another repository can depend on this one, so a tag has to mean something; this is what makes it mean something.

  • docs/RELEASING.md — the semver promise in plain terms (what breaks a caller and what does not, with the interface-versus-struct distinction spelled out because it is the one people get wrong), the backend/vX.Y.Z tag form Go requires of a module in a subdirectory, and the procedure.
  • The exported API of pkg/nexus is a golden file (pkg/nexus/testdata/api.txt, 66 lines — the whole ecosystem contract on one page). Changing it is allowed and often right; changing it by accident is not. go test ./pkg/nexus -update re-records it, and the diff is what a reviewer reads. Without this the promise would be broken not by anybody deciding to break it but by a rename during a refactor, or by a method added to an interface a distribution implements — neither of which fails a test here, and both of which fail in somebody else's build days later.
  • .github/workflows/release.yml runs on a backend/v* tag: it refuses a tag whose commit carries a different PlatformVersion, re-runs the contract tests and the suite on the exact commit being published, warms the Go module proxy, and cuts a GitHub release from the changelog section for that version.
  • .github/CODEOWNERS, one entry, for backend/pkg/nexus.
  • The deploy now stamps PlatformVersion into the image. The Dockerfile has accepted a VERSION build argument since it was written and nothing ever passed one, so every production image has told every app store it is 1.0.0 — which is what a manifest's "platform": ">=1.1.0" constraint would have been checked against.

Added — pkg/nexus, the SDK that makes a product possible without a fork

Step 1 of the ecosystem split (docs/ECOSYSTEM_GIT_STRATEGY.md §6), and the precondition for every step after it. The module contract lived in backend/internal/module.go, and Go forbids another repository from importing anything under internal/ — so the only way to build a product on this platform was to fork it, and one fork per product means every fix is applied once per product for ever.

  • New public package backend/pkg/nexus carrying the contract: Module, Dependency, PermissionDefinition, MenuDefinition, and the compile-time registry (Register, Get, List, VerifyModuleExists). It imports nothing from internal/, and a test enforces that by walking its import graph — an import that crept in would compile fine here and break every distribution.
  • All fourteen modules and the platform now import it, which is the only way to know the SDK is usable: an SDK its author does not use is one nobody has tried.
  • internal/module.go and internal/platform/appregistry are gone. Not deprecated — both were under internal/, so no caller outside this repository could exist to break, and leaving forwarding shims would have left two names for one thing.
  • A test defines a module in an external test package against pkg/nexus alone, registers it and mounts its routes: the same view a distribution repository has of this platform.

Implementations stay in internal/. The SDK is a contract, and a contract that also carried the machinery would drag the machinery into the semver promise.

The service half followed, and with it §6 step 1 is complete: a module in another repository can now be written, not merely declared.

  • JSON / Error; the tenant and caller context (RequireTenant, TenantID, UserClaims, UserFromContext, and the setters the session middleware uses); a DB handle; PermissionStore with RequirePermission; Audit and the AuditSink the platform installs at startup.
  • Platform, handed to every module's constructor. All fourteen took a *pgxpool.Pool and six built their own permission store with an internal constructor no external module could call. They now take one argument — New(p nexus.Platform) — which is also what stops the signature changing every time the platform lends modules something new.
  • The permission store is now one per process rather than one per module. It caches grants per tenant and is invalidated across replicas; fourteen of them meant fourteen caches of the same rows.
  • httpx, tenant, auth and rbac forward to the SDK rather than duplicating it. For the context packages that is not tidiness but correctness: two packages each holding their own context key would write and read different values, and the second would always be empty.
  • DB carries BeginTx so a module can open a read-only transaction — the report engine opens every run that way. It does not carry Acquire: pinning a connection is a platform capability, and offering it to every module would offer every module the ability to exhaust the pool. The one place that needs it asks by type assertion, in internal/platform/reporting.

The surface was measured rather than designed. Across the fourteen modules the whole demand on the platform was: write a JSON response (420 call sites), name the organisation and the caller (94), refuse on a missing permission (24), record what happened (41), and query the database. The report engine (40 symbols), the catalogue, the state rails and the SSO provider are each a subsystem or a specialised rail, and none belongs in the first version of a contract that cannot be narrowed later. settings, flags and emailverify are in docs/ECOSYSTEM_GIT_STRATEGY.md §2.1's sketch of this package and are not here, because no module imports them.

Added — egov, the front door to the state's systems

The last of the three naming corrections, and the only one that creates a module rather than renaming one. The pieces existed and were scattered: the ХУР registry lookups were two handlers in the platform's own route table, whether the eID, ДАН and ХУР rails were even configured was knowable only from the deployment's environment, and what had been looked up sat in the audit log with nothing pointing at it.

  • New app io.gerege.nexus.egov — "Цахим засгийн холболт" / "e-Government Link", three screens under /egov: registry lookups, the state of the three rails, and the history of what this organisation asked. In DefaultApps, so every existing tenant gets it on the next boot, and removable like any other.
  • POST /api/v1/xyp/citizen and /company moved to /api/v1/egov/* and are now behind the app gate. They were platform routes any tenant could reach with the permission; a tenant that removes this app now loses them, at both addresses.
  • Permissions xyp.citizen.read / xyp.company.read → egov.citizen.read / egov.company.read (migration 00057, with a down, renamed in place so every grant survives), plus a new egov.read for the screens.
  • PermissionDefinition gained AdminOnly. The installer decided who gets a permission by looking at the end of its code — anything ending .read went to every member of the organisation — and the two registry lookups are a .read by grammar and an administrative act by consequence. Without this the rename would have silently handed "look up any citizen by registration number" to every employee, since migration 00024 had granted the old codes to the administrator role alone.
  • Contacts degrades instead of depending. Its registry auto-fill now calls the e-Government endpoint and the button is not offered when the app is absent, rather than being offered and answering 403. egov exports a Registry interface for in-process callers; contacts is deliberately not a dependent of the app.
  • A first test of the app gate itself (app_gate_test.go). It had none: every module is mounted behind appGateMiddleware and nothing asserted what that does. Writing it found a nil-pointer dereference in NewServer that would have panicked every deployment at startup.

What deliberately did not move into the app: the eID and ДАН sign-in flows, which run before anybody is signed in, and a person's own list of linked identities with the button that unlinks one. The second is the same reasoning profile_handlers.go has carried since before this module existed — an app is installed per organisation and an administrator can remove one, and somebody's ability to detach their own national identity is not their employer's to take away. The connections screen links to /profile rather than owning it.

Deprecated — to be removed in the next release

  • POST /api/v1/xyp/citizen and POST /api/v1/xyp/company, now mounted by the app alongside the /api/v1/egov/* pair.

Changed — developer_portal becomes sso_clients

The second of the three naming corrections. developer_portal named the wrong thing twice: there is a real developer portal in this ecosystem — developer.gerege.mn, backed by apps/publisher_studio, where a third party submits an app to the store — and an administrator looking for that and landing here had nothing in the name to tell them they were in the wrong product. What this app actually is has no developers in it: CRUD over the OAuth2 clients registered against this platform's own OIDC provider, run by whoever looks after an organisation's integrations.

  • io.gerege.nexus.developer_portal → io.gerege.nexus.sso_clients, slug developer_portal → sso-clients, package internal/apps/developer_portal → internal/apps/sso_clients, type DeveloperPortalModule → SSOClientsModule. Name "Developer Portal & OAuth2 SSO" → "SSO Clients" / "SSO клиентүүд".
  • Permissions developer.read / developer.manage → sso_clients.read / sso_clients.manage.
  • Routes /api/v1/developer/* → /api/v1/sso-clients/*. Screens moved with them: /developer/apps → /sso-clients, and the blueprint screens from /module/developer/* to /module/sso-clients/*.
  • The developer.* i18n namespace became sso_clients.* across the base dictionary and all five generated overlays.
  • Migration 00056_sso_clients_rename.sql, with a down. It touches nothing in oauth2_clients: the clients this app manages are keyed by their own client_id and never carried the app's id, so renaming the screen renames nobody's integration.

Deprecated — to be removed in the next release

  • io.gerege.nexus.developer_portal as a catalogue id and developer_portal as a catalogue slug, both resolved by appcatalog/alias.go.
  • sso_clients.LegacyID.
  • The /api/v1/developer/* route tree, still mounted alongside /api/v1/sso-clients/*. A dual mount rather than the redirect used for the organisation rename: nothing moved between the platform and the app here, so both trees are the same handlers behind the same gate.

Changed — core becomes organisation, and stops being undeletable

The first of three naming corrections made before the platform is published as an SDK, where a name becomes part of an import path and stops being cheap to change. core was the name of the app holding departments and people and the name of the platform underneath every app; one of the two had to give it up, and it is not the platform.

  • io.gerege.nexus.core → io.gerege.nexus.organisation, slug core → organisation, package internal/apps/core → internal/apps/organisation. Permissions core.read / core.manage → organisation.read / organisation.manage, and the two reports it registers move with them (core.user_activity, core.headcount_by_unit).
  • The tenant's legal profile is no longer part of an app. The registered name, registration number, address, logo and parent organisation moved to the platform (GET/PUT /api/v1/tenant/profile), and so did a person's own preferences (GET/PUT /api/v1/profile/preferences). The control plane, the XYP rail and the SSO consent screen all read the organisation's registered name without caring which apps a tenant has, and none of that could depend on a screen an administrator is able to remove.
  • Editing the legal profile now requires the tenant administrator role, where it previously accepted core.manage — which the manager role also held. These fields print on documents and are what a state-registry lookup is checked against.
  • The organisation app can be uninstalled. CoreApps became DefaultApps: the list still installs the app for every new tenant, but nothing on the platform refuses to remove it any more, and the sweep that installs it no longer puts back what somebody has taken away. Nothing imports the app and no other module's foreign keys point at a department, so a deployment with no use for an internal directory — a queue kiosk, a single-purpose portal — is no longer made to carry one. Uninstalling closes the gate and drops no rows.
  • Migration 00055_organisation_rename.sql moves the id, the slug, the stored manifests, the permission codes, the report keys and the module kill-switch flag, and has a down. Grants survive: role_permissions joins on the permission id, which does not move.

Deprecated — to be removed in the next release

Each of these is marked // DEPRECATED: remove in vNEXT at its definition.

  • io.gerege.nexus.core as a catalogue id, and core as a catalogue slug. A catalogue published by a registry that has not caught up is rewritten into the new names as it is parsed (appcatalog/alias.go), and /api/v1/store/apps/core/... still resolves.
  • appcatalog.ResolveAppID, appcatalog.ResolveAppSlug and the rename tables behind them.
  • organisation.LegacyID.
  • The whole /api/v1/core/* route tree, which now answers 308 pointing at /api/v1/organisation/*, /api/v1/tenant/profile and /api/v1/profile/preferences.
  • The core field on the installed-apps response is gone already, since no app is undeletable for it to describe.

Added — Control Plane Catalog Management & Migration Deprecation (CP-46)

  • Control Plane Catalog Endpoints (/cp/api/catalog/sync, /cp/api/catalog/status, /cp/api/catalog/overview): Operators can now monitor app catalog status and trigger catalog sync on demand with step-up & audit logging.
  • Store Admin Deprecation: Added Deprecation: true and Link headers to tenant-level store admin endpoints (/admin/store/sync, /admin/store/status) to initiate graceful client migration.
  • Native macOS App Icon: Updated macOS native AppKit app to load logo.jpg dynamically at runtime.
  • Asset Optimization: Compressed login_landing_bg.jpg (~65% reduction, 1.0MB -> 360KB).
  • Code & Doc Audit: Full codebase verification, removing obsolete files and unused imports.

Added — The break-glass account, and the storage limit that refuses

The last two things the design document asked for and the phases had not delivered.

  • Break-glass (§2.4 of the plan): one emergency operator account whose password lives in a safe. It grants nothing extra — the same password and the same authenticator as everybody — and the whole of it is what happens when it is used: an ERROR log line naming who and from where, a metric of its own, a severity: page alert on that metric, and its own action in the operator audit. A locked door with an alarm on it rather than a hidden one. The database permits exactly one such account, because the second one becomes somebody's ordinary login and the alarm starts crying wolf.
  • The storage limit now refuses. CP-2 recorded it and CP-5 measured it; this is the check, at the one place on the platform where a file of any size is kept. It compares against the last nightly measurement rather than summing every blob on each upload — a storage quota is a commercial boundary, and the disk alert is what protects the platform.
  • The metering query for storage now sums esign_documents.byte_size, a column the upload already writes, rather than measuring the blobs themselves.

Added — Counting what each organisation used, from the database rather than from the metrics

CP-5, the last phase of the control plane. usage_events holds one row per organisation per metric per day, written by a job that runs nightly and again during the day, so the console is never showing yesterday's picture to somebody looking at it after lunch.

  • Not from Prometheus, deliberately. The first phase decided that no metric would ever carry a tenant label, because a label whose values are customers is a series count that only grows. The bill for that decision comes due here and is paid in SQL — and arguably at a profit, because what gets counted is acts recorded in the audit trail rather than HTTP requests, which is what a usage line should be based on in the first place.
  • Not an event per request. A row per API call is a table nobody can query by the second month. What is stored is the day's total, and re-running a day's collection rewrites it rather than doubling it.
  • Two of the five metrics are not sums, and nothing sums them. Active people is a peak — adding daily actives over a month counts the same person thirty times — and storage is a reading, where a sum would be a number that never goes down. The screen labels them accordingly.
  • The console reads usage and cannot write it. usage_events is granted to the operator role for SELECT alone, so there is no console request that can change a number a bill might rest on. That is the first question anybody asks of a metering system in a dispute, and the answer should be a grant rather than a promise.
  • The monthly AI limit CP-2 could only record is now enforced against these numbers, in middleware rather than in each of the six AI handlers, answering 429 — the allowance is spent, not the request forbidden. Storage remains measured and shown as not-yet-enforced, because refusing an upload means a check on every upload path and the screen should not imply one that is not there.

Added — A front page that answers "is the platform well", and the platform's first backup

CP-4. The console's home screen is now the deployment's health — requests, errors, latency, the government systems' lights, what is alerting, the disk, which background jobs have quietly stopped, what version is running — and the organisation list moved to its own page. Every panel links into Grafana, and none of it tries to replace Grafana.

  • This platform had no backups. Not a gap in the console: a gap in the platform. deploy/scripts/backup.sh takes a pg_dump, prunes what is older than a fortnight, and — the part that matters — writes the outcome into platform_backups whether it worked or not, because a cron job that fails silently is discovered on the morning somebody needs it. The console shows the last backup, its size, and the date of the last restore test, which is recorded by hand: an untested backup is not a backup, and the only way to know one has been tested is that somebody says so.
  • The deploy button asks GitHub, and can do nothing else. It dispatches the workflow this repository already has, with a fine-grained token scoped to that workflow, behind a superadmin capability and a second factor — and it hands back the link to the run rather than polling somebody else's API for ten minutes. No shell, no docker exec, no environment editing: the plan lists all three among the things this console deliberately does not have.
  • Per-organisation error rates come from the audit trail, not from Prometheus, because the very first phase decided that no metric would ever carry a tenant label. That decision has a price and this is it — paid deliberately, and arguably at a profit, since what an operator wants to know is whose work is failing.
  • Every panel degrades on its own. A Prometheus that is down leaves the alerts, the jobs, the backups and the versions intact, and a deployment with no monitoring stack at all gets a page that says so rather than a page of zeroes that reads as "everything is fine". NaN — what PromQL returns when it divides by nothing — is read as zero rather than reaching a screen.

Added — A platform that can be told to behave differently, without a deploy

CP-3, and the setting it exists for: this platform is private by default. Until now, whether a stranger who could authenticate somewhere else became a user here was decided by which environment variables happened to be set — EID_JIT_TENANT_SLUG in one file, SSO_CLIENT_TENANT in another, read by two packages, with no single place that answered "can somebody get in".

  • platform.access_mode. One check, at the one thing every provisioning path does — create an account — and it is closed unless somebody says otherwise. Signing in as an existing user is untouched; so is an invitation, because being invited is the registration. The sign-in screen reads the mode and explains itself rather than letting people discover it by failing, and switching to public takes effect on the next request with no restart.
  • Settings that cannot hold a secret. Every key is declared in Go with a kind, a default, a validation and the environment variable it still falls back to. There is no secret kind — not "should not be used", does not exist — and Register panics on a key that reads like one, so a credential cannot reach a table an operator can edit. A row whose key is not declared is ignored, so writing to the table is not a way to introduce behaviour.
  • Every change has a reason, a history and one button back. The rollback is itself a change: it writes a new history row rather than removing the one it undoes, because a history that can be rewound is a history somebody can edit. Values reach the running platform through a thirty-second refresh and the invalidation bus, so a change is felt everywhere at once where Redis is present and within half a minute where it is not.
  • Feature flags with an expiry that is a reminder, not a switch. Kinds (release, kill switch, experiment), per-organisation overrides, and a percentage rollout keyed on a stable hash — an organisation inside 10% is still inside 50%, and two flags at the same percentage select different organisations. Flags that have outlived their date keep working and appear as a warning on the configuration screen, which is the only thing that ever clears flag debt. A module's kill switch is a naming convention rather than new machinery: module.<app id>.disabled.
  • Maintenance and announcements. Read-only for the platform or for one organisation, refusing writes with 503 while leaving every read and the way out working. Announcements carry their own expiry and arrive with /me, so the shell shows a banner without a second thing to poll.
  • The first four settings — the access mode, the session idle timeout, the catalogue's sync interval and the AI model — are read where they are used rather than captured at startup, so changing them means changing them.

Fixed

  • Creating an organisation from the console could not have worked on a deployment with the row-level grants applied: the trigger that seeds a tenant's roles writes role_permissions, and the first administrator's account needs INSERT on users, neither of which the console's role held. Both were found by CP-3's integration tests rather than by a customer, which is the argument for narrow grants: a forgotten one fails loudly.

Added — Operating an organisation from the console, without being able to take it

CP-2 gives the console the buttons CP-1 deliberately withheld: creating an organisation, closing one, deleting one, helping somebody back into their account, and — with a reason and a banner — looking at the platform as they see it. Everything in it is shaped by one rule: an operator should be able to do their job without being able to do quiet damage.

  • Nothing is sudden. Suspension is reversible and ends every live session in the same transaction. Deletion is not a button: one superadmin asks, a different one agrees — the database refuses a self-approval with a CHECK constraint, not just the Go code — and only then does a thirty-day countdown start, cancellable throughout. The console holds no DELETE privilege on any table; a sweep on the platform path removes the rows when the time comes.
  • The console cannot change a password. It sends a link. Migration 00050 grants it UPDATE on exactly two columns of users — the lockout counter and its expiry — so a support handler that tried to write a password hash is refused by PostgreSQL. The platform had no password-reset flow at all before this; invitations and resets now share one single-use, 24-hour token, sent through the mail rail the platform already had.
  • Impersonation, made impossible to do quietly. A typed reason, the second factor again, thirty minutes, an amber banner the session itself drives, and two audit trails — ours and the organisation's own, which their administrators can read. A single-use sixty-second handover carries it across the hostname boundary, because a cookie cannot. A suspended organisation cannot be entered this way.
  • Limits. tenant_quotas records people, storage and AI calls per organisation, soft (warns) or hard (refuses). Only the user count is enforced today, because it is the only one this platform can count; the screen says so rather than implying otherwise, and CP-5's metering is what switches the other two on.
  • Creating an organisation installs its apps through the platform's own installer — the same path the store uses, with its dependency resolution — and reports which ones did not land instead of throwing the organisation away. Its first administrator gets an invitation, never a password an operator chose.
  • The four operator roles are not a ladder: operator can open an organisation and cannot look inside one; support is the other way round. The whole authorization model is one map in operator.go.

Added — A console for operating the platform, kept away from the platform

Somebody has to be able to see which organisations exist, which apps they run and what has been done to them — and until now that somebody used psql. This is the first phase of the operator console described in docs/CONTROL_PLANE_PLAN.md: the foundation, on which suspension, support and configuration are built next. Guide in docs/CONTROL_PLANE.md.

  • One binary, nothing else shared. The console is /cp/api on the same Go process and /cp on the same Next.js build, because a second service would double the deployment and the monitoring for a console two people use. It shares nothing else: its own hostname, accounts, sessions, cookie, database role and audit table. A tenant administrator's account being taken reaches none of it.
  • Three layers to reach it, none trusting another. nginx serves cp.nexus.gerege.mn behind an address allowlist that ships denying everyone; the API and the frontend both answer 404 — not 403, which would confirm something is there — to a request on any other hostname; and sign-in needs a password and an authenticator code. CONTROL_PLANE_HOST unset in production means the console does not exist, which is the safe reading of a variable nobody set.
  • The console cannot write. Migration 00049 gives it a database role of its own with SELECT on ten named tables and read-only policies to match — so "the operator sees every organisation" is a list of permissions rather than a switch that turns row-level security off. A table nobody granted is a table the console cannot read, and a new one does not become visible by existing.
  • A write that was not recorded did not happen. Every console write goes through one function that puts the change and its operator_audit row in the same transaction, and the middleware above it withholds the response — headers, cookies and all — from any write that answered successfully without an audit row. The table refuses UPDATE and DELETE at the database, by trigger, including from the role that owns it.
  • A code works once. The time step of every accepted TOTP is stored and must strictly increase, so a code read over somebody's shoulder within its thirty seconds is already spent. Verified against RFC 6238's own test vectors.
  • No sign-up screen, ever. The first operator is created by operator-bootstrap on the host, by somebody who already holds the database credentials — no default password, no first-run page, no environment variable left behind. The account cannot sign in until a code from its authenticator is confirmed, so an interrupted setup leaves a locked door rather than a password-only one.
  • Sessions last 8 hours and end after 30 minutes idle, against the platform's 12 and 90; step-up re-confirms the second factor for five minutes before a dangerous action, and is mounted on nothing yet because nothing dangerous exists yet.
  • cp_login_attempts_total{result} counts sign-in attempts by outcome, apart from logins_total: platform sign-ins fail all day because people mistype passwords, while a dozen failures against the console in an hour is somebody trying.

Fixed

  • The CSRF middleware defended the tenant session cookie and would not have defended the console's, which was introduced in the same change. Both are named in one place now, so the next cookie-authenticated surface is a line there rather than a hole.

Added — One organisation seeing another's report, with their permission

A coal mine contracts a hundred transport companies. Each keeps its trips in its own tenant; the mine wants one consolidated "Transport" report. That request runs against everything this platform is built to prevent, so the answer is not to weaken the isolation but to add a separate, permissioned path beside it. §3.5 of the design; guide in docs/REPORT_SHARING.md.

  • Nothing crosses a tenant boundary. A consolidated run calls the ordinary report once per grantor, inside that grantor's own tenant context. No policy is relaxed, no clause is rewritten, no query reads across organisations, and the report cannot tell it is being consolidated except by the counterparty reference it is handed.
  • Default deny, in three places. No grant, no rows. A report must implement Shareable to be nameable in a grant at all — a report written assuming one organisation may aggregate in ways its rows do not. A request is not a permission: accepted_at is null until the owning organisation's administrator answers, and the query that reads grants ignores anything unaccepted, revoked or expired.
  • The scope is the agreement. counterparty shows the mine the work done for the mine; full is the hierarchical case, a parent consolidating a subsidiary. A report that cannot filter by counterparty cannot be granted that scope — refused, rather than quietly widening to everything.
  • A two-sided row-level policy, which is why report_grants deliberately has no tenant_id column: the general policy from 00029 would have attached itself and hidden each party's own agreements from the other, leaving the receiving side unable to see what it had been given. Accepting is still the owner's alone — the grantor_tenant_id = $2 clause on that one statement is what stops a grantee accepting their own request.
  • Both sides are audited, every run. The reader records that it read; the owner records that it was read, and can open "who has read our data" and see the organisation, the report, the moment and the row count. A transport company will only agree to this if it can check afterwards.
  • Revoking is immediate and grants are never deleted. DELETE is not granted on the table: "who could see our data, and when" is a question asked after the fact.
  • One grantor failing does not fail the run. A hundred organisations is a hundred chances of one slow query, and the ninety-seventh timing out must not produce nothing. That company is named in the result's notes instead — a total quietly missing a company is worse than one that says so.
  • Neither shipped billing report offers counterparty scope, and that is the schema rather than a decision: billing_invoices records a contact name, not a registration number, and matching organisations by typed-in name is the mistake §3.5 avoids by keying grants on a registration number. Both offer full. The mechanism is complete and proven against a live database by the four tests §3.5 asks for, plus three more.

Added — Reports, as a platform layer rather than a screen

io.gerege.nexus.reports. Every app that keeps data now has reports; the module serving them knows about none of them. Guide in docs/REPORTS.md.

  • A report is a declaration. A module implements a Go interface saying what it is called in seven languages, what parameters it accepts, what columns it produces and how to produce them. The listing, the parameter form, the table, the chart, the Excel and CSV export, the schedule and the audit entry are written once and apply to every report anybody ever adds. Adding one is a file in a module and a line in its constructor — no handler, no frontend change.
  • Eight reports to start with: revenue by month and invoice status (billing); stock on hand and movement summary (inventory); signatures by rail and signer activity (e-signature); user activity and headcount by unit (core). The e-signature one separates the rails and marks which is qualified, because only the eID rail produces a qualified signature in Mongolian law and a report that counted both together would answer the wrong question.
  • The tenant boundary is the database's, not the query author's. A report runs inside the caller's tenant binding, in a read-only transaction, under a thirty-second statement_timeout — the SQL one, not a context deadline, because a cancelled context stops this process waiting and does not stop PostgreSQL working. A report that forgets its WHERE tenant_id returns nothing rather than everyone's rows, and there is an integration test against a real database that proves it.
  • Three gates, not one. A report belonging to an app the organisation has not installed is absent from the listing, refused by key with a 404, and refused again when a schedule names it. Filtering the list is not enough: the API is a separate door.
  • Every run and every export is audited, separately. An export is a copy of the organisation's numbers leaving the platform, which is a different act from reading them on screen — and §3.5 of the design requires both before one tenant may see anything of another's.
  • Scheduled reports, with no second process. A cron expression, a minute-ticker goroutine in the API, and a PostgreSQL advisory lock. Due rows are claimed before the report is produced, not after it is sent: a replica restarting between "sent" and "recorded" would send the report twice, and a second copy is indistinguishable from the first while its numbers may differ.
  • Delivery is SMTP, and the design document said otherwise. It called for the hosted verification service; that service sends one thing, a verification link, and has no endpoint for a subject, a body or an attachment. With REPORT_SMTP_URL unset a due schedule is still produced and still recorded, with "delivery not configured" as its outcome and a warning on the screen — which is a different and more useful state than not running.
  • Exports that can be used. xlsx via excelize with real numeric cells, a totals row and a frozen header; CSV with a UTF-8 BOM, without which Excel on Windows renders every Mongolian heading as mojibake — the whole content of the file.

Added — Traces, and errors that group themselves

The third pillar and the tool beside it, both env-gated and both off by default. Guides in docs/MONITORING.md §11 and §12.

  • SetupTracing now sets up tracing. It was a stub that logged "opentelemetry tracing initialized" and initialized nothing — worse than no tracing, because an operator reading the startup log had every reason to believe traces existed somewhere. It is now the OTLP exporter, a batch processor, and a ParentBased(TraceIDRatioBased) sampler at 10%.
  • Off means off. With OTEL_EXPORTER_OTLP_ENDPOINT unset there is no exporter, no batch processor, no background goroutine and no sampling decision: every span the code starts is a no-op. That is the condition for putting tracing in the default path rather than behind a build tag.
  • otelhttp on the router and otelpgx on the pool, so a slow request resolves into the queries it waited on. Spans are named by chi's route pattern, never the URL — a span per document id is unbounded, the same cardinality argument the metrics middleware already makes. /health, /ready and /metrics are excluded: Docker and Prometheus call them every few seconds, and at any sampling rate they would be most of what is stored.
  • Query parameters are never recorded, which is otelpgx's default and is now a comment saying it must stay that way. The arguments are the row a query is about — an address, a national identifier, a password hash on the way in — and a span is readable by anyone who can open Grafana. Verified against a live Tempo: db.query.text comes through as placeholders.
  • Logs join traces. Every slog line written inside a span carries trace_id and span_id, and Grafana's Loki datasource turns the first into a link. Deliberately not the otelslog bridge: that ships logs over OTLP, a second delivery path for something Alloy already carries to Loki. Tempo's datasource links back the other way, to the container's logs and to the RED metrics for the service.
  • Tempo in the monitoring stack — 72h retention, filesystem storage, no index over span contents. A trace is found by id from a log line, or through the span metrics and service graph Tempo generates into Prometheus.
  • GlitchTip as deploy/docker-compose.glitchtip.yml, its own stack with its own Postgres. Separate rather than a compose profile because compose resolves ${VAR:?} for every service in a file whether or not its profile is active, so a required secret there would have stopped the metrics stack from starting on a deployment that never wanted error tracking.
  • Panics are reported, with a scrubber that fails closed. chi's Recoverer printed a stack trace to stdout and nothing else; the replacement logs it with the request id, the route and the tenant, and sends an event when SENTRY_DSN is set. What never leaves: the query string (where single-use references live), cookies, the Authorization header, the request body, and the person — e-mail, name and IP are dropped, the tenant id stays so "how many organisations does this affect" still has an answer. Headers are an allow-list, so one added by a future proxy is dropped rather than forwarded. The frontend half is @sentry/nextjs with the same rules and no Session Replay: it records the DOM of what the person was looking at, which here is a registration number or a document awaiting signature.

Added — A monitoring stack that reads the platform

deploy/docker-compose.monitoring.yml: Prometheus, Alertmanager, Loki, Alloy, Grafana, node_exporter, cAdvisor, postgres_exporter, redis_exporter. Guides in docs/MONITORING.md and, for every alert, docs/RUNBOOKS.md.

  • A separate compose file, brought up as a separate project. Nothing in docker-compose.prod.yml depends on anything in it, no service in it is in a request path, and taking it down is a safe thing to do at any hour. It reaches the platform by joining the platform's own Docker network as an external one, so Prometheus scrapes gerege_nexus_backend:8080 directly rather than through a published port.
  • Alerting on the error budget, not on a threshold. The Google SRE Workbook's multi-window multi-burn-rate pattern against a 99.9% objective: 14.4× over 1h+5m and 6× over 6h+30m page, 1× over 3d+6h opens a ticket. Both windows must be over the rate, which is what stops a spike that has already ended from waking anybody and what lets the alert clear itself. Every external-system rule is additionally guarded by a traffic condition — without one, a system called twice at 04:00 with one failure is a 50% error rate.
  • Runbooks are part of the alert. Each rule carries a runbook annotation pointing at its section, and each section says what happened, what to check in the first five minutes, how to fix it and when to escalate. An alert nobody knows what to do about is an alert that gets silenced.
  • Dashboards as code, with allowUiUpdates: false. Four of them: API overview (RED plus remaining error budget), external systems, infrastructure, and resilience/volume. A panel fixed at 02:00 during an incident is worth keeping, and the way to keep it is a commit rather than a row in a volume.
  • monitoring database role (migration 00044) holding pg_monitor and nothing else — no table, no tenant row. Created without a password, because a migration is a file in this repository; the operator sets one once, and docs/MONITORING.md §2.2 is the command.
  • No secret in the repository. Grafana's password is required with no default. Alertmanager's receivers are rendered at container start from the environment, so leaving SMTP or Telegram empty genuinely disables that channel instead of producing a config Alertmanager refuses to load — and a stack that will not start because nobody has a mail server is a stack nobody installs.
  • Logs, without turning Loki into Elasticsearch. Alloy reads the Docker socket read-only and attaches container, service, level and deployment. request_id and tenant_id are deliberately not labels: each distinct value would be its own stream. They stay in the line, where | json | request_id=… finds them at query time.
  • Uptime Kuma is documented and deliberately not deployed here. A monitor on the server it monitors goes down with it. docs/MONITORING.md §9 has the instructions for running it somewhere else.

Added — The platform can now be measured

/metrics carried two series: a request count and a request duration. That is the R and the D of RED and nothing else — no saturation, no business volume, no sign that a call to ХУР or eID had gone slow, and no way to tell a breach of the in-flight ceiling from any other 503. Everything a dashboard would need was missing before the dashboards were, which is why this lands before the stack that reads it (design: docs/MONITORING_AND_REPORTING_PROPOSAL.md).

  • Saturation. The Go runtime and process collectors are asserted rather than assumed — client_golang registers both, and a test now fails if that ever stops being true. The pgx pool is exported as a collector read at scrape time: connections acquired, idle and total, the ceiling they are measured against, and the counters for acquisitions that had to wait or were abandoned.
  • Outbound calls. One histogram, external_request_duration_seconds{system,operation,status}, across ХУР, eID, ДАН, the eSign HSM, Gemini and the address-verification service. It wraps the call rather than the transport, because three of those six are reached through clients whose http.Client is private to open-gerege-core. system is a closed list and an unrecognised name folds into other, so a call site added without a constant cannot widen the label set.
  • Business volume. logins_total{method,result}, invoices_created_total, documents_signed_total{rail,result} and ai_requests_total{kind}, each incremented at the one place every path through it converges — failGoogle for one, store.markSigned for both e-signature rails. No tenant appears in any label: that breakdown is a reporting question, answered against rows that can be deleted rather than series that cannot.
  • The load shedder is visible. resilience_load_shed_total and resilience_in_flight_requests. There is deliberately no breaker gauge: the adaptive breaker the design document assumed was removed from platform/resilience before this work began, and a gauge pinned at zero would render a panel claiming every breaker is closed on a platform that has none.
  • Logs carry the request. Every slog line written while serving a request now carries request_id and tenant_id, read from the context by a handler wrapper rather than passed by hand through several hundred call sites. chi's colour access logger is gone with it — it wrote an unparseable second format into the middle of a JSON stream, named no request, and printed the raw path, which for /api/v1/verify/{ref} meant logging a single-use credential.
  • Audit events are kept. New audit_events table (migration 00043) with the 00029 tenant policy, written alongside the existing log line by audit.Record — same signature, so none of its sixty-eight call sites moved. The write is best effort and bounded at one second: an audit row failing must never fail the act it is recording, and the log line has already been written by then. user_id is text and unconstrained, because the trail has to outlive a deleted user and because the device handlers record device:<id> for an act nobody signed in for.

Added — Signing in with Google

A "Google-ээр нэвтрэх" button beside eID on the platform's own sign-in screen, off unless GOOGLE_LOGIN_CLIENT_ID is set. It is an addition, not the federation added a moment ago: SSO_CLIENT_ISSUER closes this deployment's own sign-in paths and hands the question of who somebody is to a provider, while this is one more of its own answers and closes nothing. On a deployment that does federate, the button is withdrawn along with the rest — a front door nobody manages is exactly what federating was meant to remove.

Google is an ordinary OpenID Connect provider, so there is no second implementation: the same discovery, PKCE, code exchange and RS256 id_token verification serve both, and both land on the same (issuer, subject) account resolution. What is written separately is only what differs — which cookie the flow parks in, and who is allowed through.

  • The credentials are deliberately not the connectors'. Drive and Meet already use GOOGLE_OAUTH_CLIENT_ID; the same Google project usually issues both and they may hold the same value, but inheriting a sign-in path from a document connector would mean enabling the connector quietly opened a new front door.
  • Three filters, in order. An unverified address is refused, because the address is what an existing local account is matched on and an unverified one would let anybody who can type into a Google profile claim somebody else's account. Then GOOGLE_LOGIN_ALLOWED_DOMAINS, when set. Then the account itself: with no GOOGLE_LOGIN_TENANT nobody is provisioned, so a Google identity only ever reaches an account that already exists here.
  • No id_token is kept. That cookie exists so signing out can end the session at a provider this deployment federates to; ending somebody's Google session because they signed out of this platform is not this platform's business.

Added — A deployment can now be an SSO client, not only a provider

The platform has always been an OpenID Connect provider: it could hand identities out and never take one in, so a group running several deployments had one sign-in per deployment and no way to make one of them the source of truth. This is the other half. Setting SSO_CLIENT_ISSUER makes a deployment a relying party of the provider named there — including of another Gerege Nexus — and the two halves are independent: an instance can be a provider, a client, or both, which is what a regional deployment federating upward while still issuing identities to its own installed apps needs. Full guide in docs/SSO_FEDERATION.md.

  • ssoclient, the relying-party protocol. Discovery with the issuer check that makes every advertised endpoint trustworthy, a JWKS cache that refetches on an unknown kid, authorization with mandatory PKCE, the code exchange, and id_token verification that is deliberately narrow: RS256 only — none and the HMAC family are what alg confusion is made of — with iss, aud, azp, exp, iat and nonce all checked before a claim is believed. The pending sign-in lives in a short-lived HttpOnly cookie rather than a table, because a row would be written for every click of a sign-in button including every crawler's.
  • Client mode closes the local front door. With a provider named, this deployment's password, eID and DAN sign-in endpoints stop answering and say where sign-in actually happens; the login screen becomes a hand-off. A deployment that federates its identity and also keeps its own password login has not federated anything — it has two front doors and one of them is unmanaged. SSO_CLIENT_LOCAL_LOGIN=true keeps them, as the documented way back in when the provider is the thing that is broken.
  • Signing out signs you out at the provider. /auth/logout now answers with an end_session_url on a federated deployment, and the browser follows it: the provider ends its own session and returns the person to this deployment's registered post-logout address. Without that step, "sign out" followed by "sign in" walks straight back into the still-live session upstream.
  • Accounts are keyed on (issuer, subject), never on the email address. An address is a label a provider can change, and treating one as an identity means whoever is given a departed colleague's address inherits their account. A local account with a matching verified address is adopted on first federated sign-in, which is what makes federating a running deployment possible; SSO_CLIENT_TENANT decides whether a stranger the provider vouches for is provisioned at all, and unset means refused.

Added — RP-initiated logout at the provider (/oauth2/logout)

The discovery document has advertised end_session_endpoint since it was written, and nothing served it: a relying party that ended its own session and sent the person here — which is what a conformant client does — landed on a 404 while staying signed in. That is worse than not advertising it, because the next click on "sign in" looks like the logout was ignored.

  • post_logout_redirect_uris is a new column on oauth2_clients (migration 00041), editable from the developer portal and matched exactly. It is not redirect_uris reused: a sign-in callback is a machine-read path that receives a code, a post-logout address is a page a person looks at, and one list would widen both whenever either was extended. An unregistered return address is refused rather than followed — a logout URL is one a client hands out freely, so following one unchecked would make the provider an open redirector.
  • The client is resolved from client_id or from a verified id_token_hint. An unverifiable hint is ignored rather than refused: by the time it is read the person is already signed out, and failing there would strand them.

Fixed — Two defects the new tests turned up

  • A client registered with no post-logout addresses failed to insert. A nil Go slice is sent as SQL NULL, and every array column on oauth2_clients is NOT NULL with an empty-array default — a default that only applies when the column is left out of the statement, and these are listed explicitly. Every array is now normalised at the store boundary.
  • HTTP Basic client credentials were not URL-decoded. RFC 6749 §2.3.1 has a client form-urlencode both halves before base64ing them, so a conformant client's secret arrived escaped and was compared, still escaped, against what was registered. It never bit in practice because the secrets this provider mints are hex, but it would have bitten the first integrator who chose their own. A value that does not decode is used as it stands, so a client that skipped the encoding is not refused over a disagreement about transport.

Without this, single sign-on is not single. A relying party signing somebody in sends the browser to /oauth2/auth, which is a top-level navigation arriving from another site, and a Strict cookie is not sent on one — so the authorization endpoint saw no session and showed a login screen to somebody who had signed in a minute earlier. It costs nothing in CSRF terms, because the cookie was never the defence: Lax adds exactly one thing over Strict, a cross-site top-level GET, and every state-changing request goes through security.CSRFMiddleware, which demands positive evidence that a page of ours made it.

Fixed — A lockout that never let go, and three silent truncations

  • A lapsed login lockout re-locked the account on the next single failure. Five bad passwords lock an account for fifteen minutes, but the counter that decides that was only ever reset by a successful sign-in. Once it had reached five it stayed there, so after the window passed the next mistyped password met the threshold on its own and locked the account for another full fifteen minutes — indefinitely. Two consequences: nobody who had been locked out once could afford to typo again, and anybody who knew an address could hold it shut with one request every quarter of an hour. The count now restarts when the lock it produced has expired, and the lapsed lock is cleared by the same statement rather than left asserting a lockout that is over. Reaching five again still locks, so this is a restart and not a way out. The statement moved to a named constant behind recordLoginFailure; all of the behaviour is in the SQL, so the three tests that come with it need a real schema (AUTH_TEST_DATABASE_URL), and CI fails if they skip.
  • A tenant's menu could lose apps it had installed. GetEnabledAppIDsForTenant discarded the per-row scan error and never checked the stream error, so a read that broke partway reached the caller as a short list with a nil error — and a broken stream leaves rows.Next() returning false exactly as a clean end does. That list is what the menu is built from, so the apps that fell off it read as ones the organisation had never installed. Its neighbour GetInstallationsForTenant already did this correctly.
  • The AI copilot could state a truncated search as fact. Its product and knowledge tools dropped the same two errors, and there the truncation becomes a sentence: the model presents whatever it is handed, so half a result set is "you do not stock that" rather than an error the person can retry.

Removed — Code that had stopped being reachable

  • oauthError.Error made the type satisfy error, but it is a carrier — the code and description are rendered into an RFC 6749 §5.2 body and it is never wrapped or unwrapped — so the method was unreachable.
  • issueTokenSet kept the token SaveToken hands back only to discard it with _ = stored; SaveToken returns the same pointer it was given.
  • The integrations screen still rendered an error paragraph from a state nothing had set since the page moved to the banner, and the warehouses screen imported useMemo without using it.

Changed — The platform's apps stop calling themselves examples

io.example.* was placeholder vocabulary from the first week — the reverse domain of nobody, borrowed the way example.com is borrowed — and it had been the primary key of every app in the store ever since. These are Gerege Nexus's own apps and they now say so: io.gerege.nexus.*.

  • A rename of a primary key is a data migration, not a search and replace. 00035 moves apps, app_installations, app_versions and app_dependencies, and rewrites the id inside each stored manifest — the copy an upgrade compares against to decide whether a new version asks for more than the installed one. Both foreign keys are ON UPDATE NO ACTION, so they come off and go back on around the update.
  • The registry carries the matching migration and is deployed first. Between the two deployments an instance can sync a catalogue that already carries the new ids and file them as apps it has never seen; 00035 folds those back into the rows that hold the tenant's history — including an installation somebody made in that window — rather than colliding with them and failing the deployment.
  • The migrations before 00035 are left as they were. They already ran everywhere, and a fresh database is expected to seed the old ids and then arrive here, which is what makes the migration equally true of a database created yesterday and one created next year. The entries above this one keep the ids they shipped with, for the same reason this file keeps the old repository name.
  • mn.example.hrms in the test fixtures stays as it is: it stands in for somebody else's app, and there example is the point.

Added — An organisation to be about

The module Odoo calls base, as a core app: the organisation itself, the people in it, and how it is arranged. The platform had tenants, users and memberships carrying only what signing somebody in needs — a slug, a name, an email. A document that has to print a registration number, an approval that has to name a department, a deadline counted in some timezone: none of those had anywhere to come from, so each app either invented its own or went without.

  • Three screens — /organisation (legal identity, address, contact, and the defaults everything else inherits: timezone, locale, currency), /organisation/people (the directory, with job title, department and roles), /organisation/departments (the structure as a tree, with a manager per unit).
  • The split follows Odoo's, because the distinctions it draws are real: res.company → tenants + tenant_profiles, res.users → users, hr.employee → memberships, hr.department → departments. A language preference belongs to a person and follows them between organisations; a job title does not — the same person can be a director in one tenant and a clerk in another.
  • What the schema refuses rather than checks. A department whose parent or manager belongs to another organisation is unrepresentable, not merely rejected: the foreign keys are composite over (id, tenant_id). A tenant without a profile is impossible — a trigger creates one with the tenant, so no reader needs the null check. Both new tables carry the same forced RLS policy as everything else; migration 00029 wrote those once, over the tables that existed then, and a table added later has to say so itself.
  • What the handlers refuse: deactivating yourself, and deactivating the last administrator. Both are support tickets otherwise. Nobody is deleted — a membership is referenced by everything the person did here, so people and departments are deactivated and archived instead.
  • Editing is partial by design. The form sends the fields it touched and the server merges field by field, so correcting a phone number cannot blank a registration number.
  • Being core means two things the store now honours: every tenant has it whether or not anybody installed it, and nobody can disable it. Settings → Apps says so where the Disable button would be, rather than offering one whose only outcome is a refusal.
  • A module with no blueprint no longer goes unlisted. The sidebar was built only for apps named in menu/blueprints.go — the list of screens still to be built — so an app that had built everything it meant to build contributed nothing, including the menus it registers itself. Core walked into exactly that: three working screens and nothing pointing at them.
  • The registry imports the bundled catalogue on every boot rather than only when it is empty. Otherwise a platform app added later reaches nobody: the registry is long past empty, the import is skipped, and every instance polls a catalogue without it.

Added — The App Store moved to appstore.gerege.mn

The catalogue now comes from a registry of its own, and the apps in it can be published by people who do not work here.

  • A registry service (backend/cmd/appstore) serving a signed catalogue every instance pulls: Ed25519 over the raw bytes of the apps array, an ETag so an unchanged catalogue costs a 304, and the document built once per revision and stored as the bytes that were signed — rebuilding per request would hold only for as long as Go's encoder is byte-stable, and the failure when it is not is silent everywhere at once. It shares the platform's appcatalog types with the client that reads it, and a test signs a catalogue the way the endpoint does and feeds it to that client.
  • A storefront (appstore.gerege.mn) that needs no account: server-rendered, seven languages as path segments, real 404s, and no install button — installing happens inside an organisation's own Nexus, so every page says that instead.
  • A publishing console (developer.gerege.mn) where a publisher registers, submits a manifest and watches it through review. The authorization code is exchanged server-side and the identity token lives in an httpOnly cookie, so no token reaches page JavaScript and the platform needs no new CORS origin.
  • Installations follow the catalogue on their own, unless the new version asks for more than the installed one — a widened permission, a widened OAuth scope, or a launch URL that has moved to another host. Those are held at the version they are on, with what they added recorded, and offered to the tenant's administrator as a decision rather than a button.
  • catalog-sign generates the signing pair and signs a catalogue offline, for an air-gapped operator or for testing a client with no registry running.
  • The OIDC endpoints at the root of nexus.gerege.mn are routed to the API. Only /oauth2/token ever was, which was enough for the platform's own screens and for nothing outside it.

Added — Preparing the App Store to live at appstore.gerege.mn

The catalogue is on its way out of this repository and into a registry of its own (docs/APPSTORE_SEPARATION_PLAN.md). Everything here works today in file mode, which stays the default and the whole story for a self-hosted deployment; the registry is opt-in and this platform never depends on it.

  • An installation's version now moves. InstallApp's reinstall branch updated status and enabled and left installed_version alone, so a tenant sat on 1.0.0 for ever while the catalogue carried the app forward — nothing could tell a current installation from a stale one. A version that actually changes is recorded as 'upgraded' with the version it came from, and SyncCatalog finally writes app_versions, the table migration 00002 created and nobody ever filled.
  • Three records of a version are held to each other: the compiled module, the catalogue entry and the manifest. They had drifted — esign shipped 2.0.0 as a module and 1.0.0 in the catalogue, and the developer portal did the same. Both are corrected and the drift is now a startup error. PlatformVersion became a var a release build can stamp with -ldflags, and /health reports it.
  • A tenant can update an app it has already installed. POST /api/v1/store/apps/{slug}/upgrade (admin) re-resolves dependencies, moves the version, records the event and refuses with 409 when there is nothing to move to. The store answers with installed_version, latest_version and update_available, compared as semver rather than as text, and the card carries an Update button beside enable/disable. Migration 00033 adds auto_update and pinned_version.
  • The catalogue can come from a registry (APP_CATALOG_URL): fetched with an ETag, verified against APPSTORE_PUBLIC_KEY before a single field of it is read, cached to disk, and behind all of it the bundled file. Boot never fails because of the registry — an unreachable or lying one costs an instance its updates and a line in the log. CATALOG_SYNC_INTERVAL drives a background refresh; POST /api/v1/admin/store/sync runs one on demand.
  • An app can be a platform that runs somewhere else ("type": "external"). No Go module is required or looked for, permissions come from the manifest, and its menu entry opens in a new tab rather than pretending to be a route here. Its OAuth2 client is gated by installation: a user whose tenant has not installed the app is refused at /oauth2/auth with access_denied, and tokens carry tenant_slug beside tenant_id so the third party knows which organisation it has been handed.

Added — Switching between the organisations you belong to

  • The membership table always allowed several; the runtime allowed one. Which tenant a session acted for was decided at login by whichever membership was oldest (internal/platform/auth_handlers.go), and nothing could change it afterwards — signing out and back in landed the same person in the same tenant, deliberately, so somebody working for two organisations could reach only the first. GET /api/v1/auth/tenants lists the ones they may act for and POST /api/v1/auth/switch-tenant moves the session to one of them.
  • The token is rotated, not the row updated. A session token is the authority to act inside one tenant, and the tenant is what changes; the new session inherits the old one's expiry, so moving between two organisations cannot be used to keep a session alive without signing in again. The membership check lives in the store, where no route can reach the insert without it, and a tenant the caller is not in answers 403 rather than 404 — whether it exists is not their business.
  • Both queries deliberately leave the tenant behind (tenant.Without): memberships carries a tenant_id and is under the row-level policy, so a request bound to the current tenant would answer "which tenants may you act for" with the one the caller is already in.
  • The brand mark is the control, and the account menu carries the same list — the mobile shell hides the header brand below 900px, and a phone is exactly where somebody moves between two organisations. Both render one component over one cached answer, so the two cannot drift and opening the second does not re-ask the server. The mark used to link to /apps, which the Platform tile beneath it in the rail still does. Choosing another organisation reloads the shell rather than patching state: the menus, the permissions and every list on screen belonged to the tenant just left.
  • The demo seed now has two organisations (cmd/api/seed.go): Demo Corporation, with contacts, products, inventory and documents, and Demo Trade LLC, with contacts and billing. One tenant exercises nothing — the switcher, the row-level isolation and the per-tenant permission set all behave identically on a single-tenant deployment and identically wrongly if they are broken. The seeder runs after the platform server is built rather than before it, because an installation row references the apps table that the catalogue sync fills, and it installs through the installer so a demo tenant cannot claim an app whose Go module is not in the binary.

Removed — The Swift macOS client (desktop-mac/)

  • The bundle was a WKWebView pointed at the web client, plus a menu bar, Touch ID and a preferences window for the two server URLs. It was built by a shell script outside CI, so nothing compiled it on a pull request and nothing caught a Swift file that no longer built until somebody ran make build-mac by hand. make build ran it, which meant a build of this repository failed on any machine without Xcode.
  • What it offered over the browser, the browser now offers: the web client is installable as a PWA and gets its own dock icon and window from /manifest.webmanifest, with no download and no store. The README section that documented make build-mac / make run-mac says that instead.
  • The API keeps the path a native client would use — bearer tokens, no ambient cookie — so this is a client leaving, not the platform closing a door. Anyone wanting a native macOS app can build one against the same API in its own repository, where it can have a real build and a real signing identity.

Removed — the Swift macOS shell, in favour of one shell for all platforms

  • desktop-mac/ is gone. It was the reference implementation of the bridge contract and it did its job: the contract exists because that shell was written first and the second one had to meet it. But once the Tauri shell shipped, macOS had two applications doing the same work, and the Tauri one had outgrown it — native sign-in, session restore, and a menu built from the tenant's own menu rather than a hand-written list. Keeping both meant implementing every contract change twice and running two CI workflows to prove the same thing.
  • What is actually lost is the NSToolbar, which Tauri cannot draw. Its contents survive elsewhere: the app shortcuts are in the native menu, search is ⌘/Ctrl+F, reload and preferences are menu items, and the server status moved to the tray icon's tooltip when the Tauri shell was written.
  • make build-mac / make run-mac are now make build-desktop / make run-desktop, and .github/workflows/desktop-mac.yml is removed — the three-platform Tauri workflow already covers what it checked.
  • The entries below that describe desktop-mac are left as written. They record what shipped at the time, which is what a changelog is for.

Fixed — the Tauri shell's bridge was dead on arrival

Found by running the app and signing in — none of it was visible to cargo build, clippy -D warnings, cargo test, or the three-platform CI, all of which stayed green throughout.

  • The work area could not reach the shell at all. Tauri's ACL grants app commands to local pages but nothing to a remote origin, and the capability that was supposed to grant them was never listed in tauri.conf.json, so it was silently ignored. Every bridge call was rejected, the rejection was swallowed by a catch, and the request sat until its 40-second timeout. Both halves are now explicit, and the work area is granted only the three bridge commands — sign-in and preferences stay with the shell's own windows.
  • Defining any permission closed the door on the local windows too: once an ACL exists, every app command is subject to it. The shell's own commands are now listed as well.
  • Every app appeared in the menu bar as "Модуль": the submenu was named after the first row the server returned, and the server returns a pathless group header first. A menu bar wants the application's name.
  • A request made before the work area finished loading hung until it timed out, because the injected script it evaluates into did not exist yet. The script now announces itself, and the bridge waits for that.
  • The shell asked for a password on every launch although the session cookie outlives the process in the webview's store; it now checks /api/v1/auth/me first — and, having restored a session, navigates to the work area instead of leaving the person on the sign-in landing page.
  • The health check was really a port check: it polled /healthz, which this API does not serve, and counted the 404 as healthy. It now polls /health and requires a 2xx.

Added — Tauri v2 desktop shell (desktop-tauri/)

  • A second implementation of one contract, not a second product. The bridge contract (docs/SHELL_CONTRACT.md) is the specification; desktop-mac/ is its Swift reference and this is the cross-platform one. Both inject the same window.GeregeShell, so the web app cannot tell them apart — it hides its own chrome and renders as a work area either way, and in a browser neither exists and nothing changes.
  • platform comes from the build target (macos, windows, linux) and reaches the styling as <html data-shell>. Declared capabilities are notify, badge, external.open, print.system, fs.save, menu.native.
  • Native sign-in window with email/password and both eID flows. The polling loop is Rust (auth.rs) and carries the same reasoning as EIDLogin.tsx: one check in flight at a time, a 400 ms gap between them because the server already holds each request for 25 s, three tolerated failures because a dropped long-poll is ordinary on a mobile network, and a 15-minute backstop that is a stop condition rather than a deadline. The QR is rendered to SVG in Rust so the window depends on no JavaScript library.
  • The session cookie forced the transport. session_token is HttpOnly and belongs to the API origin, the web app authenticates with credentials: "include" and no bearer header, and neither Tauri nor wry can write a cookie into a webview from outside. The only way it lands in the right jar is for that webview to receive the Set-Cookie itself, so the sign-in requests are issued there (bridge.rs) while the flow logic stays in Rust. The work-area window is created hidden and stays hidden until sign-in completes.
  • The native menu is the tenant's menu. GET /api/v1/menus with the Accept-Language the person chose, grouped per app; menu.changed rebuilds it; choosing an item emits shell:navigate so the work area routes without a full reload. macOS maps a small set of icon names to native symbols and leaves the rest bare, which is steadier than half-matching them.
  • Server health lives on the tray icon, checked every 5 s the way ServerManager.swift does it. Tauri has no native status bar and drawing an HTML one under the work area would put the shell inside the page it is supposed to stay out of. Being offline opens a native window that says what has to be running, not an alert that vanishes when dismissed.
  • Security: main-frame navigation is confined to the Web URL's origin and everything else opens in the system browser; the bridge is main-frame only and the remote origin allowed to reach IPC is pinned in capabilities/ (desktop-tauri/src-tauri/capabilities); every native→web value is JSON-encoded rather than concatenated into JavaScript; external.open accepts only http, https, mailto, tel; fs.saveAs writes only where the person pointed. In a release build the API and Web URLs are compile-time constants — an installed shell cannot be aimed at a server it was not built for.
  • gerege:// deep links resolve to shell:navigate.
  • Auto-update is present and deliberately inert. The plugin is left uninitialised with TODOs in three places; an updater carrying no signing key is a mechanism for installing unsigned code, so it stays off until a key exists.
  • Two capabilities are withheld, and why is recorded. secure-store has no method in contract v1, so advertising it would be a claim nothing can act on — using it needs secure.get/set/delete added to the contract and a minor version bump. biometric.authenticate exists in the contract but Tauri's biometric plugin is mobile-only, so the capability is not declared and the call is rejected, which is what lets the web app fall back.
  • Not included: installers and code signing. The shell builds; shipping it needs a Developer ID identity plus notarisation on macOS and an Authenticode certificate on Windows, both listed as TODO in desktop-tauri/README.md.

Added — CI for both desktop shells

  • desktop-tauri.yml builds on Linux, Windows and macOS. Much of the shell sits behind #[cfg(target_os = ...)], so a green build on one machine says nothing about the other two. It runs cargo clippy --all-targets -- -D warnings, cargo build --locked and cargo test --locked, with fail-fast: false because one platform failing is the signal the job exists to produce.
  • It found a real break on its first run: tauri-build needs icons/icon.ico to generate the Windows resource, and the repository had only PNGs. Added icon.ico — sizes below 256 packed as classic DIB entries, since some resource compilers reject an all-PNG .ico — and icon.icns for macOS bundling.
  • desktop-mac.yml compiles the Swift shell and then checks the two things a successful swiftc cannot: that build.sh still names every file under src/ (a source missing from that fixed list is not a compile error — it is code that silently never ships), and that the produced bundle is one macOS would launch (Info.plist, an executable Mach-O, codesign --verify --strict).
  • The bridge fixes are guarded, not just documented. The job fails on a WKUserScript injected into subframes or on JavaScript built by string interpolation — the exact two shapes that were removed. Both guards were checked against the pre-fix sources to confirm they actually catch them rather than passing vacuously.
  • Neither workflow produces a distributable artifact, and both are filtered by path. A path-filtered workflow reports no status on runs that miss its filter, so making either a required check needs a merge queue or a companion job.

Added — Native Shell + Web Work Area

  • The web app now knows whether it is a whole product or part of one. Inside a native shell, sign-in, the header, the menus and device access belong to the shell; the web app hides its own chrome and renders as a work area. In a browser there is no shell, and everything below evaluates to nothing — the browser rendering is unchanged to the pixel, which is the constraint the whole design is built around rather than an afterthought.
  • One contract, written down (docs/SHELL_CONTRACT.md): injection rules, every method's parameters, result and failure, every event's payload, the capability names, the versioning rule (adding is minor, changing is major, and the shell announces its own version), and the security requirements a shell must meet. Two shells written by different people meet here or not at all.
  • window.GeregeShell in TypeScript (frontend/lib/shell.ts): getShell() returns null during SSR and in a browser, hasCapability(), a useShell() hook, and invokeShell() — an invoke that neither throws nor hangs, because callers mostly need to know whether the shell took the request, and "not supported", "failed" and "never answered" all mean the same thing: run the web fallback. Method, event and capability names are constants, so renaming one is a compiler error rather than a silent no-op.
  • Chromeless rendering (Layout.tsx): in a shell the top bar, sidebar, mobile tabs and drawer are not rendered at all, but the menu and user fetches still run — RBAC and access checks depend on them, and only the drawing is removed. The AI assistant stays; it is part of the work area.
  • Session expiry asks the shell first. There is no web /login page inside a shell, so a 401 calls auth.reLogin and falls back to router.push("/login") only if the shell will not, cannot, or does not answer — attempted once per session, so a re-login that leaves the session invalid cannot loop.
  • The two halves talk over the contract, not over URLs. A menu change tells the shell with menu.changed so it can rebuild its native menu; the shell moves the work area with shell:navigate (internal paths only — a protocol-relative //host is not one) and opens its search with shell:search.
  • Native-leaning styling, scoped by attribute (theme.tsx, globals.css): the shell's platform lands on <html data-shell>, which switches the app to the host's system font stack and chrome-free spacing, with a few per-platform touches. The attribute is absent in a browser, so no rule can reach it. The block sits above the density rules on purpose — a person who chose "compact" must not have it overruled by being in a shell.

Fixed — Security: the macOS shell's JavaScript bridge

  • Native results were concatenated into JavaScript. The biometric callback was assembled as onBiometricResult('\(cb)', \(success), '\(err)'), so a single quote anywhere in a system error message ran as code in the work area. The toolbar search field went the same way, which made anything the user typed a script. Every native→web value is now JSON-encoded and returned through one entry point (WebViewController.swift).
  • The bridge was injected into every frame. WKUserScript is now main-frame only, and each message is checked twice — isMainFrame, and that the frame's origin matches the platform's web origin. An embedded third-party page has no business reaching biometrics, files or notifications.
  • The main frame could navigate anywhere. It is now confined to an explicit allowlist — the web and API origins plus named identity origins — and every other address opens in the system browser rather than beside our session and our bridge. Deployments whose integration consent screens must stay in-app can name those origins in gerege_nav_allowlist; unlisted ones continue in the browser rather than breaking.
  • No functional regression: tray, toolbar, printing, downloads and gerege:// deep links continue to work, and deep links and menu items now move the work area through the router instead of reloading it, which no longer discards a half-filled form.

Added — Email verification as a platform capability

  • One flow instead of one per app (internal/platform/emailverify): proving that somebody controls an address is not one module's business. Contacts wants it before it trusts an address, Documents before a signing link leaves for an outsider, Gov Services before it answers a citizen at one. Each is the same act, so it lives in the platform: an app module takes the service in its constructor the way gov_services takes the integration manager and calls emailverify.Service.Send with its own app id as the source.
  • The mail is sent by a hosted service, deliberately. Delivering mail that arrives is not a matter of holding an SMTP password: it is SPF, DKIM, DMARC, reverse DNS and a sending reputation, maintained continuously. enigma.mn runs that, so this platform holds no mailbox credential, composes no message and owns no sender address. What stays here is what only this platform can know — which module asked, for whom, why, and whether the person came back.
  • The return is good exactly once (migrations 00026, 00027): the request carries a single-use reference in the return address, stored as a SHA-256, and claimed by one conditional UPDATE. A browser reloading the landing page races itself, and a reference that travelled through a mailbox and a browser's history must not be replayable. A spent, expired or invented reference is 410 alike.
  • The platform is not an open redirector: the onward destination is validated when the request is made — HTTPS only (HTTP tolerated for localhost outside production), no embedded credentials — not when somebody arrives, by which time the mail has gone.
  • Mail bombing has a cost: a per-tenant hourly allowance in front of the shared key and a one-minute pause per recipient, answered 429 with a Retry-After somebody can obey — a limit we can avoid provoking upstream is one we do not have to explain. A request the service refuses withdraws its own row, so the Overview screen never shows a verification nobody was asked for.
  • Errors say who has to act: a bad address is 400, a missing key or an HTTP PUBLIC_ORIGIN or a rejected key is 503 (this deployment, not the request), and a failure at the service is 502 and retryable. An answer nobody documented is never read as success.
  • Settings → Email verification: whether the service is reachable, what has been asked for and by whom, the verified rate, and a test send. No key management — keys belong to the sending service and are administered there, and this platform's copy is a server-side environment variable that never reaches a browser.
  • The page shown after a click exists in all seven platform languages. It is read outside the product, by somebody who may never have seen it.
  • Known limitation, stated on the screen: the service has no webhook yet, so a verification is recorded only when the person returns here. Somebody who confirms on another device and never comes back stays PENDING. That is the honest reading — this platform did not see it happen — and it is what the Overview screen says rather than something the code knows and the operator does not.

Added — PDF E-Sign v2: eID Mongolia qualified remote signing

  • eID Mongolia signature client (internal/platform/eidsign): a real relying-party client for the v3 signature API. The citizen's own device holds the private key and approves with PIN2, so nothing here ever touches a signing key: we hash the PDF, eID pushes that digest to the phone, and the signed document is assembled by eID's own doc-signer (POST /v3/signature/stamp/{sessionId}), which embeds the PKCS#7 together with OCSP and CRL data. Certificate level defaults to QUALIFIED — accepting ADVANCED would silently downgrade every document the ERP produces.
  • Asynchronous signing ceremony (/api/v1/esign/sign/init, /sign/{id}, /sign/{id}/download, /sign/{id}/cancel): upload → verification code → PIN2 on the phone → long-poll → PAdES-signed PDF. Sessions carry the exact bytes whose digest was approved, so a document edited mid-ceremony cannot produce a signature that fails to verify.
  • Signing on behalf of an organisation: representation rights are read live from the national registry rather than from a certificate, because a director who resigned yesterday still holds yesterday's certificate.
  • eID identity linkage (user_eid_identities, migration 00010): sign-in now records who a user is to eID. Without it every signature would make the citizen retype the registration number they had just authenticated with, and a typo would push the PIN2 prompt at somebody else's phone.
  • The five module screens are now real — signature log (filters, pagination, CSV export), batch signing, stamp placement (with an A4 preview), HSM connection (read-only, with a connection probe) and signing policy — replacing the /module/esign/* coming-soon placeholders.
  • Signing policy: a tenant can require qualified eID signatures and disable the HSM rail outright, including for callers hitting the API directly.

Fixed — PDF E-Sign

  • The app's permissions were declared but never enforced. io.example.esign is absent from the platform's blanket app gate (server.go) and its handlers only checked the tenant, so anyone in a tenant could sign. Every route now asserts esign.read, esign.sign or esign.manage explicitly. Migration 00010 backfills the grants existing roles should already have had, so no current user loses access.
  • esign.sign was ungrantable by the installer: the default-role rules key off a .read/.manage suffix, so only administrators would ever have received it.
  • The signature log recorded only successes, so a refused or expired ceremony left no trace — exactly the event an auditor looks for. Failures, refusals, expiries and downloads are now recorded with an outcome.
  • Non-ASCII download filenames were mangled: signed PDFs are now served with an RFC 5987 filename*, so a Cyrillic document keeps its name.
  • Truncated PDFs were accepted: a valid %PDF- header on a truncated body was passed to the signing service, which returned something that would not open. Uploads are now checked for a trailer as well as a header.
  • Sidebar sub-menus rendered as identical grey boxes: the icons named by the server's menu blueprints were never mapped in the frontend (Layout.tsx).

Added

  • PDF E-Sign App Module (io.example.esign (backend/internal/apps/esign)):
  • PDF document upload with tenant-scoped storage (migration 00009), page-count detection, and original/signed download endpoints (/api/v1/esign).
  • Digital signature (тоон гарын үсэг) certificate validation and PKCS#7 PDF signing via the Gerege eSign HSM platform client (internal/platform/gerege/esign.go) — the private signing key never leaves the HSM.
  • Visible signature stamp placement with last-page auto-targeting and signature audit log (esign_signature_logs).
  • Frontend signing flow (/esign): certificate check → canvas signature pad → HSM signing → signed PDF download.
  • Mock mode by default (ESIGN_MOCK_MODE); configure ESIGN_LOGIN_URL, ESIGN_SIGN_URL, and ESIGN_TOKEN for live signing.

Fixed — CI/CD pipeline

  • Go toolchain mismatch broke every job: backend/go.mod requires go 1.25.7 while the workflows pinned go-version: "1.24" and both Dockerfiles used golang:1.24-alpine (which sets GOTOOLCHAIN=local, so the build hard-fails). All jobs now resolve the version from backend/go.mod and the image builder sets GOTOOLCHAIN=auto.
  • Security workflow could never pass: govulncheck ./... and gosec ./... ran at the repository root, which contains no go.mod. They now run against backend/. The workflow also only triggered on PRs to master while the default branch is main.
  • GHCR push lacked packages: write, so deployment failed with denied: installation not allowed to Write the repository.
  • Removed the swag-drift job: the sources carry no swagger annotations and backend/docs/ is untracked, so it could only fail or pass vacuously.
  • deploy.yml no longer duplicates lint/test from ci.yml; it runs migrations before swapping the API over, uses docker compose, lower-cases the GHCR image path, and skips cleanly when deployment secrets are absent.
  • Added a frontend CI job (npm ci + tsc --noEmit + next build) — the Next.js app was never built by CI.
  • Added .dockerignore, a pinned backend/.golangci.yml (v2), deleted the duplicate backend/Dockerfile, untracked committed .DS_Store files, and gofmt-ed the 11 files that had drifted.
  • docker-compose.yml: added a one-shot migration service (the API used to start against an empty schema), health checks, and build-time NEXT_PUBLIC_API_URL; frontend/Dockerfile now uses npm ci with the lock file. Database credentials are consistent across compose, .env.example and the Makefile.

Fixed — Security

  • Session tokens were the user's UUID, the same value returned by /api/v1/auth/me. Replaced with opaque 256-bit crypto/rand tokens stored as SHA-256 digests in a new sessions table, with expiry and real revocation on logout (logout previously only dropped the cookie).
  • Mock national-identity mode was on by default (os.Getenv(...) != "false"), so in production /auth/eid/login and /auth/dan/login accepted any registration number and logged the caller in as the first user in the table with is_admin: true. Mock mode is now refused in production unless requested explicitly, and identities are matched against a real ERP user.
  • OAuth2 token endpoint accepted any known client_id with no secret (clientSecret != "" && ... skipped the check entirely). Client authentication is now mandatory and constant-time, supports HTTP Basic, validates the grant type, and is also enforced on /oauth2/introspect and /oauth2/revoke.
  • Removed the hard-coded client secret secret_gerege_dev_2026; ListClients no longer discloses client secrets.
  • App install/enable/disable and integration registration now require a tenant administrator — any authenticated user could previously reconfigure the tenant.
  • Login rate limiting no longer trusts X-Forwarded-For unless TRUST_PROXY_HEADERS=true.
  • Halved-entropy generateRandomString (hex output truncated back to n) fixed.
  • /metrics no longer labels unmatched routes with the raw request path — unbounded Prometheus cardinality driven by unauthenticated requests.

Fixed — App store & modules

  • Billing, Documents and the Developer Portal could not be installed: their modules were never registered in appregistry, their rows were missing from the apps table (foreign-key violation), and developer_portal was rejected by the slug validator, which forbade underscores.
  • The apps table is now synchronised from catalog/apps.json on boot instead of a hand-maintained INSERT that listed three of six apps.
  • Three manifests were malformed ("dependencies": {} instead of an array, permissions as plain strings, depends/sequence/action keys). They parsed into a silent stub with no dependencies, permissions or menus. Manifests are fixed, and a manifest that fails to load is now a startup error.
  • Billing and Documents no longer create their tables at boot with the error discarded; the schema moved into migration 00004. Both stop answering failed writes with fabricated demo records (inv_demo_100, doc_demo_200).
  • The app store reported disabled apps as "not installed" because installed and enabled were both derived from the enabled-only query.
  • Fixed a nil-interface panic in InstallApp when a module was missing from the registry, and a nil-pointer dereference in the E-ID/DAN login handlers that called err.Error() on a nil error.

Fixed — Reliability

  • AsyncOTPMailer.Shutdown closed the queue while workers and retries could still send on it (panic: send on closed channel) and dropped already-queued mail; it now drains, is idempotent, and refuses post-shutdown enqueues.
  • AI Copilot intent classification was case-sensitive against a lowercase keyword table, so "Stock" never matched.
  • Restored demo-data seeding (dropped from cmd/api), now idempotent and disabled in production unless SEED_DEMO_DATA is set.

[0.1.0] - 2026-08-05

Added

  • Modular Monolith Core Architecture:
  • Pure Go compile-time Module interface and global module registry (appregistry).
  • Tenant-level app installation, enablement, and menu visibility engine (appinstaller).
  • ORY Hydra-Grade OAuth2 & OpenID Connect (OIDC) SSO Provider (internal/platform/ssoprovider):
  • OpenID Connect Discovery (/.well-known/openid-configuration), JWKS URI (/.well-known/jwks.json), and OAuth2 Authorization Server (/oauth2/token, /oauth2/introspect, /oauth2/revoke).
  • Supports authorization_code, client_credentials, and refresh_token grant flows.
  • Developer Portal App Module (io.example.developer_portal (backend/internal/apps/developer_portal)):
  • Developer portal interface (/developer/apps) to register third-party OAuth2 client applications, issue Client IDs and Client Secrets, and manage redirect URIs.
  • Automated Production Deployment & CI/CD Pipeline (openerp.gerege.mn, .github/workflows/deploy.yml):
  • Continuous Integration & Automated Deployment pipeline building GHCR Docker images and deploying to openerp.gerege.mn.
  • Production Multi-Stage Dockerfile (deploy/Dockerfile) and Nginx SSL Reverse Proxy config (then deploy/nginx/openerp.gerege.mn.conf, since renamed to deploy/nginx/nexus.gerege.mn.conf and replaced by deploy/nginx/nexus-private.conf, removed in 1.49.10).
  • Recursive dependency resolution algorithm with cycle detection and semver validation.
  • Shared-Schema Multi-Tenancy:
  • Context-scoped tenant_id isolation across all business entities and repositories.
  • Tenant app gating middleware returning 403 Forbidden for disabled modules.
  • Business Modules (Vertical Slices):
  • Contacts (io.example.contacts): Business contacts directory with full CRUD.
  • Products (io.example.products): Product catalog management with unique tenant-scoped SKUs.
  • Inventory (io.example.inventory): Warehouse management, live stock levels, append-only stock movement log, and transactional stock adjustments with negative stock protection.
  • Next.js App Router Admin Shell:
  • Top navigation bar with tenant badge (Demo Corporation), user profile menu, and logout.
  • Dynamic sidebar navigation driven by /api/v1/menus.
  • App Store (/apps) with search, categories, dependency badges, and Install/Enable/Disable controls.
  • Installed Apps Settings (/settings/apps).
  • Dedicated business UIs for /contacts, /products, and /inventory.
  • High-Performance Resilience Engine (go-zero Inspired):
  • Adaptive Circuit Breaker (resilience/breaker.go): Google SRE style sliding window adaptive circuit breaker.
  • Adaptive Load Shedding (resilience/loadshedder.go): In-flight HTTP request concurrency shedder returning 503 Service Unavailable under heavy traffic spikes.
  • Singleflight Coalescing (resilience/singleflight.go): Duplicate query suppressor preventing thundering herd cache stampedes.
  • Exponential Backoff Retry (resilience/retry.go): DoWithRetry execution helper for resilient DB/network operations.
  • Observability & Async Messaging:
  • Prometheus metrics endpoint (/metrics) recording HTTP request rates and latency histograms (github.com/prometheus/client_golang).
  • OpenTelemetry tracing initialization (SetupTracing).
  • Async OTP Mailer queue with worker pool, retry logic, and graceful shutdown (internal/platform/mailer).
  • Public Billing & e-Barimt Module (io.example.billing (backend/internal/apps/billing)):
  • Public service fee invoices, 10% VAT calculation for Mongolia e-Barimt, and status tracking (/billing).
  • Gerege DAN SSO Gateway System (dan.gerege.mn (backend/internal/platform/dan)):
  • Official Gerege Systems DAN SSO Gateway integration service (POST /api/v1/auth/dan/login).
  • Citizen identity verification and session token validation against https://dan.gerege.mn/api/v1.
  • E-ID Digital Identity & DAN SSO Authentication (internal/platform/eid):
  • Aligned 100% with official eidmongolia.mn & developer.gerege.mn OAuth2 and OpenID Connect (OIDC) specifications.
  • Supports 4 official Mongolian authentication channels: PKI Digital Signature (Тоон гарын үсэг), Mobile OTP, Bank SSO, and Biometric Face Verification.
  • External System Integrations & Webhook Engine (internal/platform/integration):
  • Event Dispatcher & Connector Manager supporting HMAC-SHA256 signature signing, asynchronous webhooks, and third-party REST connectors.
  • Dedicated Integration Settings Manager UI (/settings/integrations) with real-time status & health tracking.
  • XYP State Data Exchange System (xyp.gerege.mn (backend/internal/platform/gerege/xyp.go)):
  • Official Mongolian State Data Exchange (ХУР Төрийн мэдээлэл солилцооны систем) integration service.
  • Citizen civil registration (POST /api/v1/xyp/citizen) & company legal entity verification (POST /api/v1/xyp/company).
  • Interactive "ХУР / XYP Auto-fill" button integration on Contacts page.
  • Database & Migrations:
  • Goose SQL migrations (00001_platform_core.sql, 00002_app_store.sql, 00003_business_apps.sql).
  • Automated initial demo data seeder (admin@example.com / Password123!).

Inspirations & Acknowledgements

  • snykk/go-rest-boilerplate by @snykk: Initial Go REST API structure.
  • Odoo: Modular app ecosystem, App Store dependency resolver, and dynamic menu architecture.
  • go-zero: High-performance cloud-native resilience engine (Adaptive Circuit Breaker, Load Shedder, Singleflight, Exponential Retry).

Authors & Contributors

  • Gerege Systems Development Team
  • Gemini AI
  • Claude AI