Bridge Contract v1.6 — Native Shell + Web Work Area¶
Native бүрхүүл (Swift, C#, Kotlin) ба web app хоёрын хооронд байх
window.GeregeShell гэрээний бүртгэл.
Монгол
1. Зорилго¶
Платформ хоёр хэлбэрээр ажиллана:
- Хөтчийн горим — web app өөрөө бүрэн апп: толгой хэсэг, хажуугийн цэс, мобайл таб, нэвтрэлт бүгд түүнийх.
- Бүрхүүлийн горим — native бүрхүүл нь session-ий амьдралын мөчлөг
(сэргээх, дахин нэвтрүүлэх, гарах), толгой хэсэг, цэс, хөл, төхөөрөмжийн
хандалтыг эзэмшинэ. Web app өөрийн chrome-оо нуугаад зөвхөн ажлын муж
болж рендерлэгдэнэ. Нэвтрэлтийн дэлгэц нь платформын native UI; web-ийн
/loginзөвхөн browser/PWA горимд үлдэнэ.
Гэрээ нь энэ хоёрын хилийг тодорхойлно. Хамгийн чухал шаардлага:
хөтчийн горим ямар ч нөхцөлд өөрчлөгдөхгүй. Бүрхүүл байхгүй үед
window.GeregeShell тодорхойлогдоогүй байна; web талын бүх helper null
эсвэл false буцаана; data-shell атрибут <html> дээр огт тавигдахгүй.
Web талын хэрэгжилт: frontend/lib/shell.ts.
Бүрхүүлийн талын хэрэгжилтүүд: native-apps/.
1a. Нэг хүрээний дүрэм¶
Native клиент бүр цорын ганц хүрээтэй: macOS/Windows дээр нэг цонх, iOS/ Android дээр нэг scene. Нэвтрэлт, ажлын муж, тохиргоо, профайл — бүгд тэр хүрээн дотор солигддог дэлгэцүүд болохоос тусдаа цонх биш.
Хүрээнээс гарч болох цорын ганц зүйл бол popup: богино насалдаг, эцэг цонхондоо холбогддог, өөрөө навигаци агуулдаггүй элемент.
| Зөвшөөрөгдөнө (popup) | Зөвшөөрөгдөхгүй |
|---|---|
NSAlert, NSMenu, NSSavePanel |
Хоёр дахь NSWindow |
MessageBox, файл сонгох диалог |
Window.ShowDialog(), Window.Show() |
alert, confirmationDialog, share sheet |
Хоёр дахь WindowGroup/scene |
BiometricPrompt, зөвшөөрлийн диалог |
Хоёр дахь Activity |
Мөн ажлын мужаас window.open / target="_blank" хийсэн шилжилт нь webview-ийн
шинэ цонх нээхийг хүсдэг. Бүрхүүл тэр цонхыг үүсгэхгүй — хаяг нь
зөвшөөрөгдсөн scheme-тэй бол системийн хөтчөөр нээгдэнэ.
Дүрмийн шалтгаан нь загварын амт биш. Тусдаа цонх нь хэрэглэгчийг нэг аппын хоёр хүрээ хооронд үсрүүлж, taskbar/Dock дээр хоёр дахь бичлэг үлдээж, kiosk болон бүтэн дэлгэцийн горимд бүр огт олдохгүй болдог.
Дэлгэц солигдоход ажлын мужийг устгахгүй. Native дэлгэц гарахад webview-г харагдацаас нуух нь зөв; view модоос хасах нь буруу. Хасвал webview дахин үүсч, хэрэглэгчийн байсан хуудас, гүйлгэсэн байрлал, бөглөж байсан маягт бүгд алга болно.
1b. Төхөөрөмжийн шугам¶
Гарал нэг, backend нэг. Хөтөч ч, native бүрхүүл ч платформын нийтийн
гарал (PUBLIC_ORIGIN, ж: https://nexus.gerege.net) дээр ажиллана. Бүрхүүл аль
шугамынх гэдгээ host биш webview-ийн User-Agent-ийн төгсгөлд хэлнэ:
| Шугам | UA тэмдэгт | Клиент |
|---|---|---|
| хөтөч | — (тэмдэгтгүй) | Хөтөч / PWA — web app өөрөө бүрэн апп |
desktop |
GeregeShell/desktop |
macOS, Windows |
mobile |
GeregeShell/mobile |
iOS / iPadOS, Android |
kiosk |
GeregeShell/kiosk |
захиалгатай — клиент алга |
pos |
GeregeShell/pos |
захиалгатай — клиент алга |
Шугам нь form factor-ыг нэрлэнэ, платформыг биш. Ширээн дээрх Mac ба
Windows нэг шугам; платформ нь window.GeregeShell.platform дээр тусдаа.
Ажлын мужаас гарах дуудлага same-origin: session cookie SameSite хэвээр
ажиллаж, CORS preflight үүсэхгүй, NEXT_PUBLIC_API_URL ч ижил гарал руу заана.
Шугам нь зөвхөн UX шийдвэр (/login-ыг хаах, шугамын нүүр) тул UA-г хуурах нь
эрх олгохгүй — хөтөч тэмдэгт нэмж ирвэл зөвхөн web-ийн нэвтрэх дэлгэцээ алдана.
2026-10-15 хүртэл шугам бүр тусдаа host-той байв (desktop., mobile.,
kiosk., pos.nexus.gerege.net). Тэр нь DNS, TLS, nginx vhost,
DEVICE_LINE_ORIGINS-ийг шугам бүрт, distribution бүрт давхарлуулдаг байсан
бөгөөд өгдөг байсан тусгаарлалт (host-only cookie) нь native дээр аль хэдийн
бий: webview бүр өөрийн cookie сантай. Тиймээс хасагдсан.
Шугамын нүүр дэлгэц¶
Шугам бүрийн үндэс (/) нь тухайн платформын өөрийн нүүр дэлгэц рүү
шилжинэ: frontend/app/line/[line]/. Native клиент
нэвтэрсний дараа /-ыг нээх ба тэндээс тухайн төхөөрөмжийн web дэлгэцүүд
хөгжинө.
Нүүр дэлгэц нэвтрэлт шаардахгүй. Тэр нь ажлын мужид web-ийн нэвтрэх дэлгэц гарч ирэхийг орлохын тулд байгаа тул session байхгүй үед ч зогсоно — эс бөгөөс шийдэх гэсэн асуудлаа өөрөө үүсгэнэ.
Дэлгэцүүд нь өнгө, гарчгаараа биш байрлалаараа ялгаатай: ширээ (desk),
гарын алга (hand), олон нийтийн терминал (public) гурав нь товчны хэмжээ,
нягтрал, юуг эхэнд тавихыг шийднэ.
Native нэвтрэлт ажлын мужид хэрхэн хүрэх вэ¶
Бүрхүүл нэвтрэлтээ эзэмшдэг ба шугам дээр web-ийн /login хаалттай тул
ажлын муж өөрөө нэвтэрч чадахгүй. Тэгвэл webview session-оо хаанаас авах
вэ: платформын нэвтрэлт (/api/v1/auth/eid/poll) амжилттай болоход сервер
session_token-ыг Set-Cookie-гээр буцаадаг бөгөөд бүрхүүл түүнийг webview-ийн
cookie сан руу суулгана —
WorkAreaSession.swift
(macOS, iOS), WorkAreaSession.kt
(Android).
Гурван нөхцөл зэрэг биелж байж ажиллана, аль нэг нь тасрахад алдаа нь 401 биш хоосон дэлгэц хэлбэрээр илэрнэ:
- Cookie-гийн нэр серверийнхтэй таарах (
security.TenantSessionCookie). ГурвуулыгWorkAreaSessionContractTestэх кодоос нь уншиж тулгана. - Webview нь native дуудлагууд явдаг ЯГ ТЭР гаралыг ачаалах — cookie нь host-only.
- Гарахад бүрхүүл түүнийг мөн устгах. Native тал гарчихаад ажлын муж нэвтэрсэн хэвээр үлдвэл дараагийн хүн өмнөх хүний мужийг харна.
Native клиентийн HTTP давхарга нь cookie-г ЕРӨНХИЙД нь хүлээж авдаггүй
(httpCookieAcceptPolicy = .never, OkHttp-д CookieJar.NO_COOKIES) — энэ нь
зөв хэвээр. Дамжуулалт нь зөвхөн энэ НЭГ cookie-г, зөвхөн серверийн өгсөн
хэлбэрээр нь (HttpOnly, Secure, SameSite бүгд хэвээр) авч явна.
eID start/poll нь flow_token дагуулна. /api/v1/auth/eid/start ба
start-id нь хариундаа flow_token буцаана (хөтөчид мөн HttpOnly
gerege_eid_flow cookie). Poll/дуусгах endpoint бүр түүнийг шаардаж НЭГ удаа
зарцуулна. Native клиент cookie хүлээж авдаггүй тул токеныг JSON хариунаас авч
poll-ын body-д (flow_token) буцааж илгээнэ; буцааж илгээдэггүй клиент eID-ээр
нэвтэрч чадахгүй. App2App буцах холбоос (gerege-nexus://auth?sessionId=…)
токен агуулахгүй — клиент өөрийн эхлүүлсэн session-ийн токеныг л ашиглана.
2026-09-25-аас RUNNING үеийн poll бүр ч токенгүй бол 403 (CA-аас асуухаас өмнө).
Серверийн тал: OPERATIONS.md.
/api/v1/auth/* нэвтрэлтийн POST-ийн бие заавал Content-Type: application/json.
Хоосон биш биетэй бусад төрөл 415 авна (login CSRF-ийн хамгаалалт,
security.RequireJSONBody); биегүй хүсэлт (QR start) хэвээр.
Бүртгэлүүд — шугам нэмэхэд хоёуланг нь өөрчилнө:
native-apps/shared/device_lines.json— native талын эх сурвалжfrontend/lib/deviceLine.ts— web талын хуулбар
Web тал шугамыг UA-аар нь таньж (frontend/proxy.ts)
Vary: User-Agent тавьж, /login-ыг хаана: бүрхүүл дотор нэвтрэлт бол native UI.
2. Инжекцийн дүрэм¶
| Дүрэм | Утга |
|---|---|
| Эх | Нэг скрипт — native-apps/shared/gerege-shell-bridge.js. Swift/Kotlin/C# хуулбарыг gen-shell.mjs үүсгэнэ; CI дахин үүсгээд ялгаа гарвал улаан. |
| Хугацаа | Document start — hydration эхлэхээс өмнө объект байрандаа байна (WKUserScript .atDocumentStart, AddScriptToExecuteOnDocumentCreatedAsync, WebViewCompat.addDocumentStartJavaScript). Скриптийг анхны навигациас ӨМНӨ суулгана. |
| Хамрах хүрээ | Main frame only. Скрипт өөрөө window.top !== window үед юу ч хийхгүй (WebView2, Android нь document-start скриптийг дэд frame-д ч ажиллуулдаг). |
| Origin | Скрипт зөвхөн бүрхүүлийн өөрийн origin дээр (location.origin === config.origin) объект тодорхойлно. Allowlist-ийн танилтын origin руу шилжсэн хуудас гүүргүй. |
| Давхардал | Объект аль хэдийн байвал скрипт юу ч хийхгүй буцна. |
| Хувиршгүй байдал | window.GeregeShell ба capabilities нь Object.freeze; window.GeregeShell, __geregeShellResolve, __geregeShellEmit нь бичигдэхгүй, дахин тодорхойлогдохгүй. Хоёр resolver нь enumerable биш. |
Document start дээр inject хийх шаардлага нь зөвхөн тохь тухын асуудал биш:
ThemeProvider анхны рендер дээрээ data-shell-ийг тавьдаг тул объект хожуу
ирвэл хэрэглэгч эхлээд web-ийн харагдацыг хараад дараа нь native рүү үсэрнэ.
3. Объектын бүтэц¶
interface GeregeShell {
version: string; // гэрээний semver, одоо "1.6"
platform: "macos" | "windows" | "ios" | "android" | "kiosk" | "pos";
formFactor: "desktop" | "mobile" | "tablet" | "kiosk" | "pos";
capabilities: string[];
invoke<T>(method: string, params?: Record<string, unknown>): Promise<T>;
on(event: string, handler: (payload: unknown) => void): () => void;
}
version— гэрээний хувилбар, бүрхүүл програмын хувилбар БИШ.platform—<html data-shell="...">атрибутын утга болно.capabilities— тухайн бүрхүүлд үнэхээр хэрэгжсэн чадварууд. Энд байхгүй боломжийг зарлах нь web талын fallback-ыг ажиллах боломжгүй болгоно.invoke()— бүрхүүлийн зарлаагүй method-ыг native руу илгээлгүй шууд reject.on()— буцаах утга нь бүртгэлээ цуцлах функц (useEffect-ийн cleanup).
Утас. Web → native нь нэг дугтуй:
| Бүрхүүл | Web → native | Native → web |
|---|---|---|
| macOS, iOS | webkit.messageHandlers.geregeShell.postMessage(object) |
evaluateJavaScript |
| Windows | chrome.webview.postMessage(object) → WebMessageReceived |
ExecuteScriptAsync |
| Android | geregeShellNative.postMessage(JSON string) — WebViewCompat.addWebMessageListener |
evaluateJavascript |
Native → web нь хоёр л entry point: window.__geregeShellResolve(id, ok, value)
(дуудлага бүрд НЭГ удаа; танихгүй эсвэл аль хэдийн шийдэгдсэн id-г
үл тоомсорлоно) ба window.__geregeShellEmit(event, payload) (зөвхөн зарласан
event дамжина). ok === false үед value нь алдааны текст.
4. Capability нэрс¶
Жагсаалтын эх сурвалж нь frontend/lib/shell.ts-ийн
SHELL_CAPABILITIES. Энэ хүснэгт түүнтэй ЯГ таарахыг
scripts/check-shell-contract.mjs CI дээр шалгана.
| Нэр | Method | Утга |
|---|---|---|
biometric |
biometric.authenticate |
Touch ID / Face ID / Windows Hello / төхөөрөмжийн эзэн танилт. |
print.system |
print.system |
Системийн хэвлэх харилцах цонх. |
fs.save |
fs.saveAs |
Файлыг хэрэглэгчийн сонгосон газарт хадгалах. |
notify |
notify.show |
Системийн мэдэгдэл. |
badge |
badge.set |
Апп дүрсэн дээрх тоолуур. |
external.open |
external.open |
Системийн хөтчөөр гадаад хаяг нээх. |
secure-store |
— | Keychain / Credential Manager маягийн нууц хадгалалт. v1.4-т method байхгүй тул зарлахгүй. |
menu.native |
— | Native цэсийг тенантын цэснээс динамикаар барих. Method байхгүй; одоо ямар ч бүрхүүл зарлахгүй. |
shell.pane |
shell.openPane |
Бүрхүүлийн эзэмшдэг дэлгэц рүү хүрээн дотор шилжих. |
escpos |
escpos.print, escpos.drawer |
ESC/POS баримтын принтер ба мөнгөний шургуулга. |
scanner |
scanner.start, scanner.stop |
Баркод/QR уншигч; уншилт shell:scan event-ээр. |
serial |
serial.transact |
COM/serial төхөөрөмжтэй түүхий солилцоо. |
nfc |
— | NFC уншигч. v1.4-т method байхгүй тул зарлахгүй; method нэмэгдэхэд minor хувилбар. |
camera.scan |
camera.scan |
Камераар код уншуулах. |
device.identity |
device.identity |
Нууц token-гүй төхөөрөмжийн тодорхойлолт. |
kiosk.lockdown |
kiosk.lockdown |
Киоскийн түгжих горим. |
telemetry |
telemetry.emit |
Бүрхүүлийн телеметр рүү үйл явдал. |
payments |
payments.start |
Vendor-neutral төлбөрийн терминал. |
document.scan |
document.scan |
Камераар олон хуудастай баримтыг скан хийж PDF болгох (VisionKit / ML Kit). |
saved |
saved.put, saved.remove, saved.list |
Ажлын мужийн хуудсыг төхөөрөмж дээр шифрлэж хадгалах; native жагсаалт сүлжээгүй үед ч нээгдэнэ. |
Capability нэр ба method нэр нь тусдаа: capability нь боломж, method нь
дуудлага. Жишээ нь fs.save чадвар нь fs.saveAs method-оор хэрэгжинэ.
Бүрхүүл capability-г зөвхөн түүний method-ын аль нэгийг барьдаг үед зарлана
(шалгагч скрипт мөн үүнийг тулгана).
5. Method-ууд¶
Бүх дуудлага invoke(method, params) хэлбэртэй ба Promise буцаана.
Дэмжигдээгүй method-ыг бүрхүүл reject хийх ёстой — тэр үед web тал өөрийн
fallback-аа ажиллуулна.
auth.reLogin¶
Session дуусахад нэвтрэлтийн урсгалыг эхлүүлнэ.
| Параметр | Байхгүй |
| Хариу | null — бүрхүүл нэвтрэлтийг барьж авсан, web тал өгөгдлөө дахин татна |
| Алдаа | Бүрхүүл нэвтрэлтийг эзэмшдэггүй бол reject; web тал /login руу шилжинэ |
Web тал үүнийг нэг session-д нэг л удаа оролдоно: дахин нэвтэрсэн ч session хүчингүй хэвээр байвал мөчлөг үүсэхээс сэргийлнэ.
auth.lock¶
Native biometric/PIN түгжээг гаргана. Unlock дуусах хүртэл resolve хийхгүй,
цуцалбал reject хийнэ. auth.reLogin, auth.lock нь lifecycle method учраас
capability шаардахгүй.
notify.show¶
| Параметр | { title: string, body?: string } |
| Хариу | null |
| Алдаа | Мэдэгдлийн зөвшөөрөл олгогдоогүй бол reject |
badge.set¶
| Параметр | { count: number } — 0 бол тоолуурыг арилгана |
| Хариу | null |
biometric.authenticate¶
| Параметр | { reason?: string } — хэрэглэгчид харагдах шалтгаан |
| Хариу | { authenticated: true } |
| Алдаа | Цуцлагдсан, амжилтгүй, эсвэл боломжгүй үед системийн алдааны текстээр reject |
external.open¶
| Параметр | { url: string } |
| Хариу | null |
| Алдаа | Зөвхөн http, https, mailto, tel scheme зөвшөөрөгдөнө; бусад нь reject |
print.system¶
| Параметр | Байхгүй |
| Хариу | null — харилцах цонх хаагдсаны дараа |
| Алдаа | Хэвлэх цонх нээх боломжгүй бол reject |
fs.saveAs¶
| Параметр | { filename?: string, base64?: string, text?: string } |
| Хариу | { path: string } |
| Алдаа | Агуулга дутуу/буруу, хэрэглэгч цуцалсан, бичиж чадаагүй үед reject |
Замыг web тал заахгүй — зөвхөн санал болгох файлын нэр дамжуулна. Бүрхүүл хэрэглэгчийн сонгосон газарт л бичнэ, дамжуулсан нэрнээс зөвхөн сүүлийн бүрэлдэхүүнийг авна.
menu.changed¶
Тенантын апп цэс өөрчлөгдсөнийг мэдэгдэнэ; бүрхүүл native цэсээ дахин барих боломжтой болно.
| Параметр | {} |
| Хариу | null |
shell.openPane¶
Бүрхүүлийн эзэмшдэг дэлгэц рүү шилжинэ. Capability: shell.pane.
| Параметр | { pane: "work" \| "settings" } |
| Хариу | null |
| Алдаа | Танихгүй нэр, эсвэл хэрэглэгч хараахан нэвтрээгүй бол reject |
Энэ нь цонх нээх дуудлага биш — ижил хүрээн доторх дэлгэц солигдоно (§1a). Ажлын муж "Төхөөрөмжийн тохиргоо" гэсэн холбоос гаргах бол native тохиргоог өөрөө зурахын оронд үүнийг дуудна. Reject ирвэл web тал өөрийн тохиргооны хуудсаа үзүүлнэ.
saved.put / saved.remove / saved.list¶
Хүн «хадгалах» гэж сонгосон хуудсыг бүрхүүл төхөөрөмж дээр (Keychain / Android
Keystore-оор шифрлэж) хадгалж, өөрийн native жагсаалтад сүлжээгүй үед ч
харуулна. Мөрийг дарахад ажлын муж тэр path-ийг өөрийн суурь хаяг дээр нээнэ.
Capability: saved. Гарахад (logout) жагсаалт устна.
| Method | Параметр | Хариу |
|---|---|---|
saved.put |
{ id, path, title, subtitle?, rows?: [{ label, value }] } |
null — ижил id байвал snapshot-ыг шинэчилнэ |
saved.remove |
{ id } |
null |
saved.list |
{} |
[{ id, path, title, subtitle, rows, saved_at }] |
Хязгаар: id нь [A-Za-z0-9_.:/-]{1,128}; path нь /-ээр эхэлсэн, //-гүй,
2048 тэмдэгт хүртэл (WorkAreaLink-ийн дүрэм); title ≤ 200, subtitle ≤ 300,
rows ≤ 6 (label/value тус бүр ≤ 100); нийт ≤ 200 бичлэг. Зөрсөн бол reject.
Web тал хуудсаа нээх бүрд saved.put-ыг дахин дуудаж snapshot-оо шинэчилнэ.
document.scan¶
Системийн баримт скан (iOS VisionKit, Android ML Kit document scanner) — ирмэг
олох, хэвийг засах, олон хуудас. Capability: document.scan.
| Параметр | { maxPages?: number } — 1..20, анхдагч 20 |
| Хариу | { base64: string, mime: "application/pdf", pages: number } |
| Алдаа | Хэрэглэгч цуцалсан, камер/скан боломжгүй (simulator, Play services байхгүй), PDF 15 MB-аас их бол reject |
Үр дүн зөвхөн дуудсан хуудсанд очно; бүрхүүл хаана ч хадгалахгүй, илгээхгүй.
Хадгалах бол web тал fs.saveAs-ийг дуудна.
Device method-ууд (v1.1–v1.3)¶
| Method | Capability | Параметр | Хариу |
|---|---|---|---|
escpos.print |
escpos |
{ text?: string, base64?: string, cut?: boolean } |
null |
escpos.drawer |
escpos |
{ pulse_ms?: number } |
null |
scanner.start / scanner.stop |
scanner |
{ mode?: string } / {} |
null; уншилт shell:scan event-ээр ирнэ |
serial.transact |
serial |
{ port?: string, baud?: number, base64: string, read_timeout_ms?: number } |
{ base64: string } |
device.identity |
device.identity |
{} |
{ platform, form_factor, app } — device token, secret, HMAC түлхүүр хэзээ ч биш |
camera.scan |
camera.scan |
{ formats?: string[] } |
{ value: string, format: string } |
kiosk.lockdown |
kiosk.lockdown |
{ enabled: boolean } |
{ enabled: boolean } |
telemetry.emit |
telemetry |
{ level: string, event: string, payload?: object } |
null |
payments.start |
payments |
{ amount: number, currency: string, reference: string } |
Vendor-neutral төлбөрийн үр дүн |
payments.start нь vendor SDK суусан adapter байхгүй үед reject хийнэ. Web тал
тэр үед өөр төлбөрийн арга руу fallback хийнэ. Device token, secure-store key,
printer raw credential зэрэг нь ямар ч method-ын хариунд орж болохгүй.
6. Event-ууд¶
Event нь бүрхүүлээс ажлын муж руу чиглэнэ. on(name, handler) нь handler-т
payload-ыг шууд дамжуулна.
| Event | Payload | Утга |
|---|---|---|
shell:navigate |
{ path: string } |
SPA router-ээр шилжинэ. Зөвхөн /-ээр эхэлсэн дотоод зам хүлээн авна. |
shell:search |
{ query: string } |
Ажлын мужийн хайлтыг нээж, үгийг нь дамжуулна. |
shell:menu-refresh |
Байхгүй (null) |
Цэсээ сервертэй дахин тааруулахыг хүснэ. |
shell:auth-changed |
{ reason: string, user_id?: string } |
Session/ажилтан солигдсон; tenant data-г дахин татна. |
shell:capabilities-changed |
{ capabilities: string[] } |
Peripheral/settings өөрчлөгдсөн; web fallback-аа дахин тооцно. |
shell:scan |
{ value: string, format: string } |
Keyboard wedge, camera эсвэл vendor scanner уншилт. |
Бүрхүүл зарлаагүй event-ийг __geregeShellEmit дамжуулахгүй. Одоогоор ямар ч
бүрхүүл event илгээдэггүй (§9) — web тал on()-ыг бүртгэж болох ч дуудагдахгүй.
7. Хувилбарын дүрэм¶
Гэрээ semver-ээр явна. GeregeShell.version нь гэрээний хувилбар.
- Minor (1.0 → 1.1) — шинэ method, шинэ event, шинэ capability, эсвэл сонголтот параметр нэмэх. Хуучин web бүрхүүлийн шинэ хувилбартай ажиллана.
- Major (1.x → 2.0) — байгаа method-ын нэр, параметр, хариу, эсвэл
event-ийн payload өөрчлөх, юм хасах. Бүрхүүл шинэ
version-оо ЗААВАЛ зарлана. - v1.6 нь minor:
document.scanmethod ба capability нэмэгдсэн; iOS, Androidfs.saveAsба (Android)biometric.authenticate,auth.lock-ийг хэрэгжүүлсэн. - v1.5 нь minor:
saved.put,saved.remove,saved.listmethod баsavedcapability нэмэгдсэн. Хуучин бүрхүүл тэдгээрийг reject хийж, web тал «хадгалах» товчоо харуулахгүй (capability-аар шалгана). - v1.4 нь minor:
shell.openPanemethod баshell.panecapability нэмэгдсэн, байгаа юу ч өөрчлөгдөөгүй. v1.3 бүрхүүл дээр v1.4 web ажиллана —shell.openPaneнь reject хийж, web тал өөрийн тохиргооны хуудас руу шилжинэ. - Device method нэмэхдээ түүнд харгалзах capability-г мөн зарлана.
auth.*lifecycle method-ууд capability шаардахгүй. - Web тал үл мэдэгдэх method-ыг дуудаж болно — reject ирнэ гэдгийг тооцсон fallback-тай байх ёстой. Энэ нь хуучин бүрхүүл дээр шинэ web ажиллах гол механизм.
8. Аюулгүй байдлын шаардлага¶
Бүрхүүл хэрэгжүүлэх бүрд дараах зүйлс заавал биелэх ёстой.
- Origin шалгалт. Гүүрээр ирсэн мессежийг боловсруулахын өмнө илгээгч frame-ийн одоогийн хаягийн origin нь платформын web origin-той тохирч байгааг шалгана. Тохирохгүй бол мессежийг үл тоомсорлоно.
- Main frame only. Скриптийг зөвхөн гол frame-д inject хийж, мессеж
хүлээн авахдаа
isMainFrame-ийг дахин шалгана. iframe нь биометр, файл, мэдэгдэлд хүрэхгүй. - JSON serialize. Native талаас JS руу орох бүх утга JSON-оор кодлогдоно. Алдааны текст, callback ID, хайлтын үг зэргийг JS эх бичвэрт мөр залгаж оруулахыг хориглоно — ямар ч эх сурвалжаас ирсэн текст код болж ажиллах боломжгүй байх ёстой.
- Хариуг нэг цэгээр. Native хариу зөвхөн
__geregeShellResolve(id, ok, value)гэсэн нэг entry point-оор буцна (§3). Дуудлага бүрд шинэ дэлхийн функц үүсгэдэггүй. Дугтуйнidнь[A-Za-z0-9_-]{1,128},methodнь[A-Za-z0-9_.-]{1,64},paramsнь объект эсвэл байхгүй — өөр хэлбэрийн мессежийг хариугүй хаяна. - Navigation allowlist. Гол frame зөвхөн scheme + host + порт гурвуулаа
таарсан origin дотор үлдэнэ: бүрхүүлийн өөрийн суурь хаяг ба
device_lines.json-ынworkArea.authOrigins(одоогоор хоосон — шалтгааныг тэр файл бичнэ). Origin ньhttps, эсвэл loopback (localhost,127.0.0.1,::1, Android эмулятор дээр10.0.2.2) дээрхhttpбайх ёстой — LAN дээрхhttpсуурь хаягийг ажлын муж ачаалахгүй. Бусад хаяг, мөн шинэ цонхны хүсэлт бүр (target="_blank",window.open) webview дотор биш:http,https,mailto,telбол системд өгөгдөнө, бусад scheme хаягдана. Дэд frame ньhttps(loopbackhttp),about:,blob:-ийг ачаална; дэд frame-ээс юу ч гадагш нээгдэхгүй. - Scheme хязгаарлалт.
external.openнь зөвхөнhttp,https,mailto,tel-ийг хүлээн авна.file://болон бүртгэгдсэн дурын scheme нь webview-гээс код ажиллуулах гарц болно. - Файлын зам. Хадгалах байршлыг web тал сонгохгүй; зөвхөн хэрэглэгчийн сонгосон газар руу бичнэ.
- Deep link.
gerege-nexus://authнь зөвхөн «зөвшөөрөл болсон, одоо шалга» гэсэн дохио; үр дүнг сервер poll-оор өгнө. Хүлээн авах хэлбэр: schemegerege-nexus, hostauth, зам хоосон, user/port/fragment байхгүй, query нь зөвхөн ганцsessionId(эсвэлsession_id)[A-Za-z0-9_-]{1,128}. Өөр хэлбэрийг хаяна, URL-ийг лог руу бичихгүй (session id агуулж болно).
Бүгдийг нэг дүрмээр гурван хэл дээр хэрэгжүүлсэн бөгөөд тус бүр ижил
тохиолдлуудаар тестлэгдэнэ:
GeregeShellKit (macOS, iOS — swift test),
WorkAreaPolicy.kt
(./gradlew test),
WorkAreaPolicy.cs
(dotnet test).
8a. TLS pinning — юу pin хийгдсэн, юу нь үгүй¶
| Холболт | Pinning |
|---|---|
| Native HTTP клиент (нэвтрэлт, poll, самбарын дуудлага) | ✅ ижил багц — tls_pins.json: ISRG Root X1, X2, YE. Гинжний дурын гэрчилгээ таарвал хүчинтэй. |
| Ажлын муж (WKWebView, WebView2, Android WebView) | ❌ pin хийгдээгүй — системийн trust store л. Гурван webview-д өөрийн холболтыг pin хийх дэмжигдсэн API байхгүй; хуурамч арга (сертификатыг JS-ээр шалгах, proxy) нь pinning-ээс илүү эмзэг. |
Идэвхжүүлэлт нэг биш — энэ нь мэдэгдэж буй зөрүү:
| Клиент | Хаана | Анхдагч |
|---|---|---|
| Windows | appsettings.json → Security.RequireCertificatePinning |
асаалттай |
| macOS, iOS | UserDefaults["security.tlsPinning"] (Debug-д алгасна) |
унтраалттай |
| Android | SharedPreferences["security.tlsPinning"] (Debug-д алгасна) |
унтраалттай |
Apple/Android дээрх анхдагчийг асаах нь дараах эргүүлэх дарааллын 1-р алхам
хэрэглэгчдэд хүрсний дараа л. Эргүүлэх дараалал: (1) шинэ үндсийн pin-ийг
tls_pins.json-д нэмж (хуучныг хасахгүй), gen-shell.mjs ажиллуулж,
Windows appsettings.json ба Obfuscated.swift-ийг шинэчлээд гаргалт хийнэ;
(2) хэрэглэгчдийн ихэнх шинэ хувилбар дээр гарсны дараа л серверийн гинжийг
солино; (3) хуучин үндэс гинжнээс бүрмөсөн гарсны дараагийн гаргалтад хуучин
pin-ийг хасна. Гурван хуулбарын таарлыг check-shell-contract.mjs шалгана.
9. Бүрхүүлийн одоогийн байдал¶
Гэрээний хувилбар: 1.6. Linux нь PWA (бүрхүүлгүй).
Доорх хүснэгт нь клиент бүрийн зарлал (shell-manifest маркер) -аас
үүсгэгддэг — гараар бүү зас: node scripts/check-shell-contract.mjs --write.
Зарласан method бүр native handler дотор нэрлэгдсэн байх, capability нь зөвхөн
түүний method баригдсан үед зарлагдахыг скрипт CI дээр тулгана.
| Бүрхүүл | platform |
Method | Event | Capability | Хэрэгжилт |
|---|---|---|---|---|---|
| macOS | macos |
auth.reLogin, auth.lock, biometric.authenticate, external.open, print.system, menu.changed, shell.openPane, device.identity |
— | biometric, external.open, print.system, shell.pane, device.identity |
PlatformView.swift |
| iOS / iPadOS | ios |
auth.reLogin, auth.lock, biometric.authenticate, external.open, menu.changed, device.identity, saved.put, saved.remove, saved.list, fs.saveAs, document.scan |
— | biometric, external.open, device.identity, saved, fs.save, document.scan |
MobilePlatformPage.swift |
| Android | android |
auth.reLogin, auth.lock, biometric.authenticate, external.open, menu.changed, device.identity, saved.put, saved.remove, saved.list, fs.saveAs, document.scan |
— | biometric, external.open, device.identity, saved, fs.save, document.scan |
ShellBridge.kt |
| Windows | windows |
auth.reLogin, external.open, menu.changed, shell.openPane, device.identity |
— | external.open, shell.pane, device.identity |
PlatformPage.xaml.cs |
Бүх клиент ижил:
- Origin/main-frame шалгалт: macOS/iOS
WKFrameInfo.isMainFrame+securityOrigin, WindowsWebMessageReceived-ийнSourceбаCoreWebView2.Source, AndroidaddWebMessageListener-ийн origin rule +isMainFrame+sourceOrigin. Гурвуулаа зөвхөн өөрийн origin-ыг хүлээн авна (§8.1–8.2). - Navigation allowlist (§8.5): macOS/iOS
webView(_:decidePolicyFor:decisionHandler:)(гол frame, дэд frame,targetFrame == nil) +createWebViewWith; WindowsNavigationStarting,FrameNavigationStarting,NewWindowRequested; AndroidshouldOverrideUrlLoading(гол frame ба дэд frame-ийн http(s) бус scheme — WebView дэд frame-ийн http(s) шилжилтийг энэ callback-аар дамжуулдаггүй). - Android-ийн нөхцөл: гүүр нь System WebView
WEB_MESSAGE_LISTENERбаDOCUMENT_START_SCRIPT-ийг дэмжих үед л суурилагдана; эс бөгөөсwindow.GeregeShellбайхгүй, web тал fallback-аа ажиллуулна.
Хэрэгжээгүй зүйлс — дуудвал reject, web тал fallback-аа ажиллуулна:
notify.show,badge.set— ямар ч бүрхүүл хэрэгжүүлээгүй.fs.saveAs— зөвхөн iOS, Android.biometric.authenticate,auth.lock— macOS, iOS (LAContext,.deviceOwnerAuthentication), Android (BiometricPrompt, биометр эсвэл PIN) хэрэгжүүлсэн; Windows зарлаагүй.saved.*,document.scan— зөвхөн iOS, Android (утасны офлайн жагсаалт, баримт скан).print.system— зөвхөн macOS.shell.openPane— macOS, Windows (iOS/Android дээр тохиргоо, хадгалсан жагсаалт нь бүрхүүлийн дээд мөрний товч бөгөөд ажлын мужаас route дүрмээрnative:settings/native:savedгэж нээнэ).- Device method-ууд (
escpos.*,scanner.*,serial.transact,camera.scan,kiosk.lockdown,telemetry.emit,payments.start) — kiosk/pos клиент хараахан байхгүй. - Event — ямар ч бүрхүүл илгээдэггүй.
shell:navigate/shell:searchнь native цэс/хайлт байх үед л зарлагдана; одоогийн клиентүүдэд тэдгээр алга.
menu.native зарлагдаагүй: навигацийн цэсийг ажлын муж өөрөө зурдаг тул
shell:menu-refresh мөн илгээгддэггүй — menu.changed дуудлагыг хүлээж авч,
хариу нь амжилттай гэдгийг л баталгаажуулна.
Deep link¶
| Клиент | gerege-nexus://auth |
|---|---|
| iOS | onOpenURL → AuthCallback.parse → нэвтрэх дэлгэцийн poll-ыг шууд сэргээнэ. |
| Android | Manifest-ийн intent-filter (scheme + host="auth"); singleTop Activity-г нүүр рүү л авчирна, URL-ийн агуулгыг уншдаггүй. |
| macOS | AppDelegate.application(_:open:) → AuthCallback.parse → цонхыг идэвхжүүлнэ. SwiftUI onOpenURL БИШ — WindowGroup гадаад URL-д хоёр дахь цонх нээдэг (§1a). Ширээн дээр callbackUrl хоосон илгээгддэг тул eID апп энэ холбоосыг ердийн урсгалд нээхгүй. |
| Windows | Бүртгэгдээгүй (uap:Protocol байхгүй) нь санаатай: ширээн дээр зөвшөөрөгч нь ӨӨР төхөөрөмж (QR/push) бөгөөд буцах хаяг хэрэггүй. |
10. Гараар шалгах хувилбарууд¶
Бүрхүүлийн өөрчлөлт бүрийн дараа дараах жагсаалтыг гүйцэтгэнэ.
A. Хөтчийн горим өөрчлөгдөөгүй эсэх
cd frontend && npm run dev, дараа нь хөтчөөрhttp://localhost:3000руу орж нэвтэрнэ.- Толгой хэсэг, хажуугийн цэс, мобайл таб бүгд урьдын адил байгааг харна.
- DevTools → Elements:
<html>дээрdata-shellатрибут байхгүй. - Console:
window.GeregeShell→undefined.
B. Бүрхүүлийн горимд chrome нуугдсан эсэх
- Тухайн native target-ыг ажиллуулж, native дэлгэцээр нэвтэрнэ.
- Нэвтэрсний дараа
gerege-topbar, хажуугийн цэс, мобайл таб аль нь ч зурагдаагүй; зөвхөн ажлын муж ба AI туслах харагдана. - Апп Стороос модуль асаагаад/унтраагаад цэсний өгөгдөл шинэчлэгдэж байгааг шалгана — өгөгдлийн fetch нь зурагдахгүй ч ажиллаж байх ёстой.
C. data-shell тавигдсан эсэх
- Аппын цонхон дээр баруун товшоод Inspect Element (dev build).
- Elements:
<html data-shell="macos" ...>— эсвэлwindows/ios/android. - Console:
getComputedStyle(document.body).fontFamily— Inter биш, системийн фонтоор эхэлсэн байна.
D. Дэмжигдээгүй method няцаагдаж байгаа эсэх
Console дээр:
- Promise reject хийнэ — ямар ч бүрхүүл
notify.show-г хэрэгжүүлээгүй (§9). window.GeregeShell.capabilitiesдоторnotifyбайхгүйг шалгана: web тал fallback-аа ажиллуулах болзол нь тэр.- Хэрэгжсэнийг шалгана:
await window.GeregeShell.invoke("device.identity")→{ platform, form_factor, app }.
E. iframe-ээс гүүр дуудагдахгүй эсэх
Console дээр:
const f = document.createElement("iframe");
document.body.appendChild(f);
f.contentWindow.GeregeShell; // undefined байх ёстой
f.contentWindow.webkit?.messageHandlers?.geregeShell; // undefined байх ёстой
F. Гадаад URL webview дотор нээгдэхгүй эсэх
- Console дээр
window.location.href = "https://example.com". - Хуудас системийн хөтөч дээр нээгдэнэ; аппын цонх өөрчлөгдөхгүй үлдэнэ.
window.open("https://example.com")— мөн адил гадаад хөтчөөр нээгдэнэ.- Аппын дотоод холбоос (жишээ нь
/apps) хэвийн ажиллана.
G. Нэг хүрээний дүрэм зөрчигдөөгүй эсэх (§1a)
- Тохиргоог нээнэ: цэснээс, rail/tab-аас, мөн console дээр
await window.GeregeShell.invoke("shell.openPane", { pane: "settings" }). - Гурвуулын дараа шинэ цонх гараагүй: macOS дээр Dock дүрс нэг хэвээр, Windows дээр taskbar-д нэг бичлэг, Android дээр Recents-д нэг entry.
- Тохиргоон дээр байхад толгойн ribbon/menu, rail/tab bar, footer бүгд байрандаа хэвээр — зөвхөн дунд хэсэг солигдсон.
- Ажлын муж руу буцаад хуудас дахин ачаалагдаагүй эсэхийг шалгана: тохиргоо руу орохын өмнө хуудсыг доош гүйлгээд, буцаж ирэхэд ижил байрлалдаа байх ёстой.
H. Төхөөрөмжийн domain шугам зөв эсэх (§1b)
- Ажлын мужийн console дээр
location.hostнь нийтийн гарал,navigator.userAgentньGeregeShell/<шугам>-аар төгсөнө. - Network таб:
/api/v1/...дуудлагууд ижил host руу явж байна;OPTIONSpreflight огт байхгүй. document.cookie-дsession_tokenхарагдахгүй (HttpOnly) ч API дуудлага 200 буцааж байна — cookie same-origin-оор явж байгаагийн шинж.- Бүрхүүл дотор
/loginруу орвол шугамын нүүр (/line/<шугам>) руу шилжинэ — тэнд нэвтрэлт нь native UI. - Хөтчөөр
https://nexus.gerege.netхэвээр бүрэн web app: толгой хэсэг, хажуугийн цэс,/loginбүгд урьдын адил.
I. Функциональ регресс байхгүй эсэх
- Цэсний мөрийн Хайх (⌘F) → үг бичиж Enter дарахад ажлын мужид хайлтын давхарга нээгдэж, үр дүн гарна.
- Toolbar-ын Апп Стор / E-Sign / Төрийн үйлчилгээ товчнууд шилжүүлнэ.
- Хэвлэх (⌘P), файл татах (Save panel), tray цэс ажиллана; iOS дээр
gerege-nexus://auth?sessionId=xхолбоос нэвтрэх дэлгэцийн poll-ыг сэргээнэ,gerege-nexus://auth?next=https://evilюу ч хийхгүй.
11. Модулийн өөрийн хуудас (/r/)¶
Compiled модуль дэлгэцээ өөрөө үйлчилж болно (nexus.PageServer,
MODULES). Web бүрхүүл тэр хуудсыг ижил
origin-ы iframe-д, өөрийн Layout дотор (цэс, tenant сэлгэгч, native shell
хэвээр) зурна (frontend/components/ModuleFrame.tsx).
| Бүрхүүлийн зам | Frame-ийн хаяг | Gate |
|---|---|---|
/module/frame/<slug>/<rest> |
/r/<slug>/<rest> |
workspace (байгууллага) |
/module/frame/me/<slug>/<rest> |
/r/me/<slug>/<rest> |
person |
/cp/apps/<slug>/<rest> |
/r/cp/<slug>/<rest> |
консол (оператор) |
Бүрхүүл frame-ийн замын хэсэг бүрийг [A-Za-z0-9._~-]-ээр шалгаж, ./..-г
хаяна — бүрхүүлийн URL-ийг гараар бичсэн ч /r/-ээс гадагш юу ч frame хийхгүй.
Контекст. Frame нь хоёр замаар контекст авна:
- Хаяг:
?tenant=<id>&lang=<locale>&theme=<light|dark>— хуудас анх нээгдэх үеийнх. Консолдtenantхоосон. - Мессеж: бүрхүүл frame ачаалагдах бүрд (
load) ба хэл, харагдац, tenant өөрчлөгдөх бүрд{type: "nexus:context", tenant, lang, theme}илгээнэ;targetOriginнь бүрхүүлийн өөрийн origin. Хэл, харагдац солигдоход хуудас дахин ачаалагдахгүй (зөвхөн мессеж); tenant солигдоход хаяг өөрчлөгдөж хуудас дахин ачаалагдана — хуучин tenant-д зурсан хуудас шинэ session дотор үлдэхгүй.
Хуудас мессежийг event.origin === location.origin ба
event.data.type === "nexus:context" үед л уншина. Мессеж нь мэдээлэл: эрх,
tenant-ийн жинхэнэ хариулт нь session (nexus.WorkspaceOf) — query-г итгэл
болгохгүй.
Толгой. API зөвхөн /r/ дор X-Frame-Options: SAMEORIGIN ба
Content-Security-Policy: …; frame-ancestors 'self' илгээнэ; бусад бүх хариу
DENY хэвээр. Бүрхүүлийн CSP frame-src 'self' https: (https: нь
mini app-д). Embed шугамд (өөр платформ дотор) модулийн хуудас
ажиллахгүй: frame-ancestors 'self' бүх өвөг frame-ийг шалгадаг.
Native бүрхүүл. Дэд frame нь ижил https origin тул §8.5-ын дүрмээр ачаалагдана;
гүүр (window.GeregeShell) зөвхөн гол frame-д (§8.2) — модулийн хуудас native
method дуудахгүй.
CSRF. Frame доторх fetch("/api/v1/<module>/…", {credentials: "same-origin"})
нь Sec-Fetch-Site: same-origin тул cookie-тэй бичих хүсэлт CSRFMiddleware-ийг
давна; өөр origin-оос ирсэн хүсэлт 403 хэвээр.