Compare commits
5 Commits
f2d4f8e04b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fdb15ae462 | |||
| 2acf5dd50b | |||
| 042c4d6550 | |||
| 747ed623ab | |||
| b8ddf8f22d |
@@ -48,7 +48,7 @@ vpn-lemanapro.sh --patch-only
|
|||||||
|
|
||||||
- `vpn` / `vpn-auto`: hidden browser, autofill, auto-submit.
|
- `vpn` / `vpn-auto`: hidden browser, autofill, auto-submit.
|
||||||
- `vpn --manual` / `vpn-manual`: visible browser, autofill, no submit.
|
- `vpn --manual` / `vpn-manual`: visible browser, autofill, no submit.
|
||||||
- `vpn --manual-full` / `vpn-manual-full`: visible browser, no autofill, no submit.
|
- `vpn --manual-full` / `vpn-manual-full`: visible browser, no autofill, no submit, and no saved LDAP/TOTP preflight.
|
||||||
- `vpn-debug`: visible browser and raw logs.
|
- `vpn-debug`: visible browser and raw logs.
|
||||||
|
|
||||||
When diagnosing SSO, use manual mode first. Do not repeatedly start automatic mode if a connection attempt is already in progress.
|
When diagnosing SSO, use manual mode first. Do not repeatedly start automatic mode if a connection attempt is already in progress.
|
||||||
|
|||||||
27
.understand-anything/.understandignore
Normal file
27
.understand-anything/.understandignore
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# .understandignore - patterns for files/dirs to exclude from analysis
|
||||||
|
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
|
||||||
|
# Lines below are suggestions - uncomment to activate.
|
||||||
|
# Use ! prefix to force-include something excluded by defaults.
|
||||||
|
#
|
||||||
|
# Built-in defaults (always excluded unless negated):
|
||||||
|
# node_modules/, .git/, dist/, build/, obj/, *.lock, *.min.js, etc.
|
||||||
|
#
|
||||||
|
.understand-anything/
|
||||||
|
|
||||||
|
# --- From .gitignore (uncomment to exclude) ---
|
||||||
|
|
||||||
|
# .DS_Store
|
||||||
|
# *.tmp
|
||||||
|
# .build/
|
||||||
|
# app/.build/
|
||||||
|
|
||||||
|
# --- Detected directories (uncomment to exclude) ---
|
||||||
|
|
||||||
|
# tests/
|
||||||
|
# docs/
|
||||||
|
|
||||||
|
# --- Test file patterns (uncomment to exclude) ---
|
||||||
|
|
||||||
|
# *.test.*
|
||||||
|
# *.spec.*
|
||||||
|
# *.snap
|
||||||
3
.understand-anything/config.json
Normal file
3
.understand-anything/config.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"outputLanguage": "ru"
|
||||||
|
}
|
||||||
733
.understand-anything/fingerprints.json
Normal file
733
.understand-anything/fingerprints.json
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"gitCommitHash": "042c4d655090e0ee334208511ec63b6d909d4354",
|
||||||
|
"generatedAt": "2026-06-23T06:34:02.856Z",
|
||||||
|
"files": {
|
||||||
|
".agents/skills/lemana-vpn-operations/SKILL.md": {
|
||||||
|
"filePath": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"contentHash": "27cec75695c23c1dc759d61b5b6250306f3b494688aabc794014711b59aa89ab",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 71,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
".agents/skills/lemana-vpn-sso-autofill/SKILL.md": {
|
||||||
|
"filePath": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"contentHash": "e597d0e67861787f5c444c74376bdd514614765dbe27dd62b62f7e61925a643c",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 81,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
".agents/skills/lemana-vpn-testing/SKILL.md": {
|
||||||
|
"filePath": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"contentHash": "6f4698688ef498f4f1427024a735d7d36cb96a96f36bf8597977fdc28e3ffa13",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 102,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"AGENTS.md": {
|
||||||
|
"filePath": "AGENTS.md",
|
||||||
|
"contentHash": "3ccea1ed9dacb6f1173a03d8053c5466fc03aebcadb6dbf4db52814c82bc6a92",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 89,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"app/Package.swift": {
|
||||||
|
"filePath": "app/Package.swift",
|
||||||
|
"contentHash": "338b2954b4fde437b35e7b419d7aff8451d91df790429a168d80d19bf5b99f75",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 15,
|
||||||
|
"hasStructuralAnalysis": false
|
||||||
|
},
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": {
|
||||||
|
"filePath": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"contentHash": "0caddaae55a376d20d023c3e82b525fa2a54002a2730978e22826aad56574474",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 191,
|
||||||
|
"hasStructuralAnalysis": false
|
||||||
|
},
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": {
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"contentHash": "16b7da9acd2bad9ff71f3c067ae2bd420c3ef11cab4dc6d9c509be2d0a011d67",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 698,
|
||||||
|
"hasStructuralAnalysis": false
|
||||||
|
},
|
||||||
|
"bin/vpn-lemanapro.sh": {
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"contentHash": "e1946792a405d2178b197ea11285029ef172a65dd9f9c6b4a8f308e0ac58bbc2",
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "_emit",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_write_status",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_clear_status",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_prepare_connection_log",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_log_connection_line",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_show_connection_log_tail",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_start_connect_progress",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_stop_connect_progress",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_json_get",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_find_webengine_process",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_find_authenticator",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_bool",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_patches_active",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_keychain_has",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_status_json",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_human_part",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_status_human",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_check_status",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 63
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_patch_oc",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 228
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "autoFill",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "autoFill",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_store_keychain",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 16
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_normalize_totp_secret",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 27
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_can_prompt",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_prompt_read",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_persist_username",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_ensure_username",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_configure_keychain",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_ensure_keychain_credentials",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_bw_cache_session",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_bw_unlock",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 67
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_sync_bitwarden",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_sync_credentials",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_dns_cleanup",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_filter_output",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 51
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 1091,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"docs/goals/manual-first-auth-flow/GOAL.md": {
|
||||||
|
"filePath": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"contentHash": "27fc0e7feddd4c77f78f1966ab1e58c3727b4796f8600003e31e81b907b6ebe7",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 14,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"docs/goals/manual-first-auth-flow/PLAN.md": {
|
||||||
|
"filePath": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"contentHash": "459cb4b63f0ea0eee13ed6c8dd91d4bae1be7adaf443b2824a93d2217cbe1c58",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 441,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"docs/superpowers/plans/2026-05-24-keychain-totp-provider.md": {
|
||||||
|
"filePath": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"contentHash": "1a5d622b3bb8c6277d5d97effbdd07713975b4595bec236ff8710abde772c5dc",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 936,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"install.sh": {
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"contentHash": "d33186be9dc6457ca80886f629995edf21143d091b6902814853424400a1ec6f",
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "usage",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color_line",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_step",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_info",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_detail",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_ok",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_warn",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_skip",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "die",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "run",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "need_cmd",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "has_tty",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interactive_enabled",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prompt_printf",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prompt_read_answer",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yes_no",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 27
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "choose_credential_source_interactive",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bool_word",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keychain_has",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zsh_aliases_installed",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "print_detected_state",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "choose_modules",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "script_dir",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "download_file",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "write_file",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_homebrew_packages",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 27
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_openconnect_lite",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_cli",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_config",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_dns_cleanup",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 14
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_sudoers",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_touchid_helper",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_menu_bar_app",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_launch_agent",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 41
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_running_menu_bar_app",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_shell_aliases",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vpn",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maybe_login_bitwarden",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 40
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 944,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": {
|
||||||
|
"filePath": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"contentHash": "9338f2615ba4ef133b42b07dcdbc9e54ddf59b301fd1434e81ea3f36bba16dfb",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 24,
|
||||||
|
"hasStructuralAnalysis": false
|
||||||
|
},
|
||||||
|
"README.md": {
|
||||||
|
"filePath": "README.md",
|
||||||
|
"contentHash": "fa2024f70a4eb7556b501ec01f5c74d7981939d20a805bdbb4a534853618560d",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 580,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"templates/openconnect-lite-config.toml": {
|
||||||
|
"filePath": "templates/openconnect-lite-config.toml",
|
||||||
|
"contentHash": "e3378bc1f313fd8087886e582f9ec402268649125db4b0320365b5c3f65611b6",
|
||||||
|
"functions": [],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 33,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"tests/smoke.sh": {
|
||||||
|
"filePath": "tests/smoke.sh",
|
||||||
|
"contentHash": "1cf9648a22e831c0693d328edcbc0e5e6561084244a20dfb37d95dd6f7f8f23e",
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "autoFill",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 439,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
},
|
||||||
|
"uninstall.sh": {
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"contentHash": "6ad75a4fbbe52575eb3672a1eac06734e13f4a82d77cba17dfe2aee75b6ecfba",
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "usage",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color_line",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_step",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_info",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_detail",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_ok",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_warn",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_skip",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "die",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "run",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "find_webengine_process",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restore_openconnect_lite_patch",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_zshrc_block",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_keychain_entries",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"params": [],
|
||||||
|
"exported": false,
|
||||||
|
"lineCount": 83
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"classes": [],
|
||||||
|
"imports": [],
|
||||||
|
"exports": [],
|
||||||
|
"totalLines": 321,
|
||||||
|
"hasStructuralAnalysis": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
.understand-anything/intermediate/assemble-review.json
Normal file
11
.understand-anything/intermediate/assemble-review.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"warnings": [],
|
||||||
|
"stats": {
|
||||||
|
"nodes": 73,
|
||||||
|
"edges": 56,
|
||||||
|
"duplicates": 0,
|
||||||
|
"dangling": 0,
|
||||||
|
"missingFiles": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
2037
.understand-anything/intermediate/assembled-graph.json
Normal file
2037
.understand-anything/intermediate/assembled-graph.json
Normal file
File diff suppressed because it is too large
Load Diff
634
.understand-anything/intermediate/batch-1.json
Normal file
634
.understand-anything/intermediate/batch-1.json
Normal file
@@ -0,0 +1,634 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "document:AGENTS.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "AGENTS.md",
|
||||||
|
"filePath": "AGENTS.md",
|
||||||
|
"summary": "Репозиторные правила для агентов: модель системы, live VPN safety, тестовая лестница и контракт финального ответа.",
|
||||||
|
"tags": [
|
||||||
|
"agent-rules",
|
||||||
|
"documentation",
|
||||||
|
"safety"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "document:README.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "README.md",
|
||||||
|
"filePath": "README.md",
|
||||||
|
"summary": "Основная пользовательская документация Lemana VPN: установка, credential sources, модули, использование, диагностика и удаление.",
|
||||||
|
"tags": [
|
||||||
|
"documentation",
|
||||||
|
"overview",
|
||||||
|
"user-guide"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:install.sh",
|
||||||
|
"type": "file",
|
||||||
|
"name": "install.sh",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"summary": "Installer wizard для Homebrew/pipx/openconnect-lite, CLI, config, DNS wrapper, sudoers, Touch ID helper, Swift app, launch agent и shell aliases.",
|
||||||
|
"tags": [
|
||||||
|
"installer",
|
||||||
|
"wizard",
|
||||||
|
"macos"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:usage",
|
||||||
|
"type": "function",
|
||||||
|
"name": "usage",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
72,
|
||||||
|
106
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция usage в install.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"usage"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:yes_no",
|
||||||
|
"type": "function",
|
||||||
|
"name": "yes_no",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
304,
|
||||||
|
330
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция yes_no в install.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"yes-no"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:choose_credential_source_interactive",
|
||||||
|
"type": "function",
|
||||||
|
"name": "choose_credential_source_interactive",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
332,
|
||||||
|
382
|
||||||
|
],
|
||||||
|
"summary": "Показывает интерактивный выбор credential source и нормализует ответ пользователя.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"choose-credential-source-interactive"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:print_detected_state",
|
||||||
|
"type": "function",
|
||||||
|
"name": "print_detected_state",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
400,
|
||||||
|
417
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция print_detected_state в install.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"print-detected-state"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:choose_modules",
|
||||||
|
"type": "function",
|
||||||
|
"name": "choose_modules",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
419,
|
||||||
|
514
|
||||||
|
],
|
||||||
|
"summary": "Определяет набор устанавливаемых модулей с учетом интерактивного режима и флагов installer.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"choose-modules"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:download_file",
|
||||||
|
"type": "function",
|
||||||
|
"name": "download_file",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
523,
|
||||||
|
540
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция download_file в install.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"download-file"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_homebrew_packages",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_homebrew_packages",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
552,
|
||||||
|
578
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция install_homebrew_packages в install.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-homebrew-packages"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_openconnect_lite",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_openconnect_lite",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
580,
|
||||||
|
597
|
||||||
|
],
|
||||||
|
"summary": "Устанавливает или проверяет openconnect-lite runtime, необходимый для SAML SSO.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-openconnect-lite"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_cli",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_cli",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
599,
|
||||||
|
613
|
||||||
|
],
|
||||||
|
"summary": "Копирует основной CLI и uninstall helper в пользовательский bin.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-cli"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_config",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_config",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
615,
|
||||||
|
639
|
||||||
|
],
|
||||||
|
"summary": "Устанавливает шаблон openconnect-lite config и сохраняет локальную конфигурацию модулей.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-config"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_dns_cleanup",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_dns_cleanup",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
641,
|
||||||
|
654
|
||||||
|
],
|
||||||
|
"summary": "Устанавливает root-owned DNS cleanup wrapper для безопасного сброса корпоративных DNS.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-dns-cleanup"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_sudoers",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_sudoers",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
656,
|
||||||
|
683
|
||||||
|
],
|
||||||
|
"summary": "Пишет ограниченные sudoers правила для openconnect и DNS cleanup wrapper.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-sudoers"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_touchid_helper",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_touchid_helper",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
685,
|
||||||
|
708
|
||||||
|
],
|
||||||
|
"summary": "Собирает и устанавливает Touch ID helper для Bitwarden master password.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-touchid-helper"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_menu_bar_app",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_menu_bar_app",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
710,
|
||||||
|
769
|
||||||
|
],
|
||||||
|
"summary": "Собирает release-версию Swift menu bar app и устанавливает LemanaVPN.app.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_launch_agent",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_launch_agent",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
771,
|
||||||
|
811
|
||||||
|
],
|
||||||
|
"summary": "Создает LaunchAgent для автозапуска menu bar app при логине.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-launch-agent"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:restart_running_menu_bar_app",
|
||||||
|
"type": "function",
|
||||||
|
"name": "restart_running_menu_bar_app",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
813,
|
||||||
|
831
|
||||||
|
],
|
||||||
|
"summary": "Перезапускает уже открытое menu bar приложение после обновления bundle.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"restart-running-menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:install_shell_aliases",
|
||||||
|
"type": "function",
|
||||||
|
"name": "install_shell_aliases",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
833,
|
||||||
|
877
|
||||||
|
],
|
||||||
|
"summary": "Добавляет idempotent zsh aliases для CLI режимов подключения и диагностики.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"install-shell-aliases"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:maybe_login_bitwarden",
|
||||||
|
"type": "function",
|
||||||
|
"name": "maybe_login_bitwarden",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
879,
|
||||||
|
900
|
||||||
|
],
|
||||||
|
"summary": "Проверяет Bitwarden session и при необходимости запускает login/unlock flow.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"maybe-login-bitwarden"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:install.sh:main",
|
||||||
|
"type": "function",
|
||||||
|
"name": "main",
|
||||||
|
"filePath": "install.sh",
|
||||||
|
"lineRange": [
|
||||||
|
902,
|
||||||
|
941
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция main в install.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:uninstall.sh",
|
||||||
|
"type": "file",
|
||||||
|
"name": "uninstall.sh",
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"summary": "Uninstaller для восстановления runtime patch backups, удаления CLI/app/config/sudoers/aliases и опциональной очистки Keychain/Touch ID state.",
|
||||||
|
"tags": [
|
||||||
|
"uninstaller",
|
||||||
|
"cleanup",
|
||||||
|
"macos"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:uninstall.sh:usage",
|
||||||
|
"type": "function",
|
||||||
|
"name": "usage",
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"lineRange": [
|
||||||
|
62,
|
||||||
|
79
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция usage в uninstall.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"usage"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:uninstall.sh:restore_openconnect_lite_patch",
|
||||||
|
"type": "function",
|
||||||
|
"name": "restore_openconnect_lite_patch",
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"lineRange": [
|
||||||
|
161,
|
||||||
|
181
|
||||||
|
],
|
||||||
|
"summary": "Возвращает backup исходника openconnect-lite, если runtime patch был установлен.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"restore-openconnect-lite-patch"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:uninstall.sh:remove_zshrc_block",
|
||||||
|
"type": "function",
|
||||||
|
"name": "remove_zshrc_block",
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"lineRange": [
|
||||||
|
183,
|
||||||
|
204
|
||||||
|
],
|
||||||
|
"summary": "Удаляет idempotent блок Lemana VPN aliases из zshrc.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"remove-zshrc-block"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:uninstall.sh:remove_keychain_entries",
|
||||||
|
"type": "function",
|
||||||
|
"name": "remove_keychain_entries",
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"lineRange": [
|
||||||
|
206,
|
||||||
|
234
|
||||||
|
],
|
||||||
|
"summary": "Опционально удаляет password, TOTP seed и username из macOS Keychain.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"remove-keychain-entries"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:uninstall.sh:main",
|
||||||
|
"type": "function",
|
||||||
|
"name": "main",
|
||||||
|
"filePath": "uninstall.sh",
|
||||||
|
"lineRange": [
|
||||||
|
236,
|
||||||
|
318
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция main в uninstall.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:usage",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:yes_no",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:choose_credential_source_interactive",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:print_detected_state",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:choose_modules",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:download_file",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_homebrew_packages",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_openconnect_lite",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_cli",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_config",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_dns_cleanup",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_sudoers",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_touchid_helper",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_menu_bar_app",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_launch_agent",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:restart_running_menu_bar_app",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:install_shell_aliases",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:maybe_login_bitwarden",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:install.sh",
|
||||||
|
"target": "function:install.sh:main",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:uninstall.sh",
|
||||||
|
"target": "function:uninstall.sh:usage",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:uninstall.sh",
|
||||||
|
"target": "function:uninstall.sh:restore_openconnect_lite_patch",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:uninstall.sh",
|
||||||
|
"target": "function:uninstall.sh:remove_zshrc_block",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:uninstall.sh",
|
||||||
|
"target": "function:uninstall.sh:remove_keychain_entries",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:uninstall.sh",
|
||||||
|
"target": "function:uninstall.sh:main",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
943
.understand-anything/intermediate/batch-2.json
Normal file
943
.understand-anything/intermediate/batch-2.json
Normal file
@@ -0,0 +1,943 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "document:.agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "Lemana VPN Operations skill",
|
||||||
|
"filePath": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"summary": "Инструкции для операционных задач: установка, обновление, статусы, безопасные ответы пользователю и live-connection правила.",
|
||||||
|
"tags": [
|
||||||
|
"skill",
|
||||||
|
"operations",
|
||||||
|
"documentation"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "document:.agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "Lemana VPN SSO skill",
|
||||||
|
"filePath": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"summary": "Инструкции по Bitwarden, Keychain, TOTP, Keycloak SSO, autofill и runtime-патчам openconnect-lite.",
|
||||||
|
"tags": [
|
||||||
|
"skill",
|
||||||
|
"sso",
|
||||||
|
"credentials"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "document:.agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "Lemana VPN Testing skill",
|
||||||
|
"filePath": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"summary": "Инструкции по non-live проверкам, status checks, live VPN safety и финальному отчету для изменений в репозитории.",
|
||||||
|
"tags": [
|
||||||
|
"skill",
|
||||||
|
"testing",
|
||||||
|
"verification"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:app/Package.swift",
|
||||||
|
"type": "file",
|
||||||
|
"name": "Package.swift",
|
||||||
|
"filePath": "app/Package.swift",
|
||||||
|
"summary": "Swift Package manifest для сборки executable target LemanaVPN под macOS 13+.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"build-system",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"type": "file",
|
||||||
|
"name": "LemanaVPNApp.swift",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"summary": "SwiftUI/AppKit menu bar surface: отображает состояние VPN, кнопки подключения, логи и действия пользователя.",
|
||||||
|
"tags": [
|
||||||
|
"swiftui",
|
||||||
|
"menu-bar",
|
||||||
|
"ui"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/LemanaVPNApp.swift:LemanaVPNApp",
|
||||||
|
"type": "class",
|
||||||
|
"name": "LemanaVPNApp",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"lineRange": [
|
||||||
|
5,
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"summary": "Точка входа SwiftUI menu bar app, связывает VPNManager с MenuBarExtra.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/LemanaVPNApp.swift:StatusBarLabel",
|
||||||
|
"type": "class",
|
||||||
|
"name": "StatusBarLabel",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"lineRange": [
|
||||||
|
17,
|
||||||
|
51
|
||||||
|
],
|
||||||
|
"summary": "Компактный status bar label с иконкой и оставшимся временем сессии.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/LemanaVPNApp.swift:VPNMenuView",
|
||||||
|
"type": "class",
|
||||||
|
"name": "VPNMenuView",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"lineRange": [
|
||||||
|
52,
|
||||||
|
191
|
||||||
|
],
|
||||||
|
"summary": "Основной menu UI с командами auto/manual/manual-full connect, disconnect, logs и quit.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"type": "file",
|
||||||
|
"name": "VPNManager.swift",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"summary": "Основной state manager Swift-приложения: запускает CLI в JSON mode, декодирует события, отслеживает health и управляет reconnect/disconnect.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"state-manager",
|
||||||
|
"cli-bridge"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNEvent",
|
||||||
|
"type": "class",
|
||||||
|
"name": "VPNEvent",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
4,
|
||||||
|
17
|
||||||
|
],
|
||||||
|
"summary": "Decodable-модель JSON events, которые runtime CLI отправляет в Swift app.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNStatusResponse",
|
||||||
|
"type": "class",
|
||||||
|
"name": "VPNStatusResponse",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
18,
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"summary": "Decodable envelope для ответа `vpn --status --json`.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/VPNManager.swift:ModuleStatus",
|
||||||
|
"type": "class",
|
||||||
|
"name": "ModuleStatus",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
22,
|
||||||
|
101
|
||||||
|
],
|
||||||
|
"summary": "Модель статуса модулей и вычисление summary/systemImage для menu bar app.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNState",
|
||||||
|
"type": "class",
|
||||||
|
"name": "VPNState",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
102,
|
||||||
|
121
|
||||||
|
],
|
||||||
|
"summary": "Finite state model для disconnected/connecting/connected/reconnecting/error состояний.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNLaunchMode",
|
||||||
|
"type": "class",
|
||||||
|
"name": "VPNLaunchMode",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
122,
|
||||||
|
136
|
||||||
|
],
|
||||||
|
"summary": "Маппинг app launch modes на CLI flags `--auto`, `--manual`, `--manual-full`.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"menu-bar-app"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNManager",
|
||||||
|
"type": "class",
|
||||||
|
"name": "VPNManager",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
137,
|
||||||
|
698
|
||||||
|
],
|
||||||
|
"summary": "ObservableObject, который мостит Swift menu bar app с shell CLI, JSON events и health checks.",
|
||||||
|
"tags": [
|
||||||
|
"swift",
|
||||||
|
"type",
|
||||||
|
"state-manager"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:app/Sources/LemanaVPN/VPNManager.swift:refreshStatus",
|
||||||
|
"type": "function",
|
||||||
|
"name": "refreshStatus",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
194,
|
||||||
|
248
|
||||||
|
],
|
||||||
|
"summary": "Запускает CLI status JSON и обновляет module summary в menu bar app.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"refreshStatus"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:app/Sources/LemanaVPN/VPNManager.swift:connect",
|
||||||
|
"type": "function",
|
||||||
|
"name": "connect",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
249,
|
||||||
|
326
|
||||||
|
],
|
||||||
|
"summary": "Запускает CLI в JSON mode с выбранным режимом подключения и подписывается на stdout/stderr.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"connect"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:app/Sources/LemanaVPN/VPNManager.swift:disconnect",
|
||||||
|
"type": "function",
|
||||||
|
"name": "disconnect",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
327,
|
||||||
|
349
|
||||||
|
],
|
||||||
|
"summary": "Отправляет SIGINT текущему CLI process и останавливает reconnect state.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"disconnect"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:app/Sources/LemanaVPN/VPNManager.swift:parseEvent",
|
||||||
|
"type": "function",
|
||||||
|
"name": "parseEvent",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
350,
|
||||||
|
448
|
||||||
|
],
|
||||||
|
"summary": "Декодирует JSON events из CLI и переводит их в Swift state updates.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"parseEvent"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:app/Sources/LemanaVPN/VPNManager.swift:handleTermination",
|
||||||
|
"type": "function",
|
||||||
|
"name": "handleTermination",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
449,
|
||||||
|
576
|
||||||
|
],
|
||||||
|
"summary": "Обрабатывает завершение CLI process и решает, нужен ли автоматический reconnect.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"handleTermination"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:app/Sources/LemanaVPN/VPNManager.swift:startHealthCheck",
|
||||||
|
"type": "function",
|
||||||
|
"name": "startHealthCheck",
|
||||||
|
"filePath": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"lineRange": [
|
||||||
|
577,
|
||||||
|
666
|
||||||
|
],
|
||||||
|
"summary": "Периодически проверяет tunnel health и запускает reconnect при повторных failures.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"startHealthCheck"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"type": "file",
|
||||||
|
"name": "vpn-lemanapro.sh",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"summary": "Главный runtime CLI: status JSON, sync credentials, Keychain/TOTP, runtime patches, SSO запуск, reconnect loop, DNS cleanup и фильтрация openconnect-lite output.",
|
||||||
|
"tags": [
|
||||||
|
"cli",
|
||||||
|
"runtime",
|
||||||
|
"sso"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_module_bool",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_module_bool",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
197,
|
||||||
|
203
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция _module_bool в bin/vpn-lemanapro.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"module-bool"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_patches_active",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_patches_active",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
205,
|
||||||
|
219
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция _patches_active в bin/vpn-lemanapro.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"patches-active"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_module_status_json",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_module_status_json",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
225,
|
||||||
|
264
|
||||||
|
],
|
||||||
|
"summary": "Собирает машинно-читаемый JSON со статусом модулей, credentials, app, DNS и patches.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"module-status-json"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_module_human_part",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_module_human_part",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
266,
|
||||||
|
275
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция _module_human_part в bin/vpn-lemanapro.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"module-human-part"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_module_status_human",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_module_status_human",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
277,
|
||||||
|
311
|
||||||
|
],
|
||||||
|
"summary": "Печатает человекочитаемую строку статуса модулей перед подключением.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"module-status-human"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_check_status",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_check_status",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
313,
|
||||||
|
375
|
||||||
|
],
|
||||||
|
"summary": "Проверяет PID/status file, running process, DNS target и возвращает disconnected/connected JSON.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"check-status"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_patch_oc",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_patch_oc",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
377,
|
||||||
|
604
|
||||||
|
],
|
||||||
|
"summary": "Применяет runtime patches к openconnect-lite для Keycloak autofill/manual/debug сценариев.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"patch-oc"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_normalize_totp_secret",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_normalize_totp_secret",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
623,
|
||||||
|
649
|
||||||
|
],
|
||||||
|
"summary": "Нормализует TOTP secret из raw secret или otpauth URI перед сохранением.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"normalize-totp-secret"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_persist_username",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_persist_username",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
675,
|
||||||
|
718
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция _persist_username в bin/vpn-lemanapro.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"persist-username"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_ensure_username",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_ensure_username",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
720,
|
||||||
|
745
|
||||||
|
],
|
||||||
|
"summary": "Значимая функция _ensure_username в bin/vpn-lemanapro.sh.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"ensure-username"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_configure_keychain",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_configure_keychain",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
747,
|
||||||
|
785
|
||||||
|
],
|
||||||
|
"summary": "Интерактивно сохраняет LDAP password и TOTP seed в macOS Keychain.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"configure-keychain"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_ensure_keychain_credentials",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_ensure_keychain_credentials",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
787,
|
||||||
|
814
|
||||||
|
],
|
||||||
|
"summary": "Проверяет наличие обязательных Keychain credentials перед auto/manual подключением.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"ensure-keychain-credentials"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_bw_unlock",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_bw_unlock",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
823,
|
||||||
|
889
|
||||||
|
],
|
||||||
|
"summary": "Открывает Bitwarden session с учетом Touch ID helper и fallback prompts.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"bw-unlock"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_sync_bitwarden",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_sync_bitwarden",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
891,
|
||||||
|
925
|
||||||
|
],
|
||||||
|
"summary": "Синхронизирует LDAP password и TOTP seed из Bitwarden в macOS Keychain.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"sync-bitwarden"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_sync_credentials",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_sync_credentials",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
927,
|
||||||
|
936
|
||||||
|
],
|
||||||
|
"summary": "Выбирает credential source и запускает соответствующий sync/preflight путь.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"sync-credentials"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_dns_cleanup",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_dns_cleanup",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
938,
|
||||||
|
954
|
||||||
|
],
|
||||||
|
"summary": "Вызывает narrow sudo wrapper для очистки VPN DNS при завершении сессии.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"dns-cleanup"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "function:bin/vpn-lemanapro.sh:_filter_output",
|
||||||
|
"type": "function",
|
||||||
|
"name": "_filter_output",
|
||||||
|
"filePath": "bin/vpn-lemanapro.sh",
|
||||||
|
"lineRange": [
|
||||||
|
956,
|
||||||
|
1006
|
||||||
|
],
|
||||||
|
"summary": "Фильтрует поток openconnect-lite, извлекает IP/expiry, пишет status JSON и события для app.",
|
||||||
|
"tags": [
|
||||||
|
"function",
|
||||||
|
"runtime",
|
||||||
|
"filter-output"
|
||||||
|
],
|
||||||
|
"complexity": "moderate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "document:docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "Manual-first auth goal",
|
||||||
|
"filePath": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"summary": "Краткое описание цели manual-first authentication flow и ожидаемого пользовательского результата.",
|
||||||
|
"tags": [
|
||||||
|
"goal",
|
||||||
|
"auth-flow",
|
||||||
|
"planning"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "document:docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "Manual-first auth plan",
|
||||||
|
"filePath": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"summary": "Подробный план изменения installer UX, CLI auth profiles, Swift menu-bar surface и smoke coverage для manual-first сценария.",
|
||||||
|
"tags": [
|
||||||
|
"plan",
|
||||||
|
"auth-flow",
|
||||||
|
"implementation"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "document:docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"type": "document",
|
||||||
|
"name": "Keychain TOTP provider plan",
|
||||||
|
"filePath": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"summary": "Исторический implementation plan для разделения credential sources, нормализации TOTP и расширения smoke-проверок.",
|
||||||
|
"tags": [
|
||||||
|
"plan",
|
||||||
|
"keychain",
|
||||||
|
"totp"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"type": "file",
|
||||||
|
"name": "lemana-vpn-dns-cleanup",
|
||||||
|
"filePath": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"summary": "Узкий root-owned wrapper, который сбрасывает только корпоративные DNS и обновляет macOS resolver cache.",
|
||||||
|
"tags": [
|
||||||
|
"dns",
|
||||||
|
"sudo-wrapper",
|
||||||
|
"cleanup"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "config:templates/openconnect-lite-config.toml",
|
||||||
|
"type": "config",
|
||||||
|
"name": "openconnect-lite-config.toml",
|
||||||
|
"filePath": "templates/openconnect-lite-config.toml",
|
||||||
|
"summary": "Шаблон профиля openconnect-lite: gateway, credentials mode и Keycloak auto-fill selectors для username, password и TOTP.",
|
||||||
|
"tags": [
|
||||||
|
"configuration",
|
||||||
|
"openconnect-lite",
|
||||||
|
"autofill"
|
||||||
|
],
|
||||||
|
"complexity": "simple"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "file:tests/smoke.sh",
|
||||||
|
"type": "file",
|
||||||
|
"name": "smoke.sh",
|
||||||
|
"filePath": "tests/smoke.sh",
|
||||||
|
"summary": "Non-live smoke suite: shell syntax, installer/uninstaller checks, Swift wiring, SSO patch snippets, config selectors и regression assertions.",
|
||||||
|
"tags": [
|
||||||
|
"test",
|
||||||
|
"smoke",
|
||||||
|
"verification"
|
||||||
|
],
|
||||||
|
"complexity": "complex"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/LemanaVPNApp.swift:LemanaVPNApp",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/LemanaVPNApp.swift:StatusBarLabel",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/LemanaVPNApp.swift:VPNMenuView",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNEvent",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNStatusResponse",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/VPNManager.swift:ModuleStatus",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNState",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNLaunchMode",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "class:app/Sources/LemanaVPN/VPNManager.swift:VPNManager",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "function:app/Sources/LemanaVPN/VPNManager.swift:refreshStatus",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "function:app/Sources/LemanaVPN/VPNManager.swift:connect",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "function:app/Sources/LemanaVPN/VPNManager.swift:disconnect",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "function:app/Sources/LemanaVPN/VPNManager.swift:parseEvent",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "function:app/Sources/LemanaVPN/VPNManager.swift:handleTermination",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"target": "function:app/Sources/LemanaVPN/VPNManager.swift:startHealthCheck",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_module_bool",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_patches_active",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_module_status_json",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_module_human_part",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_module_status_human",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_check_status",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_patch_oc",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_normalize_totp_secret",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_persist_username",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_ensure_username",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_configure_keychain",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_ensure_keychain_credentials",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_bw_unlock",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_sync_bitwarden",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_sync_credentials",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_dns_cleanup",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "file:bin/vpn-lemanapro.sh",
|
||||||
|
"target": "function:bin/vpn-lemanapro.sh:_filter_output",
|
||||||
|
"type": "contains",
|
||||||
|
"direction": "forward",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
149
.understand-anything/intermediate/batches.json
Normal file
149
.understand-anything/intermediate/batches.json
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"algorithm": "louvain",
|
||||||
|
"totalFiles": 17,
|
||||||
|
"totalBatches": 2,
|
||||||
|
"exportsByPath": {
|
||||||
|
"app/Package.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": [],
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": []
|
||||||
|
},
|
||||||
|
"batches": [
|
||||||
|
{
|
||||||
|
"batchIndex": 1,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "AGENTS.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 88,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 579,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "install.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 943,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "uninstall.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 320,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batchImportData": {
|
||||||
|
"AGENTS.md": [],
|
||||||
|
"README.md": [],
|
||||||
|
"install.sh": [],
|
||||||
|
"uninstall.sh": []
|
||||||
|
},
|
||||||
|
"neighborMap": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"batchIndex": 2,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 70,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 80,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 101,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Package.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 14,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 190,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 697,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/vpn-lemanapro.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 1090,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 12,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 251,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 935,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"language": "unknown",
|
||||||
|
"sizeLines": 23,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/openconnect-lite-config.toml",
|
||||||
|
"language": "toml",
|
||||||
|
"sizeLines": 32,
|
||||||
|
"fileCategory": "config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/smoke.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 438,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batchImportData": {
|
||||||
|
".agents/skills/lemana-vpn-operations/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-sso-autofill/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-testing/SKILL.md": [],
|
||||||
|
"app/Package.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": [],
|
||||||
|
"bin/vpn-lemanapro.sh": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/GOAL.md": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/PLAN.md": [],
|
||||||
|
"docs/superpowers/plans/2026-05-24-keychain-totp-provider.md": [],
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": [],
|
||||||
|
"templates/openconnect-lite-config.toml": [],
|
||||||
|
"tests/smoke.sh": []
|
||||||
|
},
|
||||||
|
"neighborMap": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
107
.understand-anything/intermediate/import-input.json
Normal file
107
.understand-anything/intermediate/import-input.json
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"projectRoot": "/Users/dokril/git/home/lemana-vpn",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 70,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 80,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 101,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "AGENTS.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 88,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Package.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 14,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 190,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 697,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/vpn-lemanapro.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 1090,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 12,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 251,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 935,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "install.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 943,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"language": "unknown",
|
||||||
|
"sizeLines": 23,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 579,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/openconnect-lite-config.toml",
|
||||||
|
"language": "toml",
|
||||||
|
"sizeLines": 32,
|
||||||
|
"fileCategory": "config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/smoke.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 438,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "uninstall.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 320,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
27
.understand-anything/intermediate/import-map.json
Normal file
27
.understand-anything/intermediate/import-map.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"scriptCompleted": true,
|
||||||
|
"stats": {
|
||||||
|
"filesScanned": 17,
|
||||||
|
"filesWithImports": 0,
|
||||||
|
"totalEdges": 0
|
||||||
|
},
|
||||||
|
"importMap": {
|
||||||
|
".agents/skills/lemana-vpn-operations/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-sso-autofill/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-testing/SKILL.md": [],
|
||||||
|
"AGENTS.md": [],
|
||||||
|
"app/Package.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": [],
|
||||||
|
"bin/vpn-lemanapro.sh": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/GOAL.md": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/PLAN.md": [],
|
||||||
|
"docs/superpowers/plans/2026-05-24-keychain-totp-provider.md": [],
|
||||||
|
"install.sh": [],
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": [],
|
||||||
|
"README.md": [],
|
||||||
|
"templates/openconnect-lite-config.toml": [],
|
||||||
|
"tests/smoke.sh": [],
|
||||||
|
"uninstall.sh": []
|
||||||
|
}
|
||||||
|
}
|
||||||
126
.understand-anything/intermediate/scan-raw.json
Normal file
126
.understand-anything/intermediate/scan-raw.json
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"scriptCompleted": true,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 70,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 80,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 101,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "AGENTS.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 88,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Package.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 14,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 190,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 697,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/vpn-lemanapro.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 1090,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 12,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 251,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 935,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "install.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 943,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"language": "unknown",
|
||||||
|
"sizeLines": 23,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 579,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/openconnect-lite-config.toml",
|
||||||
|
"language": "toml",
|
||||||
|
"sizeLines": 32,
|
||||||
|
"fileCategory": "config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/smoke.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 438,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "uninstall.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 320,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalFiles": 17,
|
||||||
|
"filteredByIgnore": 3,
|
||||||
|
"estimatedComplexity": "small",
|
||||||
|
"stats": {
|
||||||
|
"filesScanned": 17,
|
||||||
|
"byCategory": {
|
||||||
|
"docs": 8,
|
||||||
|
"code": 4,
|
||||||
|
"script": 4,
|
||||||
|
"config": 1
|
||||||
|
},
|
||||||
|
"byLanguage": {
|
||||||
|
"markdown": 8,
|
||||||
|
"swift": 3,
|
||||||
|
"shell": 4,
|
||||||
|
"unknown": 1,
|
||||||
|
"toml": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
.understand-anything/intermediate/scan-result.json
Normal file
163
.understand-anything/intermediate/scan-result.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"name": "Lemana VPN",
|
||||||
|
"projectName": "Lemana VPN",
|
||||||
|
"description": "macOS-пакет для корпоративного VPN Lemana: installer, CLI runtime, SSO/autofill через openconnect-lite, DNS cleanup wrapper и Swift menu bar app.",
|
||||||
|
"rawDescription": "CLI-установка корпоративного VPN vpn.lemanapro.ru для macOS.",
|
||||||
|
"readmeHead": "# Lemana VPN\n\nCLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.\n\n**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; Swift Menu Bar app: включён; автозапуск приложения: включён; runtime-патчи: применяются автоматически перед подключением.\n\n**Credential sources:** `bitwarden` синхронизирует LDAP-пароль и TOTP seed из Bitwarden в macOS Keychain; `keychain` хранит LDAP-пароль и постоянный TOTP seed напрямую в macOS Keychain. Оба источника используют один и тот же runtime `openconnect-lite` для SSO/autofill.\n\nРепозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:\n\n- `openconnect` как VPN-клиент;\n- `openconnect-lite` для SAML SSO через Keycloak;\n- опциональный Bitwarden CLI для LDAP-пароля и TOTP seed;\n- опциональный Touch ID helper для мастер-пароля Bitwarden;\n- Swift Menu Bar app `LemanaVPN.app`;\n- безопасный DNS cleanup через root-owned wrapper;\n- алиасы `vpn`, `vpn-auto`, `vpn-manual`, `vpn-manual-full`, `vpn-debug`, `vpn-fix-dns`.\n\n## Быстрая установка\n\n```sh\ncurl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh\n```\n\nЕсли установка запущена из терминала, скрипт работает как интерактивный wizard: проверит, что уже стоит, спросит как хранить credentials, предложит нужные модули и проведёт через настройку.\n\nПосле установки открой новый shell или выполни:\n\n```sh\nexec zsh\nvpn\n```\n\n## Интерактивная установка\n\nОбычный путь — запустить installer без флагов и ответить на вопросы:\n\n```sh\ncurl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh\n```\n\nПервый важный вопрос — как хранить VPN credentials:\n\n```text\nКак хранить VPN credentials?\n 1) Bitwarden -> macOS Keychain\n 2) macOS Keychain: ввести LDAP password и TOTP seed сейчас\n 3) macOS Keychain: настрою вручную позже\nВыбор [1/2/3, Enter=1]:\n```\n\nЧто означают варианты:\n\n- `1` — использовать Bitwarden как sync-provider: installer поставит/проверит `bw`, а при запуске `vpn` CLI переложит LDAP password и TOTP seed из Bitwarden в macOS Keychain.\n- `2` — бесплатный Keychain-only путь: после установки CLI спросит LDAP password и постоянный TOTP seed или `otpauth://...secret=...`, затем сохранит их в macOS Keychain.\n- `3` — поставить CLI/app сейчас, а credentials настроить позже командой `vpn --configure-keychain`.\n\nДальше installer спросит только про системные модули: Touch ID для Bitwarden, sudoers, aliases, Swift Menu Bar app и автозапуск.\n\nФлаги остаются для CI, повторяемых установок и диагностики. Для обычной установки они не нужны.\n\nПроверить действия без изменений:\n\n```sh\ncurl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --dry-run\n```\n\nПример неинтерактивного режима для автоматизации:\n\n```sh\ncurl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \\\n | sh -s -- --non-interactive --credential-source keychain --configure-keychain\n```\n\nПринудительно включить интерактивные вопросы:\n\n```sh\n",
|
||||||
|
"languages": [
|
||||||
|
"markdown",
|
||||||
|
"swift",
|
||||||
|
"shell",
|
||||||
|
"unknown",
|
||||||
|
"toml"
|
||||||
|
],
|
||||||
|
"frameworks": [
|
||||||
|
"SwiftUI",
|
||||||
|
"AppKit",
|
||||||
|
"openconnect-lite",
|
||||||
|
"Bitwarden CLI",
|
||||||
|
"macOS Keychain"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 70,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 80,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 101,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "AGENTS.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 88,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Package.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 14,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 190,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 697,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/vpn-lemanapro.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 1090,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 12,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 251,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 935,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "install.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 943,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"language": "unknown",
|
||||||
|
"sizeLines": 23,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 579,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/openconnect-lite-config.toml",
|
||||||
|
"language": "toml",
|
||||||
|
"sizeLines": 32,
|
||||||
|
"fileCategory": "config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/smoke.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 438,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "uninstall.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 320,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"totalFiles": 17,
|
||||||
|
"filteredByIgnore": 3,
|
||||||
|
"estimatedComplexity": "small",
|
||||||
|
"stats": {
|
||||||
|
"filesScanned": 17,
|
||||||
|
"byCategory": {
|
||||||
|
"docs": 8,
|
||||||
|
"code": 4,
|
||||||
|
"script": 4,
|
||||||
|
"config": 1
|
||||||
|
},
|
||||||
|
"byLanguage": {
|
||||||
|
"markdown": 8,
|
||||||
|
"swift": 3,
|
||||||
|
"shell": 4,
|
||||||
|
"unknown": 1,
|
||||||
|
"toml": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"importMap": {
|
||||||
|
".agents/skills/lemana-vpn-operations/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-sso-autofill/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-testing/SKILL.md": [],
|
||||||
|
"AGENTS.md": [],
|
||||||
|
"app/Package.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": [],
|
||||||
|
"bin/vpn-lemanapro.sh": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/GOAL.md": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/PLAN.md": [],
|
||||||
|
"docs/superpowers/plans/2026-05-24-keychain-totp-provider.md": [],
|
||||||
|
"install.sh": [],
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": [],
|
||||||
|
"README.md": [],
|
||||||
|
"templates/openconnect-lite-config.toml": [],
|
||||||
|
"tests/smoke.sh": [],
|
||||||
|
"uninstall.sh": []
|
||||||
|
}
|
||||||
|
}
|
||||||
2037
.understand-anything/knowledge-graph.json
Normal file
2037
.understand-anything/knowledge-graph.json
Normal file
File diff suppressed because it is too large
Load Diff
6
.understand-anything/meta.json
Normal file
6
.understand-anything/meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"lastAnalyzedAt": "2026-06-23T06:34:14.317Z",
|
||||||
|
"gitCommitHash": "042c4d655090e0ee334208511ec63b6d909d4354",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"analyzedFiles": 17
|
||||||
|
}
|
||||||
206
.understand-anything/tmp/build-batches.cjs
Normal file
206
.understand-anything/tmp/build-batches.cjs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const root = process.cwd();
|
||||||
|
const inter = path.join(root, ".understand-anything", "intermediate");
|
||||||
|
const tmp = path.join(root, ".understand-anything", "tmp");
|
||||||
|
const scan = JSON.parse(fs.readFileSync(path.join(inter, "scan-result.json"), "utf8"));
|
||||||
|
const batches = JSON.parse(fs.readFileSync(path.join(inter, "batches.json"), "utf8")).batches;
|
||||||
|
|
||||||
|
const fileInfo = new Map(scan.files.map((f) => [f.path, f]));
|
||||||
|
const fileNodeId = (file) => {
|
||||||
|
if (file.fileCategory === "docs") return `document:${file.path}`;
|
||||||
|
if (file.fileCategory === "config") return `config:${file.path}`;
|
||||||
|
return `file:${file.path}`;
|
||||||
|
};
|
||||||
|
const edge = (source, target, type, weight) => ({ source, target, type, direction: "forward", weight });
|
||||||
|
const complexity = (lines) => (lines > 200 ? "complex" : lines > 50 ? "moderate" : "simple");
|
||||||
|
const nameOf = (p) => p.split("/").pop();
|
||||||
|
|
||||||
|
const fileMeta = {
|
||||||
|
".agents/skills/lemana-vpn-operations/SKILL.md": ["Lemana VPN Operations skill", "Инструкции для операционных задач: установка, обновление, статусы, безопасные ответы пользователю и live-connection правила.", ["skill", "operations", "documentation"]],
|
||||||
|
".agents/skills/lemana-vpn-sso-autofill/SKILL.md": ["Lemana VPN SSO skill", "Инструкции по Bitwarden, Keychain, TOTP, Keycloak SSO, autofill и runtime-патчам openconnect-lite.", ["skill", "sso", "credentials"]],
|
||||||
|
".agents/skills/lemana-vpn-testing/SKILL.md": ["Lemana VPN Testing skill", "Инструкции по non-live проверкам, status checks, live VPN safety и финальному отчету для изменений в репозитории.", ["skill", "testing", "verification"]],
|
||||||
|
"AGENTS.md": ["AGENTS.md", "Репозиторные правила для агентов: модель системы, live VPN safety, тестовая лестница и контракт финального ответа.", ["agent-rules", "documentation", "safety"]],
|
||||||
|
"README.md": ["README.md", "Основная пользовательская документация Lemana VPN: установка, credential sources, модули, использование, диагностика и удаление.", ["documentation", "overview", "user-guide"]],
|
||||||
|
"docs/goals/manual-first-auth-flow/GOAL.md": ["Manual-first auth goal", "Краткое описание цели manual-first authentication flow и ожидаемого пользовательского результата.", ["goal", "auth-flow", "planning"]],
|
||||||
|
"docs/goals/manual-first-auth-flow/PLAN.md": ["Manual-first auth plan", "Подробный план изменения installer UX, CLI auth profiles, Swift menu-bar surface и smoke coverage для manual-first сценария.", ["plan", "auth-flow", "implementation"]],
|
||||||
|
"docs/superpowers/plans/2026-05-24-keychain-totp-provider.md": ["Keychain TOTP provider plan", "Исторический implementation plan для разделения credential sources, нормализации TOTP и расширения smoke-проверок.", ["plan", "keychain", "totp"]],
|
||||||
|
"app/Package.swift": ["Package.swift", "Swift Package manifest для сборки executable target LemanaVPN под macOS 13+.", ["swift", "build-system", "menu-bar-app"]],
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": ["LemanaVPNApp.swift", "SwiftUI/AppKit menu bar surface: отображает состояние VPN, кнопки подключения, логи и действия пользователя.", ["swiftui", "menu-bar", "ui"]],
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": ["VPNManager.swift", "Основной state manager Swift-приложения: запускает CLI в JSON mode, декодирует события, отслеживает health и управляет reconnect/disconnect.", ["swift", "state-manager", "cli-bridge"]],
|
||||||
|
"bin/vpn-lemanapro.sh": ["vpn-lemanapro.sh", "Главный runtime CLI: status JSON, sync credentials, Keychain/TOTP, runtime patches, SSO запуск, reconnect loop, DNS cleanup и фильтрация openconnect-lite output.", ["cli", "runtime", "sso"]],
|
||||||
|
"install.sh": ["install.sh", "Installer wizard для Homebrew/pipx/openconnect-lite, CLI, config, DNS wrapper, sudoers, Touch ID helper, Swift app, launch agent и shell aliases.", ["installer", "wizard", "macos"]],
|
||||||
|
"uninstall.sh": ["uninstall.sh", "Uninstaller для восстановления runtime patch backups, удаления CLI/app/config/sudoers/aliases и опциональной очистки Keychain/Touch ID state.", ["uninstaller", "cleanup", "macos"]],
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": ["lemana-vpn-dns-cleanup", "Узкий root-owned wrapper, который сбрасывает только корпоративные DNS и обновляет macOS resolver cache.", ["dns", "sudo-wrapper", "cleanup"]],
|
||||||
|
"templates/openconnect-lite-config.toml": ["openconnect-lite-config.toml", "Шаблон профиля openconnect-lite: gateway, credentials mode и Keycloak auto-fill selectors для username, password и TOTP.", ["configuration", "openconnect-lite", "autofill"]],
|
||||||
|
"tests/smoke.sh": ["smoke.sh", "Non-live smoke suite: shell syntax, installer/uninstaller checks, Swift wiring, SSO patch snippets, config selectors и regression assertions.", ["test", "smoke", "verification"]],
|
||||||
|
};
|
||||||
|
|
||||||
|
const functionSummaries = {
|
||||||
|
choose_credential_source_interactive: "Показывает интерактивный выбор credential source и нормализует ответ пользователя.",
|
||||||
|
choose_modules: "Определяет набор устанавливаемых модулей с учетом интерактивного режима и флагов installer.",
|
||||||
|
install_openconnect_lite: "Устанавливает или проверяет openconnect-lite runtime, необходимый для SAML SSO.",
|
||||||
|
install_cli: "Копирует основной CLI и uninstall helper в пользовательский bin.",
|
||||||
|
install_config: "Устанавливает шаблон openconnect-lite config и сохраняет локальную конфигурацию модулей.",
|
||||||
|
install_dns_cleanup: "Устанавливает root-owned DNS cleanup wrapper для безопасного сброса корпоративных DNS.",
|
||||||
|
install_sudoers: "Пишет ограниченные sudoers правила для openconnect и DNS cleanup wrapper.",
|
||||||
|
install_touchid_helper: "Собирает и устанавливает Touch ID helper для Bitwarden master password.",
|
||||||
|
install_menu_bar_app: "Собирает release-версию Swift menu bar app и устанавливает LemanaVPN.app.",
|
||||||
|
install_launch_agent: "Создает LaunchAgent для автозапуска menu bar app при логине.",
|
||||||
|
restart_running_menu_bar_app: "Перезапускает уже открытое menu bar приложение после обновления bundle.",
|
||||||
|
install_shell_aliases: "Добавляет idempotent zsh aliases для CLI режимов подключения и диагностики.",
|
||||||
|
maybe_login_bitwarden: "Проверяет Bitwarden session и при необходимости запускает login/unlock flow.",
|
||||||
|
restore_openconnect_lite_patch: "Возвращает backup исходника openconnect-lite, если runtime patch был установлен.",
|
||||||
|
remove_zshrc_block: "Удаляет idempotent блок Lemana VPN aliases из zshrc.",
|
||||||
|
remove_keychain_entries: "Опционально удаляет password, TOTP seed и username из macOS Keychain.",
|
||||||
|
_module_status_json: "Собирает машинно-читаемый JSON со статусом модулей, credentials, app, DNS и patches.",
|
||||||
|
_module_status_human: "Печатает человекочитаемую строку статуса модулей перед подключением.",
|
||||||
|
_check_status: "Проверяет PID/status file, running process, DNS target и возвращает disconnected/connected JSON.",
|
||||||
|
_patch_oc: "Применяет runtime patches к openconnect-lite для Keycloak autofill/manual/debug сценариев.",
|
||||||
|
_normalize_totp_secret: "Нормализует TOTP secret из raw secret или otpauth URI перед сохранением.",
|
||||||
|
_configure_keychain: "Интерактивно сохраняет LDAP password и TOTP seed в macOS Keychain.",
|
||||||
|
_ensure_keychain_credentials: "Проверяет наличие обязательных Keychain credentials перед auto/manual подключением.",
|
||||||
|
_bw_unlock: "Открывает Bitwarden session с учетом Touch ID helper и fallback prompts.",
|
||||||
|
_sync_bitwarden: "Синхронизирует LDAP password и TOTP seed из Bitwarden в macOS Keychain.",
|
||||||
|
_sync_credentials: "Выбирает credential source и запускает соответствующий sync/preflight путь.",
|
||||||
|
_dns_cleanup: "Вызывает narrow sudo wrapper для очистки VPN DNS при завершении сессии.",
|
||||||
|
_filter_output: "Фильтрует поток openconnect-lite, извлекает IP/expiry, пишет status JSON и события для app.",
|
||||||
|
refreshStatus: "Запускает CLI status JSON и обновляет module summary в menu bar app.",
|
||||||
|
connect: "Запускает CLI в JSON mode с выбранным режимом подключения и подписывается на stdout/stderr.",
|
||||||
|
disconnect: "Отправляет SIGINT текущему CLI process и останавливает reconnect state.",
|
||||||
|
parseEvent: "Декодирует JSON events из CLI и переводит их в Swift state updates.",
|
||||||
|
handleTermination: "Обрабатывает завершение CLI process и решает, нужен ли автоматический reconnect.",
|
||||||
|
startHealthCheck: "Периодически проверяет tunnel health и запускает reconnect при повторных failures.",
|
||||||
|
};
|
||||||
|
|
||||||
|
const swiftTypeSummaries = {
|
||||||
|
LemanaVPNApp: "Точка входа SwiftUI menu bar app, связывает VPNManager с MenuBarExtra.",
|
||||||
|
StatusBarLabel: "Компактный status bar label с иконкой и оставшимся временем сессии.",
|
||||||
|
VPNMenuView: "Основной menu UI с командами auto/manual/manual-full connect, disconnect, logs и quit.",
|
||||||
|
VPNEvent: "Decodable-модель JSON events, которые runtime CLI отправляет в Swift app.",
|
||||||
|
VPNStatusResponse: "Decodable envelope для ответа `vpn --status --json`.",
|
||||||
|
ModuleStatus: "Модель статуса модулей и вычисление summary/systemImage для menu bar app.",
|
||||||
|
VPNState: "Finite state model для disconnected/connecting/connected/reconnecting/error состояний.",
|
||||||
|
VPNLaunchMode: "Маппинг app launch modes на CLI flags `--auto`, `--manual`, `--manual-full`.",
|
||||||
|
VPNManager: "ObservableObject, который мостит Swift menu bar app с shell CLI, JSON events и health checks.",
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeFileNode(file) {
|
||||||
|
const meta = fileMeta[file.path] || [nameOf(file.path), `Файл проекта ${file.path}.`, ["project-file", file.language, file.fileCategory]];
|
||||||
|
const type = file.fileCategory === "docs" ? "document" : file.fileCategory === "config" ? "config" : "file";
|
||||||
|
return {
|
||||||
|
id: fileNodeId(file),
|
||||||
|
type,
|
||||||
|
name: meta[0],
|
||||||
|
filePath: file.path,
|
||||||
|
summary: meta[1],
|
||||||
|
tags: meta[2],
|
||||||
|
complexity: complexity(file.sizeLines),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIncludeFunction(filePath, fn) {
|
||||||
|
if (fn.name === "autoFill" || fn.name === "vpn") return false;
|
||||||
|
const span = fn.endLine - fn.startLine + 1;
|
||||||
|
if (functionSummaries[fn.name]) return true;
|
||||||
|
if (fn.name === "usage" || fn.name === "main") return true;
|
||||||
|
if (/^(install_|remove_|restore_|choose_|_sync|_ensure|_configure|_patch|_check|_filter|_module|_dns)/.test(fn.name)) return true;
|
||||||
|
return span >= 18 && !/^log|color_line|die|run|need_cmd|has_tty|bool_word|_emit|_json_get|_can_prompt|_prompt_read/.test(fn.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFunctionNode(filePath, fn) {
|
||||||
|
const id = `function:${filePath}:${fn.name}`;
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: "function",
|
||||||
|
name: fn.name,
|
||||||
|
filePath,
|
||||||
|
lineRange: [fn.startLine, fn.endLine],
|
||||||
|
summary: functionSummaries[fn.name] || `Значимая функция ${fn.name} в ${filePath}.`,
|
||||||
|
tags: ["function", filePath.includes("test") ? "verification" : "runtime", fn.name.replace(/^_+/, "").replaceAll("_", "-")],
|
||||||
|
complexity: complexity(fn.endLine - fn.startLine + 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function swiftDecls(filePath) {
|
||||||
|
if (!filePath.endsWith(".swift")) return [];
|
||||||
|
const text = fs.readFileSync(path.join(root, filePath), "utf8");
|
||||||
|
const lines = text.split(/\n/);
|
||||||
|
const decls = [];
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const m = line.match(/^\s*(?:@main\s+)?(?:struct|class|enum)\s+([A-Za-z_][A-Za-z0-9_]*)/);
|
||||||
|
if (m && swiftTypeSummaries[m[1]]) {
|
||||||
|
decls.push({ name: m[1], startLine: index + 1 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (let i = 0; i < decls.length; i += 1) {
|
||||||
|
decls[i].endLine = (decls[i + 1]?.startLine || lines.length + 1) - 1;
|
||||||
|
}
|
||||||
|
return decls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function swiftMethods(filePath) {
|
||||||
|
if (!filePath.endsWith("VPNManager.swift")) return [];
|
||||||
|
const text = fs.readFileSync(path.join(root, filePath), "utf8");
|
||||||
|
const lines = text.split(/\n/);
|
||||||
|
const wanted = new Set(["refreshStatus", "connect", "disconnect", "parseEvent", "handleTermination", "startHealthCheck"]);
|
||||||
|
const methods = [];
|
||||||
|
lines.forEach((line, index) => {
|
||||||
|
const m = line.match(/^\s*(?:private\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
||||||
|
if (m && wanted.has(m[1])) methods.push({ name: m[1], startLine: index + 1 });
|
||||||
|
});
|
||||||
|
for (let i = 0; i < methods.length; i += 1) {
|
||||||
|
methods[i].endLine = (methods[i + 1]?.startLine || Math.min(lines.length + 1, methods[i].startLine + 90)) - 1;
|
||||||
|
}
|
||||||
|
return methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphForBatch(batch) {
|
||||||
|
const extract = JSON.parse(fs.readFileSync(path.join(tmp, `ua-file-extract-results-${batch.batchIndex}.json`), "utf8"));
|
||||||
|
const nodes = [];
|
||||||
|
const edges = [];
|
||||||
|
const resultByPath = new Map(extract.results.map((r) => [r.path, r]));
|
||||||
|
|
||||||
|
for (const file of batch.files) {
|
||||||
|
const fileNode = makeFileNode(file);
|
||||||
|
nodes.push(fileNode);
|
||||||
|
const result = resultByPath.get(file.path);
|
||||||
|
|
||||||
|
for (const fn of result?.functions || []) {
|
||||||
|
if (!shouldIncludeFunction(file.path, fn)) continue;
|
||||||
|
const node = makeFunctionNode(file.path, fn);
|
||||||
|
nodes.push(node);
|
||||||
|
edges.push(edge(fileNode.id, node.id, "contains", 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const decl of swiftDecls(file.path)) {
|
||||||
|
const node = {
|
||||||
|
id: `class:${file.path}:${decl.name}`,
|
||||||
|
type: "class",
|
||||||
|
name: decl.name,
|
||||||
|
filePath: file.path,
|
||||||
|
lineRange: [decl.startLine, decl.endLine],
|
||||||
|
summary: swiftTypeSummaries[decl.name],
|
||||||
|
tags: ["swift", "type", decl.name === "VPNManager" ? "state-manager" : "menu-bar-app"],
|
||||||
|
complexity: complexity(decl.endLine - decl.startLine + 1),
|
||||||
|
};
|
||||||
|
nodes.push(node);
|
||||||
|
edges.push(edge(fileNode.id, node.id, "contains", 1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fn of swiftMethods(file.path)) {
|
||||||
|
const node = makeFunctionNode(file.path, fn);
|
||||||
|
nodes.push(node);
|
||||||
|
edges.push(edge(fileNode.id, node.id, "contains", 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const batch of batches) {
|
||||||
|
const graph = graphForBatch(batch);
|
||||||
|
fs.writeFileSync(path.join(inter, `batch-${batch.batchIndex}.json`), JSON.stringify(graph, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
35
.understand-anything/tmp/ua-file-analyzer-input-1.json
Normal file
35
.understand-anything/tmp/ua-file-analyzer-input-1.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"projectRoot": "/Users/dokril/git/home/lemana-vpn",
|
||||||
|
"batchFiles": [
|
||||||
|
{
|
||||||
|
"path": "AGENTS.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 88,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 579,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "install.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 943,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "uninstall.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 320,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batchImportData": {
|
||||||
|
"AGENTS.md": [],
|
||||||
|
"README.md": [],
|
||||||
|
"install.sh": [],
|
||||||
|
"uninstall.sh": []
|
||||||
|
}
|
||||||
|
}
|
||||||
98
.understand-anything/tmp/ua-file-analyzer-input-2.json
Normal file
98
.understand-anything/tmp/ua-file-analyzer-input-2.json
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"projectRoot": "/Users/dokril/git/home/lemana-vpn",
|
||||||
|
"batchFiles": [
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 70,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 80,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 101,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Package.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 14,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 190,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"sizeLines": 697,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/vpn-lemanapro.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 1090,
|
||||||
|
"fileCategory": "script"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 12,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 251,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"sizeLines": 935,
|
||||||
|
"fileCategory": "docs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"language": "unknown",
|
||||||
|
"sizeLines": 23,
|
||||||
|
"fileCategory": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/openconnect-lite-config.toml",
|
||||||
|
"language": "toml",
|
||||||
|
"sizeLines": 32,
|
||||||
|
"fileCategory": "config"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/smoke.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"sizeLines": 438,
|
||||||
|
"fileCategory": "script"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"batchImportData": {
|
||||||
|
".agents/skills/lemana-vpn-operations/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-sso-autofill/SKILL.md": [],
|
||||||
|
".agents/skills/lemana-vpn-testing/SKILL.md": [],
|
||||||
|
"app/Package.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/LemanaVPNApp.swift": [],
|
||||||
|
"app/Sources/LemanaVPN/VPNManager.swift": [],
|
||||||
|
"bin/vpn-lemanapro.sh": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/GOAL.md": [],
|
||||||
|
"docs/goals/manual-first-auth-flow/PLAN.md": [],
|
||||||
|
"docs/superpowers/plans/2026-05-24-keychain-totp-provider.md": [],
|
||||||
|
"libexec/lemana-vpn-dns-cleanup": [],
|
||||||
|
"templates/openconnect-lite-config.toml": [],
|
||||||
|
"tests/smoke.sh": []
|
||||||
|
}
|
||||||
|
}
|
||||||
540
.understand-anything/tmp/ua-file-extract-results-1.json
Normal file
540
.understand-anything/tmp/ua-file-extract-results-1.json
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
{
|
||||||
|
"scriptCompleted": true,
|
||||||
|
"filesAnalyzed": 4,
|
||||||
|
"filesSkipped": [],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"path": "AGENTS.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 88,
|
||||||
|
"nonEmptyLines": 61,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Lemana VPN Agent Instructions",
|
||||||
|
"level": 1,
|
||||||
|
"line": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Scope",
|
||||||
|
"level": 2,
|
||||||
|
"line": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "System Model",
|
||||||
|
"level": 2,
|
||||||
|
"line": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Live VPN Safety",
|
||||||
|
"level": 2,
|
||||||
|
"line": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Testing",
|
||||||
|
"level": 2,
|
||||||
|
"line": 44
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Answer Contract",
|
||||||
|
"level": 2,
|
||||||
|
"line": 60
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 579,
|
||||||
|
"nonEmptyLines": 405,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Lemana VPN",
|
||||||
|
"level": 1,
|
||||||
|
"line": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Быстрая установка",
|
||||||
|
"level": 2,
|
||||||
|
"line": 19
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Интерактивная установка",
|
||||||
|
"level": 2,
|
||||||
|
"line": 34
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Что ставится",
|
||||||
|
"level": 2,
|
||||||
|
"line": 94
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Статус модулей",
|
||||||
|
"level": 2,
|
||||||
|
"line": 111
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Интерактивная установка",
|
||||||
|
"level": 2,
|
||||||
|
"line": 148
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Логи установщика и удаления",
|
||||||
|
"level": 2,
|
||||||
|
"line": 183
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Модули",
|
||||||
|
"level": 2,
|
||||||
|
"line": 201
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Core",
|
||||||
|
"level": 3,
|
||||||
|
"line": 203
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Как работают credential sources",
|
||||||
|
"level": 3,
|
||||||
|
"line": 214
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Bitwarden",
|
||||||
|
"level": 3,
|
||||||
|
"line": 235
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Если Bitwarden нет",
|
||||||
|
"level": 3,
|
||||||
|
"line": 259
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Touch ID",
|
||||||
|
"level": 3,
|
||||||
|
"line": 302
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Swift Menu Bar app",
|
||||||
|
"level": 3,
|
||||||
|
"line": 314
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Использование",
|
||||||
|
"level": 2,
|
||||||
|
"line": 352
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Настройка",
|
||||||
|
"level": 2,
|
||||||
|
"line": 385
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Bitwarden item",
|
||||||
|
"level": 2,
|
||||||
|
"line": 415
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Почему DNS wrapper, а не wildcard sudoers",
|
||||||
|
"level": 2,
|
||||||
|
"line": 426
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Runtime-патчи openconnect-lite",
|
||||||
|
"level": 2,
|
||||||
|
"line": 432
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Диагностика",
|
||||||
|
"level": 2,
|
||||||
|
"line": 460
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Удаление",
|
||||||
|
"level": 2,
|
||||||
|
"line": 528
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 21
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "install.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"fileCategory": "script",
|
||||||
|
"totalLines": 943,
|
||||||
|
"nonEmptyLines": 824,
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "usage",
|
||||||
|
"startLine": 72,
|
||||||
|
"endLine": 106,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log",
|
||||||
|
"startLine": 220,
|
||||||
|
"endLine": 222,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color_line",
|
||||||
|
"startLine": 224,
|
||||||
|
"endLine": 228,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_step",
|
||||||
|
"startLine": 230,
|
||||||
|
"endLine": 232,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_info",
|
||||||
|
"startLine": 234,
|
||||||
|
"endLine": 236,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_detail",
|
||||||
|
"startLine": 238,
|
||||||
|
"endLine": 240,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_ok",
|
||||||
|
"startLine": 242,
|
||||||
|
"endLine": 244,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_warn",
|
||||||
|
"startLine": 246,
|
||||||
|
"endLine": 248,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_skip",
|
||||||
|
"startLine": 250,
|
||||||
|
"endLine": 252,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "die",
|
||||||
|
"startLine": 254,
|
||||||
|
"endLine": 257,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "run",
|
||||||
|
"startLine": 259,
|
||||||
|
"endLine": 269,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "need_cmd",
|
||||||
|
"startLine": 271,
|
||||||
|
"endLine": 273,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "has_tty",
|
||||||
|
"startLine": 275,
|
||||||
|
"endLine": 277,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "interactive_enabled",
|
||||||
|
"startLine": 279,
|
||||||
|
"endLine": 286,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prompt_printf",
|
||||||
|
"startLine": 288,
|
||||||
|
"endLine": 294,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "prompt_read_answer",
|
||||||
|
"startLine": 296,
|
||||||
|
"endLine": 302,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "yes_no",
|
||||||
|
"startLine": 304,
|
||||||
|
"endLine": 330,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "choose_credential_source_interactive",
|
||||||
|
"startLine": 332,
|
||||||
|
"endLine": 382,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "bool_word",
|
||||||
|
"startLine": 384,
|
||||||
|
"endLine": 390,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keychain_has",
|
||||||
|
"startLine": 392,
|
||||||
|
"endLine": 394,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "zsh_aliases_installed",
|
||||||
|
"startLine": 396,
|
||||||
|
"endLine": 398,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "print_detected_state",
|
||||||
|
"startLine": 400,
|
||||||
|
"endLine": 417,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "choose_modules",
|
||||||
|
"startLine": 419,
|
||||||
|
"endLine": 514,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "script_dir",
|
||||||
|
"startLine": 516,
|
||||||
|
"endLine": 521,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "download_file",
|
||||||
|
"startLine": 523,
|
||||||
|
"endLine": 540,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "write_file",
|
||||||
|
"startLine": 542,
|
||||||
|
"endLine": 550,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_homebrew_packages",
|
||||||
|
"startLine": 552,
|
||||||
|
"endLine": 578,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_openconnect_lite",
|
||||||
|
"startLine": 580,
|
||||||
|
"endLine": 597,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_cli",
|
||||||
|
"startLine": 599,
|
||||||
|
"endLine": 613,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_config",
|
||||||
|
"startLine": 615,
|
||||||
|
"endLine": 639,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_dns_cleanup",
|
||||||
|
"startLine": 641,
|
||||||
|
"endLine": 654,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_sudoers",
|
||||||
|
"startLine": 656,
|
||||||
|
"endLine": 683,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_touchid_helper",
|
||||||
|
"startLine": 685,
|
||||||
|
"endLine": 708,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_menu_bar_app",
|
||||||
|
"startLine": 710,
|
||||||
|
"endLine": 769,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_launch_agent",
|
||||||
|
"startLine": 771,
|
||||||
|
"endLine": 811,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restart_running_menu_bar_app",
|
||||||
|
"startLine": 813,
|
||||||
|
"endLine": 831,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "install_shell_aliases",
|
||||||
|
"startLine": 833,
|
||||||
|
"endLine": 877,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vpn",
|
||||||
|
"startLine": 854,
|
||||||
|
"endLine": 854,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "maybe_login_bitwarden",
|
||||||
|
"startLine": 879,
|
||||||
|
"endLine": 900,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"startLine": 902,
|
||||||
|
"endLine": 941,
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 40,
|
||||||
|
"classCount": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "uninstall.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"fileCategory": "script",
|
||||||
|
"totalLines": 320,
|
||||||
|
"nonEmptyLines": 279,
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "usage",
|
||||||
|
"startLine": 62,
|
||||||
|
"endLine": 79,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log",
|
||||||
|
"startLine": 102,
|
||||||
|
"endLine": 104,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "color_line",
|
||||||
|
"startLine": 106,
|
||||||
|
"endLine": 110,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_step",
|
||||||
|
"startLine": 112,
|
||||||
|
"endLine": 114,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_info",
|
||||||
|
"startLine": 116,
|
||||||
|
"endLine": 118,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_detail",
|
||||||
|
"startLine": 120,
|
||||||
|
"endLine": 122,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_ok",
|
||||||
|
"startLine": 124,
|
||||||
|
"endLine": 126,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_warn",
|
||||||
|
"startLine": 128,
|
||||||
|
"endLine": 130,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "log_skip",
|
||||||
|
"startLine": 132,
|
||||||
|
"endLine": 134,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "die",
|
||||||
|
"startLine": 136,
|
||||||
|
"endLine": 139,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "run",
|
||||||
|
"startLine": 141,
|
||||||
|
"endLine": 151,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "find_webengine_process",
|
||||||
|
"startLine": 153,
|
||||||
|
"endLine": 159,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "restore_openconnect_lite_patch",
|
||||||
|
"startLine": 161,
|
||||||
|
"endLine": 181,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_zshrc_block",
|
||||||
|
"startLine": 183,
|
||||||
|
"endLine": 204,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "remove_keychain_entries",
|
||||||
|
"startLine": 206,
|
||||||
|
"endLine": 234,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "main",
|
||||||
|
"startLine": 236,
|
||||||
|
"endLine": 318,
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 16,
|
||||||
|
"classCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
727
.understand-anything/tmp/ua-file-extract-results-2.json
Normal file
727
.understand-anything/tmp/ua-file-extract-results-2.json
Normal file
@@ -0,0 +1,727 @@
|
|||||||
|
{
|
||||||
|
"scriptCompleted": true,
|
||||||
|
"filesAnalyzed": 13,
|
||||||
|
"filesSkipped": [],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-operations/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 70,
|
||||||
|
"nonEmptyLines": 48,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Lemana VPN Operations",
|
||||||
|
"level": 1,
|
||||||
|
"line": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Overview",
|
||||||
|
"level": 2,
|
||||||
|
"line": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "System Map",
|
||||||
|
"level": 2,
|
||||||
|
"line": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Live Connection Rules",
|
||||||
|
"level": 2,
|
||||||
|
"line": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Update Commands",
|
||||||
|
"level": 2,
|
||||||
|
"line": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Answer Format",
|
||||||
|
"level": 2,
|
||||||
|
"line": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Common Mistakes",
|
||||||
|
"level": 2,
|
||||||
|
"line": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-sso-autofill/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 80,
|
||||||
|
"nonEmptyLines": 54,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Lemana VPN SSO And Autofill",
|
||||||
|
"level": 1,
|
||||||
|
"line": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Overview",
|
||||||
|
"level": 2,
|
||||||
|
"line": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Credential Flow",
|
||||||
|
"level": 2,
|
||||||
|
"line": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Runtime Patches",
|
||||||
|
"level": 2,
|
||||||
|
"line": 29
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Modes",
|
||||||
|
"level": 2,
|
||||||
|
"line": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Testing",
|
||||||
|
"level": 2,
|
||||||
|
"line": 56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Troubleshooting Signals",
|
||||||
|
"level": 2,
|
||||||
|
"line": 76
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".agents/skills/lemana-vpn-testing/SKILL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 101,
|
||||||
|
"nonEmptyLines": 68,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Lemana VPN Testing",
|
||||||
|
"level": 1,
|
||||||
|
"line": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Overview",
|
||||||
|
"level": 2,
|
||||||
|
"line": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Non-Live Verification",
|
||||||
|
"level": 2,
|
||||||
|
"line": 12
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Status Checks",
|
||||||
|
"level": 2,
|
||||||
|
"line": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Live VPN Checks",
|
||||||
|
"level": 2,
|
||||||
|
"line": 46
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Installer/Uninstaller Checks",
|
||||||
|
"level": 2,
|
||||||
|
"line": 83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Final Report",
|
||||||
|
"level": 2,
|
||||||
|
"line": 94
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Package.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"fileCategory": "code",
|
||||||
|
"totalLines": 14,
|
||||||
|
"nonEmptyLines": 12,
|
||||||
|
"metrics": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/LemanaVPNApp.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"fileCategory": "code",
|
||||||
|
"totalLines": 190,
|
||||||
|
"nonEmptyLines": 174,
|
||||||
|
"metrics": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "app/Sources/LemanaVPN/VPNManager.swift",
|
||||||
|
"language": "swift",
|
||||||
|
"fileCategory": "code",
|
||||||
|
"totalLines": 697,
|
||||||
|
"nonEmptyLines": 618,
|
||||||
|
"metrics": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "bin/vpn-lemanapro.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"fileCategory": "script",
|
||||||
|
"totalLines": 1090,
|
||||||
|
"nonEmptyLines": 954,
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "_emit",
|
||||||
|
"startLine": 119,
|
||||||
|
"endLine": 126,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_write_status",
|
||||||
|
"startLine": 128,
|
||||||
|
"endLine": 131,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_clear_status",
|
||||||
|
"startLine": 133,
|
||||||
|
"endLine": 135,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_prepare_connection_log",
|
||||||
|
"startLine": 137,
|
||||||
|
"endLine": 144,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_log_connection_line",
|
||||||
|
"startLine": 146,
|
||||||
|
"endLine": 148,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_show_connection_log_tail",
|
||||||
|
"startLine": 150,
|
||||||
|
"endLine": 154,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_start_connect_progress",
|
||||||
|
"startLine": 158,
|
||||||
|
"endLine": 166,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_stop_connect_progress",
|
||||||
|
"startLine": 168,
|
||||||
|
"endLine": 174,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_json_get",
|
||||||
|
"startLine": 176,
|
||||||
|
"endLine": 179,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_find_webengine_process",
|
||||||
|
"startLine": 181,
|
||||||
|
"endLine": 187,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_find_authenticator",
|
||||||
|
"startLine": 189,
|
||||||
|
"endLine": 195,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_bool",
|
||||||
|
"startLine": 197,
|
||||||
|
"endLine": 203,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_patches_active",
|
||||||
|
"startLine": 205,
|
||||||
|
"endLine": 219,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_keychain_has",
|
||||||
|
"startLine": 221,
|
||||||
|
"endLine": 223,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_status_json",
|
||||||
|
"startLine": 225,
|
||||||
|
"endLine": 264,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_human_part",
|
||||||
|
"startLine": 266,
|
||||||
|
"endLine": 275,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_module_status_human",
|
||||||
|
"startLine": 277,
|
||||||
|
"endLine": 311,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_check_status",
|
||||||
|
"startLine": 313,
|
||||||
|
"endLine": 375,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_patch_oc",
|
||||||
|
"startLine": 377,
|
||||||
|
"endLine": 604,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "autoFill",
|
||||||
|
"startLine": 470,
|
||||||
|
"endLine": 473,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "autoFill",
|
||||||
|
"startLine": 488,
|
||||||
|
"endLine": 497,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_store_keychain",
|
||||||
|
"startLine": 606,
|
||||||
|
"endLine": 621,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_normalize_totp_secret",
|
||||||
|
"startLine": 623,
|
||||||
|
"endLine": 649,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_can_prompt",
|
||||||
|
"startLine": 651,
|
||||||
|
"endLine": 653,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_prompt_read",
|
||||||
|
"startLine": 655,
|
||||||
|
"endLine": 673,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_persist_username",
|
||||||
|
"startLine": 675,
|
||||||
|
"endLine": 718,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_ensure_username",
|
||||||
|
"startLine": 720,
|
||||||
|
"endLine": 745,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_configure_keychain",
|
||||||
|
"startLine": 747,
|
||||||
|
"endLine": 785,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_ensure_keychain_credentials",
|
||||||
|
"startLine": 787,
|
||||||
|
"endLine": 814,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_bw_cache_session",
|
||||||
|
"startLine": 816,
|
||||||
|
"endLine": 821,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_bw_unlock",
|
||||||
|
"startLine": 823,
|
||||||
|
"endLine": 889,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_sync_bitwarden",
|
||||||
|
"startLine": 891,
|
||||||
|
"endLine": 925,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_sync_credentials",
|
||||||
|
"startLine": 927,
|
||||||
|
"endLine": 936,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_dns_cleanup",
|
||||||
|
"startLine": 938,
|
||||||
|
"endLine": 954,
|
||||||
|
"params": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_filter_output",
|
||||||
|
"startLine": 956,
|
||||||
|
"endLine": 1006,
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 35,
|
||||||
|
"classCount": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/GOAL.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 13,
|
||||||
|
"nonEmptyLines": 11,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Goal: Manual-First Auth Flow",
|
||||||
|
"level": 1,
|
||||||
|
"line": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/goals/manual-first-auth-flow/PLAN.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 440,
|
||||||
|
"nonEmptyLines": 318,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Manual-First Auth Flow Implementation Plan",
|
||||||
|
"level": 1,
|
||||||
|
"line": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Короткий План",
|
||||||
|
"level": 2,
|
||||||
|
"line": 18
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Architecture Slice",
|
||||||
|
"level": 2,
|
||||||
|
"line": 40
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Пользовательский Процесс Установки",
|
||||||
|
"level": 2,
|
||||||
|
"line": 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Первый Экран",
|
||||||
|
"level": 3,
|
||||||
|
"line": 124
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Шаг 1: Что Будет Установлено",
|
||||||
|
"level": 3,
|
||||||
|
"line": 148
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Шаг 2: Базовый Режим Входа",
|
||||||
|
"level": 3,
|
||||||
|
"line": 162
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Шаг 3: Автоматический Вход Только Для Продвинутого Пути",
|
||||||
|
"level": 3,
|
||||||
|
"line": 187
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Шаг 4: Bitwarden Как Дополнительная Функция",
|
||||||
|
"level": 3,
|
||||||
|
"line": 214
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Шаг 5: Touch ID Только После Bitwarden",
|
||||||
|
"level": 3,
|
||||||
|
"line": 234
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Шаг 6: Понятный Прогресс Установки",
|
||||||
|
"level": 3,
|
||||||
|
"line": 246
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Финальный Экран",
|
||||||
|
"level": 3,
|
||||||
|
"line": 265
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Ошибки И Подсказки",
|
||||||
|
"level": 3,
|
||||||
|
"line": 292
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Acceptance Evidence Для Installer UX",
|
||||||
|
"level": 3,
|
||||||
|
"line": 301
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Дополнительные Функции",
|
||||||
|
"level": 2,
|
||||||
|
"line": 308
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Tasks",
|
||||||
|
"level": 2,
|
||||||
|
"line": 345
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 1: Rewrite user-facing contract and docs",
|
||||||
|
"level": 3,
|
||||||
|
"line": 347
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 2: Change installer defaults and capability selection",
|
||||||
|
"level": 3,
|
||||||
|
"line": 356
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 3: Refactor CLI auth profiles and partial Keychain support",
|
||||||
|
"level": 3,
|
||||||
|
"line": 365
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 4: Preserve automatic compatibility without keeping it dominant",
|
||||||
|
"level": 3,
|
||||||
|
"line": 374
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 5: Update Swift menu-bar app surface",
|
||||||
|
"level": 3,
|
||||||
|
"line": 383
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 6: Expand smoke coverage",
|
||||||
|
"level": 3,
|
||||||
|
"line": 392
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 7: Final verification and local update notes",
|
||||||
|
"level": 3,
|
||||||
|
"line": 401
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Non-Goals",
|
||||||
|
"level": 2,
|
||||||
|
"line": 417
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Risks",
|
||||||
|
"level": 2,
|
||||||
|
"line": 425
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Execution Order",
|
||||||
|
"level": 2,
|
||||||
|
"line": 432
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 26
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/superpowers/plans/2026-05-24-keychain-totp-provider.md",
|
||||||
|
"language": "markdown",
|
||||||
|
"fileCategory": "docs",
|
||||||
|
"totalLines": 935,
|
||||||
|
"nonEmptyLines": 656,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "Keychain TOTP Provider Implementation Plan",
|
||||||
|
"level": 1,
|
||||||
|
"line": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "File Structure",
|
||||||
|
"level": 2,
|
||||||
|
"line": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 1: Add Smoke Tests For Credential Source Separation",
|
||||||
|
"level": 3,
|
||||||
|
"line": 39
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 2: Add Runtime Credential Source And TOTP Normalization",
|
||||||
|
"level": 3,
|
||||||
|
"line": 135
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 3: Split Bitwarden Sync From Keychain Source Flow",
|
||||||
|
"level": 3,
|
||||||
|
"line": 325
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 4: Update Installer Flags And Config",
|
||||||
|
"level": 3,
|
||||||
|
"line": 457
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 5: Update Swift Menu-Bar Status Decoding",
|
||||||
|
"level": 3,
|
||||||
|
"line": 639
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 6: Update Documentation",
|
||||||
|
"level": 3,
|
||||||
|
"line": 736
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Task 7: Full Verification",
|
||||||
|
"level": 3,
|
||||||
|
"line": 799
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "Self-Review",
|
||||||
|
"level": 2,
|
||||||
|
"line": 931
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "libexec/lemana-vpn-dns-cleanup",
|
||||||
|
"language": "unknown",
|
||||||
|
"fileCategory": "code",
|
||||||
|
"totalLines": 23,
|
||||||
|
"nonEmptyLines": 17,
|
||||||
|
"metrics": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "templates/openconnect-lite-config.toml",
|
||||||
|
"language": "toml",
|
||||||
|
"fileCategory": "config",
|
||||||
|
"totalLines": 32,
|
||||||
|
"nonEmptyLines": 23,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"heading": "default_profile",
|
||||||
|
"level": 1,
|
||||||
|
"line": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "credentials",
|
||||||
|
"level": 1,
|
||||||
|
"line": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "auto_fill_rules",
|
||||||
|
"level": 1,
|
||||||
|
"line": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "[[auto_fill_rules.\"https://employee.auth.lemanapro.ru/*\"]]",
|
||||||
|
"level": 5,
|
||||||
|
"line": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "[[auto_fill_rules.\"https://employee.auth.lemanapro.ru/*\"]]",
|
||||||
|
"level": 5,
|
||||||
|
"line": 17
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "[[auto_fill_rules.\"https://employee.auth.lemanapro.ru/*\"]]",
|
||||||
|
"level": 5,
|
||||||
|
"line": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "[[auto_fill_rules.\"https://employee.auth.lemanapro.ru/*\"]]",
|
||||||
|
"level": 5,
|
||||||
|
"line": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"heading": "[[auto_fill_rules.\"https://employee.auth.lemanapro.ru/*\"]]",
|
||||||
|
"level": 5,
|
||||||
|
"line": 29
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 0,
|
||||||
|
"classCount": 0,
|
||||||
|
"sectionCount": 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "tests/smoke.sh",
|
||||||
|
"language": "shell",
|
||||||
|
"fileCategory": "script",
|
||||||
|
"totalLines": 438,
|
||||||
|
"nonEmptyLines": 392,
|
||||||
|
"functions": [
|
||||||
|
{
|
||||||
|
"name": "autoFill",
|
||||||
|
"startLine": 113,
|
||||||
|
"endLine": 116,
|
||||||
|
"params": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metrics": {
|
||||||
|
"importCount": 0,
|
||||||
|
"exportCount": 0,
|
||||||
|
"functionCount": 1,
|
||||||
|
"classCount": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
178
README.md
178
README.md
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
|
CLI-установка корпоративного VPN `vpn.lemanapro.ru` для macOS.
|
||||||
|
|
||||||
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; Swift Menu Bar app: включён; автозапуск приложения: включён; runtime-патчи: применяются автоматически перед подключением.
|
**По умолчанию:** ручной вход без сохранения паролей. Откроется окно корпоративного входа, а логин, пароль и одноразовый код вводятся вручную. Bitwarden, Touch ID, сохранение пароля, подстановка 2FA и автоматический вход включаются только явно.
|
||||||
|
|
||||||
**Credential sources:** `bitwarden` синхронизирует LDAP-пароль и TOTP seed из Bitwarden в macOS Keychain; `keychain` хранит LDAP-пароль и постоянный TOTP seed напрямую в macOS Keychain. Оба источника используют один и тот же runtime `openconnect-lite` для SSO/autofill.
|
**Режимы входа:** `manual` ничего не сохраняет и ничего не подставляет; `saved-login` хранит LDAP логин/пароль в macOS Keychain и оставляет одноразовый код ручным; `saved-totp` дополнительно хранит постоянный код настройки 2FA; `auto` запускается только явно через `vpn-auto`/`--auto`.
|
||||||
|
|
||||||
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
||||||
|
|
||||||
- `openconnect` как VPN-клиент;
|
- `openconnect` как VPN-клиент;
|
||||||
- `openconnect-lite` для SAML SSO через Keycloak;
|
- `openconnect-lite` для SAML SSO через Keycloak;
|
||||||
- опциональный Bitwarden CLI для LDAP-пароля и TOTP seed;
|
- опциональный Bitwarden CLI для LDAP-пароля и постоянного кода настройки 2FA;
|
||||||
- опциональный Touch ID helper для мастер-пароля Bitwarden;
|
- опциональный Touch ID helper для мастер-пароля Bitwarden;
|
||||||
- Swift Menu Bar app `LemanaVPN.app`;
|
- Swift Menu Bar app `LemanaVPN.app`;
|
||||||
- безопасный DNS cleanup через root-owned wrapper;
|
- безопасный DNS cleanup через root-owned wrapper;
|
||||||
@@ -22,7 +22,7 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл
|
|||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Если установка запущена из терминала, скрипт работает как интерактивный wizard: проверит, что уже стоит, спросит как хранить credentials, предложит нужные модули и проведёт через настройку.
|
Если установка запущена из терминала, скрипт работает как простой wizard: объяснит, что будет установлено, по Enter оставит безопасный ручной режим и предложит дополнительные удобства только отдельным выбором.
|
||||||
|
|
||||||
После установки открой новый shell или выполни:
|
После установки открой новый shell или выполни:
|
||||||
|
|
||||||
@@ -39,23 +39,30 @@ vpn
|
|||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Первый важный вопрос — как хранить VPN credentials:
|
Первый важный вопрос — как подключаться:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Как хранить VPN credentials?
|
Как подключаться по умолчанию?
|
||||||
1) Bitwarden -> macOS Keychain
|
|
||||||
2) macOS Keychain: ввести LDAP password и TOTP seed сейчас
|
1) Вводить всё вручную (рекомендуется)
|
||||||
3) macOS Keychain: настрою вручную позже
|
Откроется окно входа. Вы сами введёте логин, пароль и одноразовый код.
|
||||||
|
|
||||||
|
2) Запомнить логин и пароль на этом Mac
|
||||||
|
При входе логин и пароль подставятся сами. Одноразовый код вы введёте вручную.
|
||||||
|
|
||||||
|
3) Настроить автоматический вход
|
||||||
|
Нужно заранее сохранить пароль и постоянный код настройки 2FA.
|
||||||
|
|
||||||
Выбор [1/2/3, Enter=1]:
|
Выбор [1/2/3, Enter=1]:
|
||||||
```
|
```
|
||||||
|
|
||||||
Что означают варианты:
|
Что означают варианты:
|
||||||
|
|
||||||
- `1` — использовать Bitwarden как sync-provider: installer поставит/проверит `bw`, а при запуске `vpn` CLI переложит LDAP password и TOTP seed из Bitwarden в macOS Keychain.
|
- `1` — ничего не сохранять. Это самый понятный старт: при подключении открывается окно входа, всё вводится руками.
|
||||||
- `2` — бесплатный Keychain-only путь: после установки CLI спросит LDAP password и постоянный TOTP seed или `otpauth://...secret=...`, затем сохранит их в macOS Keychain.
|
- `2` — сохранить LDAP логин и пароль в macOS Keychain. Одноразовый код всё равно вводится руками.
|
||||||
- `3` — поставить CLI/app сейчас, а credentials настроить позже командой `vpn --configure-keychain`.
|
- `3` — продвинутый путь: сохранить полный набор данных для подстановки или подключить Bitwarden. Если не уверены, выбирайте `1`.
|
||||||
|
|
||||||
Дальше installer спросит только про системные модули: Touch ID для Bitwarden, sudoers, aliases, Swift Menu Bar app и автозапуск.
|
Дальше installer спросит только про системные модули: sudoers, aliases, Swift Menu Bar app и автозапуск. Bitwarden и Touch ID появляются только если вы явно выбрали продвинутый путь.
|
||||||
|
|
||||||
Флаги остаются для CI, повторяемых установок и диагностики. Для обычной установки они не нужны.
|
Флаги остаются для CI, повторяемых установок и диагностики. Для обычной установки они не нужны.
|
||||||
|
|
||||||
@@ -69,7 +76,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh |
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||||
| sh -s -- --non-interactive --credential-source keychain --configure-keychain
|
| sh -s -- --non-interactive --auth-profile saved-login --configure-login
|
||||||
```
|
```
|
||||||
|
|
||||||
Принудительно включить интерактивные вопросы:
|
Принудительно включить интерактивные вопросы:
|
||||||
@@ -114,7 +121,7 @@ curl -fsSL https://example.org/dokril/lemana-vpn/raw/branch/main/install.sh \
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
vpn --status
|
vpn --status
|
||||||
Modules: ✅ core=ok, ✅ bitwarden=on, ✅ touchid=on, ✅ dns=on, ✅ app=on, ✅ autostart=on, ✅ patches=active, ✅ keychain=password:yes/totp_seed:yes
|
Modules: ✅ core=ok, 🔐 auth_profile=manual, 🔐 credential_source=none, ⏭️ bitwarden=off, ⏭️ touchid=off, ✅ dns=on, ✅ app=on, ✅ autostart=on, ✅ patches=active, ✅ keychain=password:no/totp_seed:no
|
||||||
VPN disconnected
|
VPN disconnected
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -129,15 +136,17 @@ Emoji в human-выводе помогают быстро отличать но
|
|||||||
| Поле | Значение |
|
| Поле | Значение |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `core=ok` | Есть `openconnect`, `openconnect-lite` и config |
|
| `core=ok` | Есть `openconnect`, `openconnect-lite` и config |
|
||||||
| `bitwarden=on` | Модуль включён и `bw` установлен |
|
| `auth_profile=manual/saved-login/saved-totp/auto` | Как CLI будет открывать окно входа и использовать сохранённые данные |
|
||||||
| `bitwarden=off` | Модуль отключён через `--without-bitwarden` или `LEMANA_VPN_USE_BITWARDEN=0` |
|
| `credential_source=none/keychain/bitwarden` | Откуда брать сохранённые данные, если выбран режим с подстановкой |
|
||||||
|
| `bitwarden=on` | Модуль явно включён и `bw` установлен |
|
||||||
|
| `bitwarden=off` | Модуль отключён по умолчанию или через `--without-bitwarden` |
|
||||||
| `bitwarden=missing` | Модуль включён, но `bw` не найден |
|
| `bitwarden=missing` | Модуль включён, но `bw` не найден |
|
||||||
| `touchid=on/off/missing` | Состояние Touch ID helper |
|
| `touchid=on/off/missing` | Состояние Touch ID helper |
|
||||||
| `dns=on/missing` | Наличие DNS cleanup wrapper |
|
| `dns=on/missing` | Наличие DNS cleanup wrapper |
|
||||||
| `app=on/missing` | Установлен ли `~/Applications/LemanaVPN.app` |
|
| `app=on/missing` | Установлен ли `~/Applications/LemanaVPN.app` |
|
||||||
| `autostart=on/off` | Есть ли LaunchAgent для запуска приложения при логине |
|
| `autostart=on/off` | Есть ли LaunchAgent для запуска приложения при логине |
|
||||||
| `patches=active/pending` | Применены ли runtime-патчи `openconnect-lite` |
|
| `patches=active/pending` | Применены ли runtime-патчи `openconnect-lite` |
|
||||||
| `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и TOTP seed в Keychain |
|
| `keychain=password:yes/totp_seed:yes` | Есть ли LDAP-пароль и постоянный код настройки 2FA в Keychain |
|
||||||
|
|
||||||
JSON-режим тоже отдаёт модульный статус:
|
JSON-режим тоже отдаёт модульный статус:
|
||||||
|
|
||||||
@@ -163,20 +172,20 @@ Detected state:
|
|||||||
Menu Bar app: no
|
Menu Bar app: no
|
||||||
LaunchAgent: no
|
LaunchAgent: no
|
||||||
Keychain password: no
|
Keychain password: no
|
||||||
Keychain TOTP seed: no
|
Keychain 2FA setup: no
|
||||||
```
|
```
|
||||||
|
|
||||||
Если доступен терминал, скрипт ведёт установку вопросами:
|
Если доступен терминал, скрипт ведёт установку вопросами:
|
||||||
|
|
||||||
- выбрать credential source: Bitwarden sync, Keychain с вводом credentials сейчас, или Keychain с настройкой позже;
|
- оставить ручной вход, сохранить только логин/пароль или настроить подстановку сохранённых данных;
|
||||||
- поставить ли Bitwarden CLI, если выбран Bitwarden и `bw` не найден;
|
- поставить Bitwarden CLI только если выбран Bitwarden;
|
||||||
- собрать ли Touch ID helper, если выбран Bitwarden и helper не найден;
|
- собрать Touch ID helper только если выбран Bitwarden;
|
||||||
- собрать ли Swift Menu Bar app, если `~/Applications/LemanaVPN.app` не найден;
|
- собрать ли Swift Menu Bar app, если `~/Applications/LemanaVPN.app` не найден;
|
||||||
- включить ли автозапуск Menu Bar app при логине;
|
- включить ли автозапуск Menu Bar app при логине;
|
||||||
- настроить ли sudoers для `openconnect` и DNS cleanup;
|
- настроить ли sudoers для `openconnect` и DNS cleanup;
|
||||||
- добавить ли алиасы в `~/.zshrc`.
|
- добавить ли алиасы в `~/.zshrc`.
|
||||||
|
|
||||||
Флаги имеют приоритет над вопросами и нужны в основном для CI, диагностики или повторяемых unattended installs. Например, `--credential-source keychain --configure-keychain` сразу выберет Keychain-only flow, а `--no-shell` не будет предлагать алиасы.
|
Флаги имеют приоритет над вопросами и нужны в основном для CI, диагностики или повторяемых unattended installs. Например, `--auth-profile saved-login --configure-login` сразу выберет сохранение логина/пароля, а `--no-shell` не будет предлагать алиасы.
|
||||||
|
|
||||||
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
|
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
|
||||||
|
|
||||||
@@ -211,54 +220,67 @@ LEMANA_VPN_NO_EMOJI=1 sh uninstall.sh
|
|||||||
- `openconnect-lite` config;
|
- `openconnect-lite` config;
|
||||||
- DNS cleanup wrapper.
|
- DNS cleanup wrapper.
|
||||||
|
|
||||||
### Как работают credential sources
|
### Как работают режимы входа
|
||||||
|
|
||||||
У Lemana VPN есть два разных способа подготовить credentials, но один общий runtime-контракт: перед запуском SSO в macOS Keychain должны лежать LDAP-пароль и постоянный TOTP seed для `openconnect-lite`.
|
|
||||||
|
|
||||||
Keychain entries:
|
Keychain entries:
|
||||||
|
|
||||||
- service `openconnect-lite`, account `<LDAP username>` — корпоративный LDAP/domain пароль;
|
- service `openconnect-lite`, account `<LDAP username>` — корпоративный LDAP/domain пароль;
|
||||||
- service `openconnect-lite`, account `totp/<LDAP username>` — постоянный TOTP seed.
|
- service `openconnect-lite`, account `totp/<LDAP username>` — постоянный код настройки 2FA, нужен только для подстановки одноразового кода и auto mode.
|
||||||
|
|
||||||
`credential_source=bitwarden` — это sync-режим. CLI открывает Bitwarden vault, читает item `LM LDAP`, берёт из него LDAP password и TOTP seed, нормализует `otpauth://...secret=...` если нужно, затем записывает оба секрета в macOS Keychain. После этого подключение идёт так же, как в остальных режимах: `openconnect-lite` читает данные из Keychain, генерирует текущий одноразовый TOTP-код из seed и заполняет Keycloak форму.
|
`auth_profile=manual` — режим по умолчанию. CLI не проверяет Keychain и Bitwarden, открывает видимое окно входа и ничего не подставляет. Логин, пароль и одноразовый код вводятся вручную.
|
||||||
|
|
||||||
`credential_source=keychain` — это бесплатный built-in режим без Bitwarden. Пользователь один раз запускает `vpn --configure-keychain` или установку с `--credential-source keychain --configure-keychain`, вводит LDAP password и постоянный TOTP seed. CLI сохраняет их напрямую в macOS Keychain и при следующих подключениях не спрашивает Bitwarden, master password или текущий OTP-код.
|
`auth_profile=saved-login` — CLI берёт из Keychain только LDAP пароль и подставляет логин/пароль в видимое окно входа. Одноразовый код вводится руками. Настройка:
|
||||||
|
|
||||||
Важно: Lemana VPN не хранит и не принимает текущий 6-значный код как постоянную настройку. Такой код живёт около 30 секунд. Для автоматического SSO нужен именно seed: raw Base32 или `otpauth://totp/...?...secret=BASE32`.
|
```sh
|
||||||
|
vpn --configure-login
|
||||||
|
```
|
||||||
|
|
||||||
Если запуск идёт из menu-bar app, интерактивного terminal prompt нет. Поэтому при пустом Keychain приложение покажет ошибку, а настройку нужно один раз выполнить в Terminal:
|
`auth_profile=saved-totp` — CLI берёт LDAP пароль и постоянный код настройки 2FA из Keychain, подставляет поля в видимое окно входа, но submit не нажимает. Настройка:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
vpn --configure-keychain
|
vpn --configure-keychain
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`auth_profile=auto` — старый автоматический путь: скрытый браузер, подстановка всех полей и submit. Запускается только явно:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn-auto
|
||||||
|
vpn --auto
|
||||||
|
```
|
||||||
|
|
||||||
|
`credential_source=bitwarden` — это sync-provider. CLI открывает Bitwarden vault, читает item `LM LDAP`, берёт из него LDAP password и постоянный код настройки 2FA, нормализует `otpauth://...secret=...` если нужно, затем записывает оба секрета в macOS Keychain. Bitwarden не включён по умолчанию.
|
||||||
|
|
||||||
|
Важно: Lemana VPN не хранит и не принимает текущий 6-значный код как постоянную настройку. Такой код живёт около 30 секунд. Для автоматического SSO нужен именно постоянный secret: raw Base32 или `otpauth://totp/...?...secret=BASE32`.
|
||||||
|
|
||||||
|
Если запуск идёт из menu-bar app, интерактивного terminal prompt нет. Поэтому настройку сохранённого логина или полного набора данных нужно один раз выполнить в Terminal.
|
||||||
|
|
||||||
### Bitwarden
|
### Bitwarden
|
||||||
|
|
||||||
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP seed из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
|
Отключён по умолчанию. Его стоит включать только если у пользователя уже есть запись Bitwarden с корпоративным LDAP-паролем и постоянным кодом настройки 2FA.
|
||||||
|
|
||||||
TOTP seed — это постоянный секрет 2FA. Сам одноразовый TOTP-код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа.
|
Постоянный код настройки 2FA — это секрет, который обычно показывается при ручной настройке authenticator. Сам одноразовый код меняется каждые 30 секунд и генерируется `openconnect-lite` в момент входа.
|
||||||
|
|
||||||
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password/TOTP seed из item `LM LDAP` и переложить их в macOS Keychain.
|
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password и постоянный код настройки 2FA из item `LM LDAP` и переложить их в macOS Keychain.
|
||||||
|
|
||||||
Выбрать Keychain вместо Bitwarden можно прямо в installer wizard: пункт `2` вводит LDAP password и TOTP seed сразу после установки, пункт `3` оставляет настройку на потом.
|
Включить Bitwarden можно через продвинутый путь в installer wizard или флагами:
|
||||||
|
|
||||||
То же самое можно задать флагами для неинтерактивной установки:
|
То же самое можно задать флагами для неинтерактивной установки:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh install.sh --non-interactive --credential-source keychain --configure-keychain
|
sh install.sh --non-interactive --with-bitwarden --auth-profile saved-totp
|
||||||
```
|
```
|
||||||
|
|
||||||
В этом режиме credentials нужно положить в Keychain вручную:
|
Для Keychain-only режима без Bitwarden используйте:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
vpn-lemanapro.sh --configure-keychain
|
sh install.sh --non-interactive --auth-profile saved-login --configure-login
|
||||||
```
|
```
|
||||||
|
|
||||||
Если credentials уже лежат в Keychain, подключение без Bitwarden не будет спрашивать пароль заново. CLI явно напишет, что Bitwarden отключён и используются сохранённые LDAP password/TOTP seed из macOS Keychain.
|
Если сохранённые данные уже лежат в Keychain, подключение без Bitwarden не будет спрашивать пароль заново. Для режима `saved-login` одноразовый код всё равно вводится вручную.
|
||||||
|
|
||||||
### Если Bitwarden нет
|
### Если Bitwarden нет
|
||||||
|
|
||||||
Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain.
|
Bitwarden не обязателен. Без него установка по умолчанию работает в полностью ручном режиме: откроется окно входа, а логин, пароль и одноразовый код вводятся руками.
|
||||||
|
|
||||||
Интерактивная установка:
|
Интерактивная установка:
|
||||||
|
|
||||||
@@ -266,27 +288,34 @@ Bitwarden не обязателен. Без него установка рабо
|
|||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
В первом вопросе выбери пункт `2`:
|
В первом вопросе можно просто нажать Enter:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
2) macOS Keychain: ввести LDAP password и TOTP seed сейчас
|
1) Вводить всё вручную (рекомендуется)
|
||||||
```
|
```
|
||||||
|
|
||||||
Это встроенный бесплатный путь: не нужен Bitwarden account, платный Bitwarden TOTP или внешний password manager. Setup prompt спросит корпоративный LDAP-пароль и постоянный TOTP seed. Seed можно вставить как raw Base32 или как `otpauth://totp/...?...secret=BASE32` URI.
|
Это встроенный бесплатный путь: не нужен Bitwarden account, платный Bitwarden TOTP или внешний password manager.
|
||||||
|
|
||||||
|
Если хочется, чтобы логин и пароль подставлялись сами, выбери пункт `2` или выполни позже:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --configure-login
|
||||||
|
```
|
||||||
|
|
||||||
Что понадобится:
|
Что понадобится:
|
||||||
|
|
||||||
- LDAP username;
|
- LDAP username;
|
||||||
- LDAP password: корпоративный LDAP/domain пароль, не мастер-пароль Bitwarden;
|
- LDAP password: корпоративный LDAP/domain пароль, не мастер-пароль Bitwarden;
|
||||||
- TOTP secret из корпоративной 2FA настройки.
|
- одноразовый код из корпоративной 2FA при каждом подключении.
|
||||||
|
|
||||||
Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP.
|
Постоянный код настройки 2FA нужен только для `vpn --configure-keychain`, `saved-totp` и `vpn-auto`.
|
||||||
|
|
||||||
Не вставляй текущий 6-значный authenticator code в `vpn --configure-keychain`. Lemana VPN сохраняет в Keychain постоянный TOTP seed, а `openconnect-lite` по нему генерирует свежие одноразовые коды во время каждого SSO login.
|
Не вставляй текущий 6-значный authenticator code в `vpn --configure-keychain`. Lemana VPN сохраняет в Keychain постоянный код настройки 2FA, а `openconnect-lite` по нему генерирует свежие одноразовые коды во время каждого SSO login.
|
||||||
|
|
||||||
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для ввода LDAP/TOTP. Если Keychain пустой, приложение покажет ошибку. В этом случае один раз выполни в Terminal:
|
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для сохранения пароля. Настройку saved-login или saved-totp нужно один раз выполнить в Terminal:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
vpn --configure-login
|
||||||
vpn --configure-keychain
|
vpn --configure-keychain
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -295,13 +324,13 @@ vpn --configure-keychain
|
|||||||
1. Открой QR-код в приложении/на портале, где настраивалась 2FA.
|
1. Открой QR-код в приложении/на портале, где настраивалась 2FA.
|
||||||
2. Найди режим ручной настройки, где показывается secret.
|
2. Найди режим ручной настройки, где показывается secret.
|
||||||
3. Если доступен только QR, его нужно расшифровать любым локальным QR-сканером и взять параметр `secret`.
|
3. Если доступен только QR, его нужно расшифровать любым локальным QR-сканером и взять параметр `secret`.
|
||||||
4. Вставь secret в prompt `TOTP secret (BASE32...)`.
|
4. Вставь secret в prompt `2FA setup secret BASE32`.
|
||||||
|
|
||||||
Если TOTP secret получить нельзя, автоматический headless-вход невозможен: `openconnect-lite` не сможет сам генерировать свежий TOTP-код на каждом входе.
|
Если постоянный 2FA secret получить нельзя, автоматический headless-вход невозможен: `openconnect-lite` не сможет сам генерировать свежий одноразовый код на каждом входе.
|
||||||
|
|
||||||
### Touch ID
|
### Touch ID
|
||||||
|
|
||||||
Включён по умолчанию. Установщик собирает `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и кладёт бинарник в `~/bin/keychain-fingerprint`.
|
Отключён по умолчанию и нужен только для Bitwarden-пути. Если пользователь выбрал Bitwarden, установщик может собрать `keychain-fingerprint` из `https://github.com/dss99911/keychain-fingerprint.git` и положить бинарник в `~/bin/keychain-fingerprint`.
|
||||||
|
|
||||||
Важно: этот helper показывает системный Touch ID prompt перед чтением мастер-пароля Bitwarden, но это не аппаратный Keychain ACL. Это удобный локальный гейт поверх записи Keychain.
|
Важно: этот helper показывает системный Touch ID prompt перед чтением мастер-пароля Bitwarden, но это не аппаратный Keychain ACL. Это удобный локальный гейт поверх записи Keychain.
|
||||||
|
|
||||||
@@ -321,6 +350,15 @@ sh install.sh --without-touchid
|
|||||||
|
|
||||||
Приложение живёт в macOS status bar, запускает `~/bin/vpn-lemanapro.sh --json`, показывает состояние VPN, IP, оставшееся время сессии, health-check тоннеля и строку состояния модулей.
|
Приложение живёт в macOS status bar, запускает `~/bin/vpn-lemanapro.sh --json`, показывает состояние VPN, IP, оставшееся время сессии, health-check тоннеля и строку состояния модулей.
|
||||||
|
|
||||||
|
В отключённом состоянии меню показывает текущий режим установки:
|
||||||
|
|
||||||
|
- `Режим: полностью вручную` — доступна только обычная кнопка `Подключить`; логин, пароль и одноразовый код вводятся руками.
|
||||||
|
- `Режим: логин и пароль сохранены` — доступен вход с подстановкой логина/пароля и ручной ввод одноразового кода.
|
||||||
|
- `Режим: сохранённые данные без автоотправки` — доступны сохранённые данные, ручной fallback и явный automatic mode.
|
||||||
|
- `Режим: автоматический вход` — обычная кнопка `Подключить` запускает настроенный auto-flow, а ручные fallback-кнопки остаются доступны.
|
||||||
|
|
||||||
|
Кнопки, для которых текущая установка не готова, не показываются. Например, если не сохранён постоянный код настройки 2FA, в меню не будет автоматического входа. Если рядом с режимом есть подсказка `Настройте в Terminal`, выполните указанную команду в Terminal и нажмите `Обновить статус модулей`.
|
||||||
|
|
||||||
Строка состояния модулей в меню приложения использует те же маркеры, что CLI: `✅` для готового модуля, `⏭️` для отключённого опционального модуля и `⚠️` для проблемы. Иконка строки тоже меняется: `checkmark.circle` для полностью готового набора и `exclamationmark.triangle` для неполной установки.
|
Строка состояния модулей в меню приложения использует те же маркеры, что CLI: `✅` для готового модуля, `⏭️` для отключённого опционального модуля и `⚠️` для проблемы. Иконка строки тоже меняется: `checkmark.circle` для полностью готового набора и `exclamationmark.triangle` для неполной установки.
|
||||||
|
|
||||||
Если в меню видно `modules unavailable: update CLI`, значит запущенное приложение обращается к старому `~/bin/vpn-lemanapro.sh`, который ещё не умеет отдавать модульный статус. Повтори установку через `curl`; установщик обновит CLI и перезапустит уже запущенное `LemanaVPN.app`.
|
Если в меню видно `modules unavailable: update CLI`, значит запущенное приложение обращается к старому `~/bin/vpn-lemanapro.sh`, который ещё не умеет отдавать модульный статус. Повтори установку через `curl`; установщик обновит CLI и перезапустит уже запущенное `LemanaVPN.app`.
|
||||||
@@ -352,9 +390,10 @@ open ~/Applications/LemanaVPN.app
|
|||||||
## Использование
|
## Использование
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
vpn # подключиться
|
vpn # подключиться в настроенном режиме; fresh install = полностью вручную
|
||||||
vpn-auto # автоматический режим: скрытый браузер, auto-fill и submit
|
vpn-auto # явный автоматический режим: скрытый браузер, auto-fill и submit
|
||||||
vpn-manual # ручной режим: видимый браузер, auto-fill без submit
|
vpn --saved-login # подставить сохранённые логин/пароль, одноразовый код руками
|
||||||
|
vpn-manual # видимый браузер, сохранённые данные auto-fill без submit
|
||||||
vpn-manual-full # полностью ручной режим: видимый браузер без auto-fill и submit
|
vpn-manual-full # полностью ручной режим: видимый браузер без auto-fill и submit
|
||||||
vpn --manual # то же самое без alias
|
vpn --manual # то же самое без alias
|
||||||
vpn --manual-full # то же самое без alias
|
vpn --manual-full # то же самое без alias
|
||||||
@@ -368,18 +407,19 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение
|
|||||||
|
|
||||||
Режимы подключения:
|
Режимы подключения:
|
||||||
|
|
||||||
- `auto` — режим по умолчанию. Браузер скрытый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain, Keycloak форма заполняется и отправляется автоматически.
|
- `manual` — режим fresh install по умолчанию. Браузер видимый, auto-fill отключён полностью, проверки сохранённых LDAP/TOTP credentials пропускаются: поля Keycloak заполняешь и отправляешь сам.
|
||||||
- `manual` — браузер видимый, LDAP-пароль и TOTP берутся из Bitwarden/Keychain и подставляются в поля, но кнопки входа не нажимаются. Нажимаешь сам после проверки формы.
|
- `saved-login` — браузер видимый, логин/пароль берутся из Keychain, одноразовый код вводится руками, submit не нажимается.
|
||||||
- `manual-full` — браузер видимый, auto-fill отключён полностью: поля Keycloak заполняешь и отправляешь сам.
|
- `saved-totp` / `--manual` — браузер видимый, LDAP-пароль и код 2FA берутся из Keychain/Bitwarden и подставляются в поля, но кнопка входа не нажимается.
|
||||||
|
- `auto` / `vpn-auto` — явный автоматический режим. Браузер скрытый, LDAP-пароль и код 2FA берутся из Keychain/Bitwarden, форма заполняется и отправляется автоматически.
|
||||||
- `--manual-sso` оставлен как совместимый alias для `--manual`.
|
- `--manual-sso` оставлен как совместимый alias для `--manual`.
|
||||||
- `--manual-no-autofill` оставлен как совместимый alias для `--manual-full`.
|
- `--manual-no-autofill` оставлен как совместимый alias для `--manual-full`.
|
||||||
|
|
||||||
Первый запуск с Bitwarden:
|
Первый запуск с Bitwarden, если он выбран явно:
|
||||||
|
|
||||||
1. CLI проверит `bw`.
|
1. CLI проверит `bw`.
|
||||||
2. Если vault locked, попросит мастер-пароль.
|
2. Если vault locked, попросит мастер-пароль.
|
||||||
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
|
3. Если установлен Touch ID helper, предложит сохранить мастер-пароль за Touch ID prompt.
|
||||||
4. Достанет `LM LDAP`, запишет LDAP-пароль и TOTP seed в Keychain.
|
4. Достанет `LM LDAP`, запишет LDAP-пароль и постоянный код настройки 2FA в Keychain.
|
||||||
5. Запустит `openconnect-lite` и пройдёт Keycloak SSO.
|
5. Запустит `openconnect-lite` и пройдёт Keycloak SSO.
|
||||||
|
|
||||||
## Настройка
|
## Настройка
|
||||||
@@ -387,29 +427,31 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение
|
|||||||
Файл `~/.config/lemana-vpn/env`:
|
Файл `~/.config/lemana-vpn/env`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
LEMANA_VPN_USERNAME="60103293"
|
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
|
||||||
LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
|
LEMANA_VPN_AUTH_PROFILE="manual"
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE="none"
|
||||||
LEMANA_VPN_BW_ITEM="LM LDAP"
|
LEMANA_VPN_BW_ITEM="LM LDAP"
|
||||||
LEMANA_VPN_USE_BITWARDEN="1"
|
LEMANA_VPN_USE_BITWARDEN="0"
|
||||||
LEMANA_VPN_USE_TOUCHID="1"
|
LEMANA_VPN_USE_TOUCHID="0"
|
||||||
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||||
```
|
```
|
||||||
|
|
||||||
Для бесплатного Keychain-only источника:
|
Для режима с сохранённым логином и паролем:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
LEMANA_VPN_USERNAME="60103293"
|
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
|
||||||
|
LEMANA_VPN_AUTH_PROFILE="saved-login"
|
||||||
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
|
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
|
||||||
LEMANA_VPN_USE_BITWARDEN="0"
|
LEMANA_VPN_USE_BITWARDEN="0"
|
||||||
LEMANA_VPN_USE_TOUCHID="0"
|
LEMANA_VPN_USE_TOUCHID="0"
|
||||||
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||||
```
|
```
|
||||||
|
|
||||||
Для другого логина:
|
Если LDAP username не указан при установке, `vpn` спросит его при первом интерактивном запуске в Terminal и сохранит в `~/.config/lemana-vpn/env`. Для неинтерактивной установки можно указать его сразу:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||||
| sh -s -- --username 12345678
|
| sh -s -- --username <LDAP_USERNAME>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bitwarden item
|
## Bitwarden item
|
||||||
@@ -421,7 +463,7 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
|||||||
- password: LDAP пароль;
|
- password: LDAP пароль;
|
||||||
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
|
- TOTP: `otpauth://...secret=BASE32...` или raw BASE32 secret.
|
||||||
|
|
||||||
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный TOTP secret, из которого коды генерируются автоматически.
|
Это не 6-значный одноразовый код. В Bitwarden должен лежать постоянный 2FA secret, из которого коды генерируются автоматически.
|
||||||
|
|
||||||
## Почему DNS wrapper, а не wildcard sudoers
|
## Почему DNS wrapper, а не wildcard sudoers
|
||||||
|
|
||||||
@@ -512,6 +554,8 @@ vpn --manual
|
|||||||
vpn --manual-full
|
vpn --manual-full
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Этот режим не требует заранее сохранённого LDAP username/password/TOTP и не запускает Bitwarden/Keychain preflight.
|
||||||
|
|
||||||
Если установка падает на строке `install: /usr/local/sbin/...: No such file or directory`, значит на машине не было `/usr/local/sbin`. Актуальный `install.sh` создаёт эту директорию сам; достаточно повторить установку свежей командой `curl`.
|
Если установка падает на строке `install: /usr/local/sbin/...: No such file or directory`, значит на машине не было `/usr/local/sbin`. Актуальный `install.sh` создаёт эту директорию сам; достаточно повторить установку свежей командой `curl`.
|
||||||
|
|
||||||
CLI перед подключением патчит `openconnect-lite`:
|
CLI перед подключением патчит `openconnect-lite`:
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ struct VPNMenuView: View {
|
|||||||
Button("Обновить статус модулей") {
|
Button("Обновить статус модулей") {
|
||||||
vpnManager.refreshStatus()
|
vpnManager.refreshStatus()
|
||||||
}
|
}
|
||||||
|
Button("Изменить логин и пароль") {
|
||||||
|
vpnManager.configureLogin()
|
||||||
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button("Открыть логи") {
|
Button("Открыть логи") {
|
||||||
let logPath = FileManager.default.homeDirectoryForCurrentUser
|
let logPath = FileManager.default.homeDirectoryForCurrentUser
|
||||||
@@ -91,19 +94,18 @@ struct VPNMenuView: View {
|
|||||||
Group {
|
Group {
|
||||||
Label("VPN отключён", systemImage: "circle")
|
Label("VPN отключён", systemImage: "circle")
|
||||||
.disabled(true)
|
.disabled(true)
|
||||||
|
Label(vpnManager.installModeLabel, systemImage: "slider.horizontal.3")
|
||||||
|
.disabled(true)
|
||||||
|
if let setupHint = vpnManager.installSetupHint {
|
||||||
|
Label(setupHint, systemImage: "info.circle")
|
||||||
|
.disabled(true)
|
||||||
|
}
|
||||||
Divider()
|
Divider()
|
||||||
Button("Подключить автоматически") {
|
ForEach(vpnManager.availableLaunchModes) { mode in
|
||||||
vpnManager.connect(mode: .auto)
|
Button(mode.menuTitle) {
|
||||||
|
vpnManager.connect(mode: mode)
|
||||||
}
|
}
|
||||||
.keyboardShortcut("c")
|
|
||||||
Button("Подключить вручную") {
|
|
||||||
vpnManager.connect(mode: .manual)
|
|
||||||
}
|
}
|
||||||
.keyboardShortcut("m")
|
|
||||||
Button("Подключить полностью вручную") {
|
|
||||||
vpnManager.connect(mode: .manualFull)
|
|
||||||
}
|
|
||||||
.keyboardShortcut("f")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ struct VPNEvent: Decodable {
|
|||||||
var reason: String?
|
var reason: String?
|
||||||
var message: String?
|
var message: String?
|
||||||
var source: String?
|
var source: String?
|
||||||
|
var profile: String?
|
||||||
|
var auth_profile: String?
|
||||||
var tier: Int?
|
var tier: Int?
|
||||||
var modules: ModuleStatus?
|
var modules: ModuleStatus?
|
||||||
}
|
}
|
||||||
@@ -34,9 +36,18 @@ struct ModuleStatus: Decodable {
|
|||||||
|
|
||||||
struct Credentials: Decodable {
|
struct Credentials: Decodable {
|
||||||
var source: String
|
var source: String
|
||||||
|
var auth_profile: String?
|
||||||
|
var login_ready: Bool?
|
||||||
var keychain_ready: Bool
|
var keychain_ready: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AppMenu: Decodable {
|
||||||
|
var install_label: String
|
||||||
|
var configured_mode: String
|
||||||
|
var available_modes: [String]
|
||||||
|
var setup_hint: String?
|
||||||
|
}
|
||||||
|
|
||||||
struct Keychain: Decodable {
|
struct Keychain: Decodable {
|
||||||
var password: Bool
|
var password: Bool
|
||||||
var totp_seed: Bool
|
var totp_seed: Bool
|
||||||
@@ -58,6 +69,7 @@ struct ModuleStatus: Decodable {
|
|||||||
|
|
||||||
var core: Core
|
var core: Core
|
||||||
var credentials: Credentials?
|
var credentials: Credentials?
|
||||||
|
var app_menu: AppMenu?
|
||||||
var bitwarden: ToggleModule
|
var bitwarden: ToggleModule
|
||||||
var touchid: ToggleModule
|
var touchid: ToggleModule
|
||||||
var keychain: Keychain
|
var keychain: Keychain
|
||||||
@@ -69,7 +81,16 @@ struct ModuleStatus: Decodable {
|
|||||||
let coreReady = core.openconnect && core.openconnect_lite && core.openconnect_lite_config
|
let coreReady = core.openconnect && core.openconnect_lite && core.openconnect_lite_config
|
||||||
let bitwardenReady = !bitwarden.enabled || bitwarden.installed
|
let bitwardenReady = !bitwarden.enabled || bitwarden.installed
|
||||||
let touchReady = !touchid.enabled || touchid.installed
|
let touchReady = !touchid.enabled || touchid.installed
|
||||||
let keychainReady = keychain.password && keychain.totp_seed
|
let profile = credentials?.auth_profile ?? "manual"
|
||||||
|
let keychainReady: Bool
|
||||||
|
switch profile {
|
||||||
|
case "manual":
|
||||||
|
keychainReady = true
|
||||||
|
case "saved-login":
|
||||||
|
keychainReady = keychain.password
|
||||||
|
default:
|
||||||
|
keychainReady = keychain.password && keychain.totp_seed
|
||||||
|
}
|
||||||
let appReady = app?.installed ?? true
|
let appReady = app?.installed ?? true
|
||||||
|
|
||||||
return !coreReady
|
return !coreReady
|
||||||
@@ -87,14 +108,23 @@ struct ModuleStatus: Decodable {
|
|||||||
|
|
||||||
var summary: String {
|
var summary: String {
|
||||||
let coreState = core.openconnect && core.openconnect_lite && core.openconnect_lite_config ? "✅ core" : "⚠️ core"
|
let coreState = core.openconnect && core.openconnect_lite && core.openconnect_lite_config ? "✅ core" : "⚠️ core"
|
||||||
let credentialState = credentials.map { "🔐 \($0.source)" } ?? "🔐 legacy"
|
let credentialState = credentials.map { "🔐 \($0.auth_profile ?? "manual")/\($0.source)" } ?? "🔐 legacy"
|
||||||
let bwState = bitwarden.enabled ? (bitwarden.installed ? "✅ bw" : "⚠️ bw") : "⏭️ bw"
|
let bwState = bitwarden.enabled ? (bitwarden.installed ? "✅ bw" : "⚠️ bw") : "⏭️ bw"
|
||||||
let touchState = touchid.enabled ? (touchid.installed ? "✅ touch" : "⚠️ touch") : "⏭️ touch"
|
let touchState = touchid.enabled ? (touchid.installed ? "✅ touch" : "⚠️ touch") : "⏭️ touch"
|
||||||
let dnsState = dns_cleanup.installed ? "✅ dns" : "⚠️ dns"
|
let dnsState = dns_cleanup.installed ? "✅ dns" : "⚠️ dns"
|
||||||
let appState = app.map { $0.installed ? "✅ app" : "⚠️ app" } ?? "❔ app"
|
let appState = app.map { $0.installed ? "✅ app" : "⚠️ app" } ?? "❔ app"
|
||||||
let autostartState = app.map { $0.autostart ? "✅ autostart" : "⏭️ autostart" } ?? "❔ autostart"
|
let autostartState = app.map { $0.autostart ? "✅ autostart" : "⏭️ autostart" } ?? "❔ autostart"
|
||||||
let patchState = patches.active ? "✅ patches" : "⚠️ patches"
|
let patchState = patches.active ? "✅ patches" : "⚠️ patches"
|
||||||
let keychainState = "\(keychain.password && keychain.totp_seed ? "✅" : "⚠️") kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")"
|
let keychainReady: Bool
|
||||||
|
switch credentials?.auth_profile ?? "manual" {
|
||||||
|
case "manual":
|
||||||
|
keychainReady = true
|
||||||
|
case "saved-login":
|
||||||
|
keychainReady = keychain.password
|
||||||
|
default:
|
||||||
|
keychainReady = keychain.password && keychain.totp_seed
|
||||||
|
}
|
||||||
|
let keychainState = "\(keychainReady ? "✅" : "⚠️") kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")"
|
||||||
return [coreState, credentialState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
return [coreState, credentialState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,18 +149,34 @@ enum VPNState: Equatable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum VPNLaunchMode: String {
|
enum VPNLaunchMode: String, Identifiable {
|
||||||
case auto
|
case configured = "configured"
|
||||||
case manual
|
case auto = "auto"
|
||||||
case manualFull
|
case savedLogin = "saved-login"
|
||||||
|
case manual = "saved-totp"
|
||||||
|
case manualFull = "manual-full"
|
||||||
|
|
||||||
var cliArgument: String {
|
var id: String { rawValue }
|
||||||
|
|
||||||
|
var cliArgument: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .configured: return nil
|
||||||
case .auto: return "--auto"
|
case .auto: return "--auto"
|
||||||
|
case .savedLogin: return "--saved-login"
|
||||||
case .manual: return "--manual"
|
case .manual: return "--manual"
|
||||||
case .manualFull: return "--manual-full"
|
case .manualFull: return "--manual-full"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var menuTitle: String {
|
||||||
|
switch self {
|
||||||
|
case .configured: return "Подключить"
|
||||||
|
case .auto: return "Подключить автоматически"
|
||||||
|
case .savedLogin: return "Подставить только логин и пароль"
|
||||||
|
case .manual: return "Подключить с сохранёнными данными"
|
||||||
|
case .manualFull: return "Подключить полностью вручную"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -140,6 +186,7 @@ class VPNManager: ObservableObject {
|
|||||||
@Published var tunnelHealthy: Bool = true
|
@Published var tunnelHealthy: Bool = true
|
||||||
@Published var moduleSummary: String = "modules loading..."
|
@Published var moduleSummary: String = "modules loading..."
|
||||||
@Published var moduleStatusSystemImage: String = "hourglass"
|
@Published var moduleStatusSystemImage: String = "hourglass"
|
||||||
|
@Published var moduleStatus: ModuleStatus?
|
||||||
|
|
||||||
private var process: Process?
|
private var process: Process?
|
||||||
private var outputPipe: Pipe?
|
private var outputPipe: Pipe?
|
||||||
@@ -153,7 +200,7 @@ class VPNManager: ObservableObject {
|
|||||||
private var autoReconnectAttempts: Int = 0
|
private var autoReconnectAttempts: Int = 0
|
||||||
private var reconnectTimer: Timer?
|
private var reconnectTimer: Timer?
|
||||||
private var consecutiveHealthFailures: Int = 0
|
private var consecutiveHealthFailures: Int = 0
|
||||||
private var currentLaunchMode: VPNLaunchMode = .auto
|
private var currentLaunchMode: VPNLaunchMode = .configured
|
||||||
|
|
||||||
private let healthCheckInterval: TimeInterval = 10
|
private let healthCheckInterval: TimeInterval = 10
|
||||||
private let maxAutoReconnectAttempts: Int = 3
|
private let maxAutoReconnectAttempts: Int = 3
|
||||||
@@ -191,6 +238,28 @@ class VPNManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var installModeLabel: String {
|
||||||
|
moduleStatus?.app_menu?.install_label ?? "Режим: проверяю установку"
|
||||||
|
}
|
||||||
|
|
||||||
|
var installSetupHint: String? {
|
||||||
|
if let hint = moduleStatus?.app_menu?.setup_hint {
|
||||||
|
return hint
|
||||||
|
}
|
||||||
|
if moduleStatus != nil, moduleStatus?.app_menu == nil {
|
||||||
|
return "Обновите CLI: sh install.sh"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableLaunchModes: [VPNLaunchMode] {
|
||||||
|
guard let menu = moduleStatus?.app_menu else {
|
||||||
|
return [.configured]
|
||||||
|
}
|
||||||
|
let modes = menu.available_modes.compactMap(VPNLaunchMode.init(rawValue:))
|
||||||
|
return modes.isEmpty ? [.configured] : modes
|
||||||
|
}
|
||||||
|
|
||||||
func refreshStatus() {
|
func refreshStatus() {
|
||||||
let proc = Process()
|
let proc = Process()
|
||||||
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||||
@@ -211,6 +280,7 @@ class VPNManager: ObservableObject {
|
|||||||
guard !lastLine.isEmpty, let jsonData = lastLine.data(using: .utf8) else {
|
guard !lastLine.isEmpty, let jsonData = lastLine.data(using: .utf8) else {
|
||||||
self.moduleSummary = "modules unavailable"
|
self.moduleSummary = "modules unavailable"
|
||||||
self.moduleStatusSystemImage = "questionmark.circle"
|
self.moduleStatusSystemImage = "questionmark.circle"
|
||||||
|
self.moduleStatus = nil
|
||||||
self.log("[modules] status refresh returned no JSON output")
|
self.log("[modules] status refresh returned no JSON output")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -221,6 +291,7 @@ class VPNManager: ObservableObject {
|
|||||||
} catch {
|
} catch {
|
||||||
self.moduleSummary = "modules unavailable"
|
self.moduleSummary = "modules unavailable"
|
||||||
self.moduleStatusSystemImage = "exclamationmark.triangle"
|
self.moduleStatusSystemImage = "exclamationmark.triangle"
|
||||||
|
self.moduleStatus = nil
|
||||||
let compact = text.replacingOccurrences(of: "\n", with: "\\n")
|
let compact = text.replacingOccurrences(of: "\n", with: "\\n")
|
||||||
let preview = compact.count > 500 ? String(compact.prefix(500)) + "..." : compact
|
let preview = compact.count > 500 ? String(compact.prefix(500)) + "..." : compact
|
||||||
self.log("[modules] status decode failed: \(error.localizedDescription); output=\(preview)")
|
self.log("[modules] status decode failed: \(error.localizedDescription); output=\(preview)")
|
||||||
@@ -230,12 +301,14 @@ class VPNManager: ObservableObject {
|
|||||||
guard let modules = response.modules else {
|
guard let modules = response.modules else {
|
||||||
self.moduleSummary = "modules unavailable: update CLI"
|
self.moduleSummary = "modules unavailable: update CLI"
|
||||||
self.moduleStatusSystemImage = "exclamationmark.triangle"
|
self.moduleStatusSystemImage = "exclamationmark.triangle"
|
||||||
|
self.moduleStatus = nil
|
||||||
self.log("[modules] status has no modules field; reinstall CLI with install.sh")
|
self.log("[modules] status has no modules field; reinstall CLI with install.sh")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.moduleSummary = modules.summary
|
self.moduleSummary = modules.summary
|
||||||
self.moduleStatusSystemImage = modules.systemImage
|
self.moduleStatusSystemImage = modules.systemImage
|
||||||
|
self.moduleStatus = modules
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +319,7 @@ class VPNManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(mode: VPNLaunchMode = .auto) {
|
func connect(mode: VPNLaunchMode = .configured) {
|
||||||
guard !isRunning else {
|
guard !isRunning else {
|
||||||
log("connect() called but process already running")
|
log("connect() called but process already running")
|
||||||
return
|
return
|
||||||
@@ -264,7 +337,11 @@ class VPNManager: ObservableObject {
|
|||||||
|
|
||||||
let proc = Process()
|
let proc = Process()
|
||||||
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||||
proc.arguments = ["-l", scriptPath, "--json", mode.cliArgument]
|
var arguments = ["-l", scriptPath, "--json"]
|
||||||
|
if let cliArgument = mode.cliArgument {
|
||||||
|
arguments.append(cliArgument)
|
||||||
|
}
|
||||||
|
proc.arguments = arguments
|
||||||
proc.environment = processEnvironment()
|
proc.environment = processEnvironment()
|
||||||
|
|
||||||
let stdoutPipe = Pipe()
|
let stdoutPipe = Pipe()
|
||||||
@@ -337,6 +414,10 @@ class VPNManager: ObservableObject {
|
|||||||
proc.interrupt()
|
proc.interrupt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func configureLogin() {
|
||||||
|
openTerminalCommand(name: "configure-login", command: "\(shellQuote(scriptPath)) --configure-login")
|
||||||
|
}
|
||||||
|
|
||||||
func quit() {
|
func quit() {
|
||||||
log("-- App quit --")
|
log("-- App quit --")
|
||||||
userInitiatedDisconnect = true
|
userInitiatedDisconnect = true
|
||||||
@@ -357,6 +438,7 @@ class VPNManager: ObservableObject {
|
|||||||
if let modules = event.modules {
|
if let modules = event.modules {
|
||||||
moduleSummary = modules.summary
|
moduleSummary = modules.summary
|
||||||
moduleStatusSystemImage = modules.systemImage
|
moduleStatusSystemImage = modules.systemImage
|
||||||
|
moduleStatus = modules
|
||||||
}
|
}
|
||||||
|
|
||||||
log("[event] \(event.event)" + {
|
log("[event] \(event.event)" + {
|
||||||
@@ -367,6 +449,7 @@ class VPNManager: ObservableObject {
|
|||||||
if let a = event.attempt { extras.append("attempt=\(a)") }
|
if let a = event.attempt { extras.append("attempt=\(a)") }
|
||||||
if let m = event.message { extras.append("msg=\(m)") }
|
if let m = event.message { extras.append("msg=\(m)") }
|
||||||
if let r = event.reason { extras.append("reason=\(r)") }
|
if let r = event.reason { extras.append("reason=\(r)") }
|
||||||
|
if let p = event.auth_profile ?? event.profile { extras.append("profile=\(p)") }
|
||||||
return extras.isEmpty ? "" : " (\(extras.joined(separator: ", ")))"
|
return extras.isEmpty ? "" : " (\(extras.joined(separator: ", ")))"
|
||||||
}())
|
}())
|
||||||
|
|
||||||
@@ -383,6 +466,26 @@ class VPNManager: ObservableObject {
|
|||||||
log(" Credential source: \(source)")
|
log(" Credential source: \(source)")
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
case "auth_profile":
|
||||||
|
if let message = event.message {
|
||||||
|
log(" \(message)")
|
||||||
|
} else if let profile = event.profile {
|
||||||
|
log(" Auth profile: \(profile)")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case "manual_sso", "auto_sso":
|
||||||
|
let profile = event.auth_profile ?? event.profile ?? "configured"
|
||||||
|
log(" SSO mode: \(profile)")
|
||||||
|
return
|
||||||
|
case "username_required", "username_saved":
|
||||||
|
if let message = event.message {
|
||||||
|
log(" \(message)")
|
||||||
|
} else if event.event == "username_required" {
|
||||||
|
log(" LDAP username is missing; configure it in Terminal")
|
||||||
|
} else {
|
||||||
|
log(" LDAP username saved")
|
||||||
|
}
|
||||||
|
return
|
||||||
case "bw_cached":
|
case "bw_cached":
|
||||||
state = .unlocking(tier: "cached")
|
state = .unlocking(tier: "cached")
|
||||||
case "bw_touchid":
|
case "bw_touchid":
|
||||||
@@ -446,6 +549,46 @@ class VPNManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func openTerminalCommand(name: String, command: String) {
|
||||||
|
let stateDir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
.appendingPathComponent(".local/state/vpn-lemanapro")
|
||||||
|
let commandURL = stateDir.appendingPathComponent("\(name).command")
|
||||||
|
let script = """
|
||||||
|
#!/bin/zsh
|
||||||
|
\(command)
|
||||||
|
status=$?
|
||||||
|
printf '\\n'
|
||||||
|
if [ "$status" -eq 0 ]; then
|
||||||
|
printf 'Логин и пароль обновлены.\\n'
|
||||||
|
else
|
||||||
|
printf 'Команда завершилась с ошибкой: %s\\n' "$status"
|
||||||
|
fi
|
||||||
|
printf 'Нажмите Enter, чтобы закрыть окно...'
|
||||||
|
read -r _
|
||||||
|
exit "$status"
|
||||||
|
"""
|
||||||
|
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: stateDir, withIntermediateDirectories: true)
|
||||||
|
try script.write(to: commandURL, atomically: true, encoding: .utf8)
|
||||||
|
try FileManager.default.setAttributes([.posixPermissions: 0o700], ofItemAtPath: commandURL.path)
|
||||||
|
|
||||||
|
let proc = Process()
|
||||||
|
proc.executableURL = URL(fileURLWithPath: "/usr/bin/open")
|
||||||
|
proc.arguments = ["-a", "Terminal", commandURL.path]
|
||||||
|
try proc.run()
|
||||||
|
log("Opened Terminal credential setup: \(commandURL.path)")
|
||||||
|
} catch {
|
||||||
|
log("Credential setup launch failed: \(error.localizedDescription)")
|
||||||
|
lastError = error.localizedDescription
|
||||||
|
state = .error(message: error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func shellQuote(_ value: String) -> String {
|
||||||
|
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
private func handleTermination(exitCode: Int32) {
|
private func handleTermination(exitCode: Int32) {
|
||||||
log("-- Process terminated (exit=\(exitCode), userInitiated=\(userInitiatedDisconnect)) --")
|
log("-- Process terminated (exit=\(exitCode), userInitiated=\(userInitiatedDisconnect)) --")
|
||||||
stopTimer()
|
stopTimer()
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ set -euo pipefail
|
|||||||
|
|
||||||
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
||||||
CONFIG_FILE="$CONFIG_DIR/env"
|
CONFIG_FILE="$CONFIG_DIR/env"
|
||||||
|
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
||||||
|
|
||||||
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
|
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
|
||||||
_ENV_LEMANA_VPN_BW_ITEM="${LEMANA_VPN_BW_ITEM+x}${LEMANA_VPN_BW_ITEM-}"
|
_ENV_LEMANA_VPN_BW_ITEM="${LEMANA_VPN_BW_ITEM+x}${LEMANA_VPN_BW_ITEM-}"
|
||||||
|
_ENV_LEMANA_VPN_AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE+x}${LEMANA_VPN_AUTH_PROFILE-}"
|
||||||
_ENV_LEMANA_VPN_USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN+x}${LEMANA_VPN_USE_BITWARDEN-}"
|
_ENV_LEMANA_VPN_USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN+x}${LEMANA_VPN_USE_BITWARDEN-}"
|
||||||
_ENV_LEMANA_VPN_CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE+x}${LEMANA_VPN_CREDENTIAL_SOURCE-}"
|
_ENV_LEMANA_VPN_CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE+x}${LEMANA_VPN_CREDENTIAL_SOURCE-}"
|
||||||
_ENV_LEMANA_VPN_USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID+x}${LEMANA_VPN_USE_TOUCHID-}"
|
_ENV_LEMANA_VPN_USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID+x}${LEMANA_VPN_USE_TOUCHID-}"
|
||||||
@@ -18,6 +20,7 @@ fi
|
|||||||
|
|
||||||
[[ "${_ENV_LEMANA_VPN_USERNAME:0:1}" == "x" ]] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME:1}"
|
[[ "${_ENV_LEMANA_VPN_USERNAME:0:1}" == "x" ]] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_BW_ITEM:0:1}" == "x" ]] && LEMANA_VPN_BW_ITEM="${_ENV_LEMANA_VPN_BW_ITEM:1}"
|
[[ "${_ENV_LEMANA_VPN_BW_ITEM:0:1}" == "x" ]] && LEMANA_VPN_BW_ITEM="${_ENV_LEMANA_VPN_BW_ITEM:1}"
|
||||||
|
[[ "${_ENV_LEMANA_VPN_AUTH_PROFILE:0:1}" == "x" ]] && LEMANA_VPN_AUTH_PROFILE="${_ENV_LEMANA_VPN_AUTH_PROFILE:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_USE_BITWARDEN:0:1}" == "x" ]] && LEMANA_VPN_USE_BITWARDEN="${_ENV_LEMANA_VPN_USE_BITWARDEN:1}"
|
[[ "${_ENV_LEMANA_VPN_USE_BITWARDEN:0:1}" == "x" ]] && LEMANA_VPN_USE_BITWARDEN="${_ENV_LEMANA_VPN_USE_BITWARDEN:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:0:1}" == "x" ]] && LEMANA_VPN_CREDENTIAL_SOURCE="${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:1}"
|
[[ "${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:0:1}" == "x" ]] && LEMANA_VPN_CREDENTIAL_SOURCE="${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_USE_TOUCHID:0:1}" == "x" ]] && LEMANA_VPN_USE_TOUCHID="${_ENV_LEMANA_VPN_USE_TOUCHID:1}"
|
[[ "${_ENV_LEMANA_VPN_USE_TOUCHID:0:1}" == "x" ]] && LEMANA_VPN_USE_TOUCHID="${_ENV_LEMANA_VPN_USE_TOUCHID:1}"
|
||||||
@@ -27,21 +30,30 @@ OC_VENV="${LEMANA_VPN_OC_VENV:-$HOME/.local/pipx/venvs/openconnect-lite}"
|
|||||||
OC_PYTHON="${LEMANA_VPN_OC_PYTHON:-$OC_VENV/bin/python}"
|
OC_PYTHON="${LEMANA_VPN_OC_PYTHON:-$OC_VENV/bin/python}"
|
||||||
OC_BIN="${LEMANA_VPN_OC_BIN:-$HOME/.local/bin/openconnect-lite}"
|
OC_BIN="${LEMANA_VPN_OC_BIN:-$HOME/.local/bin/openconnect-lite}"
|
||||||
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||||
KC_USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
KC_USERNAME="${LEMANA_VPN_USERNAME:-}"
|
||||||
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
|
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
|
||||||
|
AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE:-manual}"
|
||||||
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
|
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
|
||||||
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
|
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
|
||||||
if [[ "${LEMANA_VPN_USE_BITWARDEN:-1}" == "1" ]]; then
|
if [[ "${LEMANA_VPN_USE_BITWARDEN:-0}" == "1" ]]; then
|
||||||
CREDENTIAL_SOURCE="bitwarden"
|
CREDENTIAL_SOURCE="bitwarden"
|
||||||
else
|
else
|
||||||
CREDENTIAL_SOURCE="keychain"
|
CREDENTIAL_SOURCE="none"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
case "$CREDENTIAL_SOURCE" in
|
case "$AUTH_PROFILE" in
|
||||||
bitwarden|keychain) ;;
|
manual|saved-login|saved-totp|auto) ;;
|
||||||
*)
|
*)
|
||||||
printf 'Unknown credential source: %s. Use bitwarden or keychain.\n' "$CREDENTIAL_SOURCE" >&2
|
printf 'Unknown auth profile: %s. Use manual, saved-login, saved-totp, or auto.\n' "$AUTH_PROFILE" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
none|bitwarden|keychain) ;;
|
||||||
|
*)
|
||||||
|
printf 'Unknown credential source: %s. Use none, bitwarden, or keychain.\n' "$CREDENTIAL_SOURCE" >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -51,8 +63,8 @@ if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
|||||||
else
|
else
|
||||||
USE_BITWARDEN="0"
|
USE_BITWARDEN="0"
|
||||||
fi
|
fi
|
||||||
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-1}"
|
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-0}"
|
||||||
if [[ "$CREDENTIAL_SOURCE" == "keychain" ]]; then
|
if [[ "$CREDENTIAL_SOURCE" != "bitwarden" ]]; then
|
||||||
USE_TOUCHID="0"
|
USE_TOUCHID="0"
|
||||||
fi
|
fi
|
||||||
CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}"
|
CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}"
|
||||||
@@ -68,38 +80,45 @@ PATCH_BACKUP_DIR="${LEMANA_VPN_PATCH_BACKUP_DIR:-$CONFIG_DIR/patch-backups}"
|
|||||||
CONNECT_LOG_DIR="${LEMANA_VPN_LOG_DIR:-$HOME/Library/Logs}"
|
CONNECT_LOG_DIR="${LEMANA_VPN_LOG_DIR:-$HOME/Library/Logs}"
|
||||||
CONNECT_LOG_FILE="${LEMANA_VPN_CONNECT_LOG:-$CONNECT_LOG_DIR/LemanaVPN-openconnect-lite.log}"
|
CONNECT_LOG_FILE="${LEMANA_VPN_CONNECT_LOG:-$CONNECT_LOG_DIR/LemanaVPN-openconnect-lite.log}"
|
||||||
CONNECT_WAIT_SECONDS="${LEMANA_VPN_CONNECT_WAIT_SECONDS:-20}"
|
CONNECT_WAIT_SECONDS="${LEMANA_VPN_CONNECT_WAIT_SECONDS:-20}"
|
||||||
|
OC_RUNTIME_XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||||
|
MANUAL_XDG_CONFIG_HOME=""
|
||||||
|
|
||||||
DEBUG=false
|
DEBUG=false
|
||||||
JSON_MODE=false
|
JSON_MODE=false
|
||||||
STATUS_MODE=false
|
STATUS_MODE=false
|
||||||
|
CONFIGURE_LOGIN_MODE=false
|
||||||
CONFIGURE_KEYCHAIN_MODE=false
|
CONFIGURE_KEYCHAIN_MODE=false
|
||||||
PATCH_ONLY_MODE=false
|
PATCH_ONLY_MODE=false
|
||||||
CONNECT_MODE="${LEMANA_VPN_MODE:-auto}"
|
CONNECT_MODE="${LEMANA_VPN_MODE:-}"
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--debug) DEBUG=true ;;
|
--debug) DEBUG=true ;;
|
||||||
--json) JSON_MODE=true ;;
|
--json) JSON_MODE=true ;;
|
||||||
--status) STATUS_MODE=true ;;
|
--status) STATUS_MODE=true ;;
|
||||||
|
--configure-login) CONFIGURE_LOGIN_MODE=true ;;
|
||||||
--configure-keychain) CONFIGURE_KEYCHAIN_MODE=true ;;
|
--configure-keychain) CONFIGURE_KEYCHAIN_MODE=true ;;
|
||||||
--patch-only) PATCH_ONLY_MODE=true ;;
|
--patch-only) PATCH_ONLY_MODE=true ;;
|
||||||
--auto|auto) CONNECT_MODE="auto" ;;
|
--auto|auto) AUTH_PROFILE="auto"; CONNECT_MODE="auto" ;;
|
||||||
--manual|manual|--manual-sso) CONNECT_MODE="manual" ;;
|
--manual|manual|--manual-sso) AUTH_PROFILE="saved-totp"; CONNECT_MODE="saved-totp" ;;
|
||||||
--manual-full|manual-full|--manual-no-autofill) CONNECT_MODE="manual-full" ;;
|
--saved-login|saved-login|--login-only) AUTH_PROFILE="saved-login"; CONNECT_MODE="saved-login" ;;
|
||||||
|
--manual-full|manual-full|--manual-no-autofill) AUTH_PROFILE="manual"; CONNECT_MODE="manual-full" ;;
|
||||||
--help|-h)
|
--help|-h)
|
||||||
cat <<'HELP'
|
cat <<'HELP'
|
||||||
Usage: vpn-lemanapro.sh [--auto|--manual|--manual-full] [--debug] [--json] [--status] [--configure-keychain] [--patch-only]
|
Usage: vpn-lemanapro.sh [--auto|--manual|--saved-login|--manual-full] [--debug] [--json] [--status] [--configure-login] [--configure-keychain] [--patch-only]
|
||||||
|
|
||||||
--status Show current VPN status without connecting
|
--status Show current VPN status without connecting
|
||||||
--status --json Show current VPN status as JSON
|
--status --json Show current VPN status as JSON
|
||||||
--auto Hidden browser, auto-fill and auto-submit (default)
|
--auto Hidden browser, auto-fill and auto-submit
|
||||||
--manual Visible browser, auto-fill fields, do not press submit
|
--manual Visible browser, saved fields auto-filled, do not press submit
|
||||||
--manual-full Visible browser, no auto-fill, do not press submit
|
--saved-login Visible browser, saved username/password only, OTP manual
|
||||||
|
--manual-full Visible browser, no auto-fill, do not press submit (default on fresh install)
|
||||||
--manual-no-autofill Compatibility alias for --manual-full
|
--manual-no-autofill Compatibility alias for --manual-full
|
||||||
--manual-sso Compatibility alias for --manual
|
--manual-sso Compatibility alias for --manual
|
||||||
--debug Passthrough debug logs; also shows browser in auto mode
|
--debug Passthrough debug logs; also shows browser in auto mode
|
||||||
--json Emit JSON Lines events for UI wrappers
|
--json Emit JSON Lines events for UI wrappers
|
||||||
--configure-keychain Configure the keychain credential source: LDAP password plus permanent TOTP seed or otpauth:// URI
|
--configure-login Save LDAP username/password in macOS Keychain; OTP remains manual
|
||||||
|
--configure-keychain Save LDAP password plus permanent 2FA setup code or otpauth:// URI
|
||||||
--patch-only Apply openconnect-lite runtime patches and exit
|
--patch-only Apply openconnect-lite runtime patches and exit
|
||||||
HELP
|
HELP
|
||||||
exit 0
|
exit 0
|
||||||
@@ -108,9 +127,23 @@ HELP
|
|||||||
done
|
done
|
||||||
|
|
||||||
case "$CONNECT_MODE" in
|
case "$CONNECT_MODE" in
|
||||||
auto|manual|manual-full) ;;
|
"")
|
||||||
|
;;
|
||||||
|
manual-full)
|
||||||
|
AUTH_PROFILE="manual"
|
||||||
|
;;
|
||||||
|
manual)
|
||||||
|
AUTH_PROFILE="saved-totp"
|
||||||
|
;;
|
||||||
|
saved-login|saved-totp|auto)
|
||||||
|
AUTH_PROFILE="$CONNECT_MODE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$AUTH_PROFILE" in
|
||||||
|
manual|saved-login|saved-totp|auto) ;;
|
||||||
*)
|
*)
|
||||||
printf 'Unknown VPN mode: %s. Use --auto, --manual, or --manual-full.\n' "$CONNECT_MODE" >&2
|
printf 'Unknown auth profile: %s. Use manual, saved-login, saved-totp, or auto.\n' "$AUTH_PROFILE" >&2
|
||||||
exit 2
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -133,11 +166,56 @@ _clear_status() {
|
|||||||
_write_status "{\"pid\":$$,\"state\":\"disconnected\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
_write_status "{\"pid\":$$,\"state\":\"disconnected\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_cleanup_manual_openconnect_config() {
|
||||||
|
if [[ -n "${MANUAL_XDG_CONFIG_HOME:-}" && -d "$MANUAL_XDG_CONFIG_HOME" ]]; then
|
||||||
|
rm -rf "$MANUAL_XDG_CONFIG_HOME"
|
||||||
|
MANUAL_XDG_CONFIG_HOME=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
_prepare_manual_openconnect_config() {
|
||||||
|
local source_config temp_root temp_config_dir
|
||||||
|
source_config="$OC_CONFIG_DIR/config.toml"
|
||||||
|
temp_root="$(mktemp -d "${TMPDIR:-/tmp}/lemana-vpn-manual-xdg.XXXXXX")"
|
||||||
|
temp_config_dir="$temp_root/openconnect-lite"
|
||||||
|
mkdir -p "$temp_config_dir"
|
||||||
|
|
||||||
|
if [[ -f "$source_config" ]]; then
|
||||||
|
CONFIG_SRC="$source_config" CONFIG_DST="$temp_config_dir/config.toml" python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
src = Path(os.environ["CONFIG_SRC"])
|
||||||
|
dst = Path(os.environ["CONFIG_DST"])
|
||||||
|
|
||||||
|
lines = src.read_text().splitlines()
|
||||||
|
rendered = []
|
||||||
|
skip_credentials = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped == "[credentials]" or stripped.startswith("[credentials."):
|
||||||
|
skip_credentials = True
|
||||||
|
continue
|
||||||
|
if skip_credentials and stripped.startswith("[") and stripped.endswith("]"):
|
||||||
|
skip_credentials = False
|
||||||
|
if not skip_credentials:
|
||||||
|
rendered.append(line)
|
||||||
|
|
||||||
|
dst.write_text("\n".join(rendered).rstrip() + "\n")
|
||||||
|
dst.chmod(0o600)
|
||||||
|
PY
|
||||||
|
fi
|
||||||
|
|
||||||
|
MANUAL_XDG_CONFIG_HOME="$temp_root"
|
||||||
|
OC_RUNTIME_XDG_CONFIG_HOME="$temp_root"
|
||||||
|
}
|
||||||
|
|
||||||
_prepare_connection_log() {
|
_prepare_connection_log() {
|
||||||
mkdir -p "$CONNECT_LOG_DIR"
|
mkdir -p "$CONNECT_LOG_DIR"
|
||||||
{
|
{
|
||||||
printf '\n==== Lemana VPN openconnect-lite session %s ====\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
printf '\n==== Lemana VPN openconnect-lite session %s ====\n' "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
printf 'Command: %s --browser-display-mode hidden\n' "$OC_BIN"
|
printf 'Command: %s\n' "$OC_BIN"
|
||||||
} >> "$CONNECT_LOG_FILE"
|
} >> "$CONNECT_LOG_FILE"
|
||||||
chmod 600 "$CONNECT_LOG_FILE" 2>/dev/null || true
|
chmod 600 "$CONNECT_LOG_FILE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
@@ -221,11 +299,65 @@ _keychain_has() {
|
|||||||
security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1
|
security find-generic-password -s "$1" -a "$2" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_app_menu_json() {
|
||||||
|
local login_ready="$1" keychain_ready="$2"
|
||||||
|
local install_label configured_mode available_modes setup_hint_json
|
||||||
|
|
||||||
|
configured_mode="$AUTH_PROFILE"
|
||||||
|
setup_hint_json="null"
|
||||||
|
|
||||||
|
case "$AUTH_PROFILE" in
|
||||||
|
manual)
|
||||||
|
install_label="Режим: полностью вручную"
|
||||||
|
available_modes='"configured"'
|
||||||
|
;;
|
||||||
|
saved-login)
|
||||||
|
if [[ "$login_ready" == "true" ]]; then
|
||||||
|
install_label="Режим: логин и пароль сохранены"
|
||||||
|
available_modes='"configured","manual-full"'
|
||||||
|
else
|
||||||
|
install_label="Режим: логин и пароль не настроены"
|
||||||
|
available_modes='"manual-full"'
|
||||||
|
setup_hint_json='"Настройте в Terminal: vpn --configure-login"'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
saved-totp)
|
||||||
|
if [[ "$keychain_ready" == "true" ]]; then
|
||||||
|
install_label="Режим: сохранённые данные без автоотправки"
|
||||||
|
available_modes='"configured","saved-login","manual-full","auto"'
|
||||||
|
else
|
||||||
|
install_label="Режим: сохранённые данные не настроены"
|
||||||
|
available_modes='"manual-full"'
|
||||||
|
setup_hint_json='"Настройте в Terminal: vpn --configure-keychain"'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
auto)
|
||||||
|
if [[ "$keychain_ready" == "true" ]]; then
|
||||||
|
install_label="Режим: автоматический вход"
|
||||||
|
available_modes='"configured","saved-totp","saved-login","manual-full"'
|
||||||
|
else
|
||||||
|
install_label="Режим: автоматический вход не настроен"
|
||||||
|
available_modes='"manual-full"'
|
||||||
|
setup_hint_json='"Настройте в Terminal: vpn --configure-keychain"'
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
install_label="Режим: неизвестен"
|
||||||
|
configured_mode="manual"
|
||||||
|
available_modes='"configured"'
|
||||||
|
setup_hint_json='"Обновите Lemana VPN через install.sh"'
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf '{"install_label":"%s","configured_mode":"%s","available_modes":[%s],"setup_hint":%s}' \
|
||||||
|
"$install_label" "$configured_mode" "$available_modes" "$setup_hint_json"
|
||||||
|
}
|
||||||
|
|
||||||
_module_status_json() {
|
_module_status_json() {
|
||||||
local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed
|
local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed
|
||||||
local app_installed app_autostart
|
local app_installed app_autostart
|
||||||
local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed
|
local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed
|
||||||
local credential_keychain_ready
|
local credential_keychain_ready credential_login_ready app_menu_json
|
||||||
openconnect_installed="$(_module_bool command -v openconnect)"
|
openconnect_installed="$(_module_bool command -v openconnect)"
|
||||||
openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")"
|
openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")"
|
||||||
bitwarden_installed="$(_module_bool command -v bw)"
|
bitwarden_installed="$(_module_bool command -v bw)"
|
||||||
@@ -240,14 +372,19 @@ _module_status_json() {
|
|||||||
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
|
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
|
||||||
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
|
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
|
||||||
credential_keychain_ready="$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf true || printf false)"
|
credential_keychain_ready="$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf true || printf false)"
|
||||||
|
credential_login_ready="$([[ "$keychain_password" == "true" ]] && printf true || printf false)"
|
||||||
|
app_menu_json="$(_app_menu_json "$credential_login_ready" "$credential_keychain_ready")"
|
||||||
|
|
||||||
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","keychain_ready":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","auth_profile":"%s","login_ready":%s,"keychain_ready":%s},"app_menu":%s,"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
||||||
"$openconnect_installed" \
|
"$openconnect_installed" \
|
||||||
"$openconnect_lite_installed" \
|
"$openconnect_lite_installed" \
|
||||||
"$config_present" \
|
"$config_present" \
|
||||||
"$oc_config_present" \
|
"$oc_config_present" \
|
||||||
"$CREDENTIAL_SOURCE" \
|
"$CREDENTIAL_SOURCE" \
|
||||||
|
"$AUTH_PROFILE" \
|
||||||
|
"$credential_login_ready" \
|
||||||
"$credential_keychain_ready" \
|
"$credential_keychain_ready" \
|
||||||
|
"$app_menu_json" \
|
||||||
"$([[ "$USE_BITWARDEN" == "1" ]] && printf true || printf false)" \
|
"$([[ "$USE_BITWARDEN" == "1" ]] && printf true || printf false)" \
|
||||||
"$bitwarden_installed" \
|
"$bitwarden_installed" \
|
||||||
"$BW_ITEM_NAME" \
|
"$BW_ITEM_NAME" \
|
||||||
@@ -291,7 +428,7 @@ _module_status_human() {
|
|||||||
app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
||||||
app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")"
|
app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")"
|
||||||
|
|
||||||
printf 'Modules: %s %s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$CREDENTIAL_SOURCE"
|
printf 'Modules: %s %s, 🔐 auth_profile=%s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$AUTH_PROFILE" "$CREDENTIAL_SOURCE"
|
||||||
_module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed"
|
_module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed"
|
||||||
printf ', '
|
printf ', '
|
||||||
_module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed"
|
_module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed"
|
||||||
@@ -304,7 +441,7 @@ _module_status_human() {
|
|||||||
"$([[ "$app_autostart" == "true" ]] && printf on || printf off)" \
|
"$([[ "$app_autostart" == "true" ]] && printf on || printf off)" \
|
||||||
"$([[ "$patches_active" == "true" ]] && printf '✅' || printf '⚠️')" \
|
"$([[ "$patches_active" == "true" ]] && printf '✅' || printf '⚠️')" \
|
||||||
"$([[ "$patches_active" == "true" ]] && printf active || printf pending)" \
|
"$([[ "$patches_active" == "true" ]] && printf active || printf pending)" \
|
||||||
"$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf '✅' || printf '⚠️')" \
|
"$([[ "$AUTH_PROFILE" == "manual" || "$keychain_password" == "true" && "$AUTH_PROFILE" == "saved-login" || "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf '✅' || printf '⚠️')" \
|
||||||
"$([[ "$keychain_password" == "true" ]] && printf yes || printf no)" \
|
"$([[ "$keychain_password" == "true" ]] && printf yes || printf no)" \
|
||||||
"$([[ "$keychain_totp_seed" == "true" ]] && printf yes || printf no)"
|
"$([[ "$keychain_totp_seed" == "true" ]] && printf yes || printf no)"
|
||||||
}
|
}
|
||||||
@@ -525,6 +662,8 @@ canonical_selectors = '''def get_selectors(rules, credentials):
|
|||||||
f"""var elem = document.querySelector({selector}); if (elem) {{ return; }}"""
|
f"""var elem = document.querySelector({selector}); if (elem) {{ return; }}"""
|
||||||
)
|
)
|
||||||
elif rule.fill:
|
elif rule.fill:
|
||||||
|
if rule.fill == "totp" and os.environ.get("LEMANA_VPN_AUTOFILL_TOTP", "1") == "0":
|
||||||
|
continue
|
||||||
value = json.dumps(getattr(credentials, rule.fill, None))
|
value = json.dumps(getattr(credentials, rule.fill, None))
|
||||||
if value:
|
if value:
|
||||||
statements.append(
|
statements.append(
|
||||||
@@ -640,7 +779,7 @@ if not value:
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if not re.fullmatch(r"[A-Z2-7]+=*", value):
|
if not re.fullmatch(r"[A-Z2-7]+=*", value):
|
||||||
print("Invalid TOTP seed. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
|
print("Invalid 2FA setup secret. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(value)
|
print(value)
|
||||||
@@ -648,7 +787,99 @@ PY
|
|||||||
}
|
}
|
||||||
|
|
||||||
_can_prompt() {
|
_can_prompt() {
|
||||||
[[ -t 0 ]]
|
[[ -r /dev/tty && -w /dev/tty ]] || [[ -t 0 ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
_prompt_read() {
|
||||||
|
local var_name="$1" prompt="$2" secret="${3:-0}" value
|
||||||
|
if [[ -r /dev/tty && -w /dev/tty ]]; then
|
||||||
|
if [[ "$secret" == "1" ]]; then
|
||||||
|
IFS= read -rsp "$prompt" value < /dev/tty || value=""
|
||||||
|
printf '\n' > /dev/tty
|
||||||
|
else
|
||||||
|
IFS= read -rp "$prompt" value < /dev/tty || value=""
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ "$secret" == "1" ]]; then
|
||||||
|
IFS= read -rsp "$prompt" value || value=""
|
||||||
|
printf '\n'
|
||||||
|
else
|
||||||
|
IFS= read -rp "$prompt" value || value=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
printf -v "$var_name" '%s' "$value"
|
||||||
|
}
|
||||||
|
|
||||||
|
_persist_username() {
|
||||||
|
KC_USERNAME="$KC_USERNAME" CONFIG_FILE="$CONFIG_FILE" OC_CONFIG_FILE="$OC_CONFIG_DIR/config.toml" python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
username = os.environ["KC_USERNAME"]
|
||||||
|
config_file = Path(os.environ["CONFIG_FILE"])
|
||||||
|
oc_config_file = Path(os.environ["OC_CONFIG_FILE"])
|
||||||
|
|
||||||
|
|
||||||
|
def shell_quote(value: str) -> str:
|
||||||
|
return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("`", "\\`") + '"'
|
||||||
|
|
||||||
|
|
||||||
|
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
lines = []
|
||||||
|
if config_file.exists():
|
||||||
|
lines = config_file.read_text().splitlines()
|
||||||
|
|
||||||
|
username_line = "LEMANA_VPN_USERNAME=" + shell_quote(username)
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if line.startswith("LEMANA_VPN_USERNAME="):
|
||||||
|
lines[index] = username_line
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
lines.insert(0, username_line)
|
||||||
|
|
||||||
|
config_file.write_text("\n".join(lines).rstrip() + "\n")
|
||||||
|
config_file.chmod(0o600)
|
||||||
|
|
||||||
|
if oc_config_file.exists():
|
||||||
|
oc_lines = oc_config_file.read_text().splitlines()
|
||||||
|
rendered = "username = " + json.dumps(username)
|
||||||
|
for index, line in enumerate(oc_lines):
|
||||||
|
if line.strip().startswith("username"):
|
||||||
|
oc_lines[index] = rendered
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
oc_lines.insert(0, rendered)
|
||||||
|
oc_config_file.write_text("\n".join(oc_lines).rstrip() + "\n")
|
||||||
|
oc_config_file.chmod(0o600)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensure_username() {
|
||||||
|
if [[ -n "${KC_USERNAME:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_emit '{"event":"username_required"}' "LDAP username is missing. Enter it once to save it for future VPN runs."
|
||||||
|
|
||||||
|
if ! _can_prompt; then
|
||||||
|
_emit '{"event":"error","message":"LDAP username is missing. Run vpn in Terminal once, or reinstall with --username <LDAP_USERNAME>."}' \
|
||||||
|
"LDAP username is missing. Run in Terminal: vpn"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [[ -z "${KC_USERNAME:-}" ]]; do
|
||||||
|
_prompt_read KC_USERNAME "Corporate LDAP username: "
|
||||||
|
KC_USERNAME="${KC_USERNAME#"${KC_USERNAME%%[![:space:]]*}"}"
|
||||||
|
KC_USERNAME="${KC_USERNAME%"${KC_USERNAME##*[![:space:]]}"}"
|
||||||
|
if [[ -z "$KC_USERNAME" ]]; then
|
||||||
|
printf 'LDAP username is required.\n' >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
export LEMANA_VPN_USERNAME="$KC_USERNAME"
|
||||||
|
_persist_username
|
||||||
|
_emit '{"event":"username_saved"}' "LDAP username saved for future VPN runs."
|
||||||
}
|
}
|
||||||
|
|
||||||
_configure_keychain() {
|
_configure_keychain() {
|
||||||
@@ -660,28 +891,26 @@ _configure_keychain() {
|
|||||||
printf 'Manual LDAP credentials setup for Lemana VPN\n'
|
printf 'Manual LDAP credentials setup for Lemana VPN\n'
|
||||||
printf 'User: %s\n' "$KC_USERNAME"
|
printf 'User: %s\n' "$KC_USERNAME"
|
||||||
printf 'LDAP password: your corporate LDAP/domain password, not the Bitwarden master password.\n'
|
printf 'LDAP password: your corporate LDAP/domain password, not the Bitwarden master password.\n'
|
||||||
printf 'TOTP seed: permanent BASE32 secret from 2FA setup, not the current 6-digit code.\n'
|
printf '2FA setup secret: permanent BASE32 secret from authenticator setup, not the current 6-digit code.\n'
|
||||||
printf 'Saved values go to macOS Keychain service openconnect-lite.\n\n'
|
printf 'Saved values go to macOS Keychain service openconnect-lite.\n\n'
|
||||||
|
|
||||||
if $password_present; then
|
if $password_present; then
|
||||||
read -rsp "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " password
|
_prompt_read password "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " 1
|
||||||
else
|
else
|
||||||
read -rsp "Corporate LDAP password for $KC_USERNAME: " password
|
_prompt_read password "Corporate LDAP password for $KC_USERNAME: " 1
|
||||||
fi
|
fi
|
||||||
printf '\n'
|
|
||||||
if $totp_present; then
|
if $totp_present; then
|
||||||
read -rsp "TOTP seed BASE32 [leave empty to keep saved seed]: " totp_secret
|
_prompt_read totp_secret "2FA setup secret BASE32 [leave empty to keep saved secret]: " 1
|
||||||
else
|
else
|
||||||
read -rsp "TOTP seed BASE32 from 2FA setup: " totp_secret
|
_prompt_read totp_secret "2FA setup secret BASE32: " 1
|
||||||
fi
|
fi
|
||||||
printf '\n'
|
|
||||||
|
|
||||||
if [[ -z "$password" && "$password_present" != "true" ]]; then
|
if [[ -z "$password" && "$password_present" != "true" ]]; then
|
||||||
printf 'LDAP password is required because no saved password was found.\n' >&2
|
printf 'LDAP password is required because no saved password was found.\n' >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if [[ -z "$totp_secret" && "$totp_present" != "true" ]]; then
|
if [[ -z "$totp_secret" && "$totp_present" != "true" ]]; then
|
||||||
printf 'TOTP seed is required because no saved seed was found. Use the BASE32 setup secret, not the current 6-digit code.\n' >&2
|
printf '2FA setup secret is required because no saved secret was found. Use the BASE32 setup secret, not the current 6-digit code.\n' >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -693,6 +922,53 @@ _configure_keychain() {
|
|||||||
printf 'Credentials are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
printf 'Credentials are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_configure_login() {
|
||||||
|
local password password_present=false
|
||||||
|
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
||||||
|
|
||||||
|
printf 'Manual LDAP login setup for Lemana VPN\n'
|
||||||
|
printf 'User: %s\n' "$KC_USERNAME"
|
||||||
|
printf 'LDAP password: your corporate LDAP/domain password, not the Bitwarden master password.\n'
|
||||||
|
printf 'Saved values go to macOS Keychain service openconnect-lite.\n'
|
||||||
|
printf 'One-time 2FA codes will still be entered manually during VPN login.\n\n'
|
||||||
|
|
||||||
|
if $password_present; then
|
||||||
|
_prompt_read password "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " 1
|
||||||
|
else
|
||||||
|
_prompt_read password "Corporate LDAP password for $KC_USERNAME: " 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$password" && "$password_present" != "true" ]]; then
|
||||||
|
printf 'LDAP password is required because no saved password was found.\n' >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_store_keychain "$password" ""
|
||||||
|
printf 'Login and password are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensure_keychain_login() {
|
||||||
|
local password_present=false
|
||||||
|
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
||||||
|
|
||||||
|
if [[ "$password_present" == "true" ]]; then
|
||||||
|
_emit '{"event":"keychain_ready","source":"keychain","scope":"login"}' \
|
||||||
|
"Keychain login is ready: saved LDAP password is available for $KC_USERNAME."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_emit '{"event":"keychain_required","source":"keychain","scope":"login"}' \
|
||||||
|
"Saved LDAP password is missing."
|
||||||
|
|
||||||
|
if ! _can_prompt; then
|
||||||
|
_emit '{"event":"error","message":"Saved LDAP password is missing. Run vpn --configure-login in Terminal, or connect fully manually."}' \
|
||||||
|
"Saved LDAP password is missing. Run: vpn --configure-login"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
_configure_login
|
||||||
|
}
|
||||||
|
|
||||||
_ensure_keychain_credentials() {
|
_ensure_keychain_credentials() {
|
||||||
local password_present=false totp_present=false
|
local password_present=false totp_present=false
|
||||||
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
_keychain_has openconnect-lite "$KC_USERNAME" && password_present=true
|
||||||
@@ -702,7 +978,7 @@ _ensure_keychain_credentials() {
|
|||||||
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
_emit '{"event":"keychain_ready","source":"bitwarden"}' "Bitwarden source synced LDAP credentials into macOS Keychain for $KC_USERNAME."
|
_emit '{"event":"keychain_ready","source":"bitwarden"}' "Bitwarden source synced LDAP credentials into macOS Keychain for $KC_USERNAME."
|
||||||
else
|
else
|
||||||
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and TOTP seed are available for $KC_USERNAME."
|
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and 2FA setup secret are available for $KC_USERNAME."
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -714,8 +990,8 @@ _ensure_keychain_credentials() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if ! _can_prompt; then
|
if ! _can_prompt; then
|
||||||
_emit '{"event":"error","message":"LDAP credentials are missing. Run vpn --configure-keychain in Terminal, or reinstall with --configure-keychain."}' \
|
_emit '{"event":"error","message":"Automatic sign-in is not configured. Run vpn --configure-keychain in Terminal, or use vpn --manual-full."}' \
|
||||||
"LDAP credentials are missing. Run: vpn --configure-keychain"
|
"Automatic sign-in is not configured. Run: vpn --configure-keychain"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -769,8 +1045,7 @@ _bw_unlock() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
_emit '{"event":"bw_manual"}' "Bitwarden vault is locked. Enter Bitwarden master password to sync LDAP credentials."
|
_emit '{"event":"bw_manual"}' "Bitwarden vault is locked. Enter Bitwarden master password to sync LDAP credentials."
|
||||||
read -rsp "Bitwarden master password (not LDAP password): " manual_pw
|
_prompt_read manual_pw "Bitwarden master password (not LDAP password): " 1
|
||||||
printf '\n'
|
|
||||||
if [[ -z "$manual_pw" ]]; then
|
if [[ -z "$manual_pw" ]]; then
|
||||||
printf 'Empty Bitwarden password. Using existing Keychain credentials.\n' >&2
|
printf 'Empty Bitwarden password. Using existing Keychain credentials.\n' >&2
|
||||||
return 1
|
return 1
|
||||||
@@ -786,7 +1061,7 @@ _bw_unlock() {
|
|||||||
|
|
||||||
if [[ "$USE_TOUCHID" == "1" && -x "$KC_FP" ]]; then
|
if [[ "$USE_TOUCHID" == "1" && -x "$KC_FP" ]]; then
|
||||||
local save_choice
|
local save_choice
|
||||||
read -rp "Save Bitwarden master password behind Touch ID for next VPN unlock? [Y/n] " save_choice
|
_prompt_read save_choice "Save Bitwarden master password behind Touch ID for next VPN unlock? [Y/n] "
|
||||||
if [[ "${save_choice:-y}" =~ ^[Yy]?$ ]]; then
|
if [[ "${save_choice:-y}" =~ ^[Yy]?$ ]]; then
|
||||||
printf '%s' "$manual_pw" | "$KC_FP" set "$BW_KC_SERVICE" "$BW_KC_ACCOUNT_MASTER" >/dev/null 2>&1 \
|
printf '%s' "$manual_pw" | "$KC_FP" set "$BW_KC_SERVICE" "$BW_KC_ACCOUNT_MASTER" >/dev/null 2>&1 \
|
||||||
&& printf 'Saved. Next unlock can use Touch ID.\n' \
|
&& printf 'Saved. Next unlock can use Touch ID.\n' \
|
||||||
@@ -836,6 +1111,9 @@ except Exception:
|
|||||||
|
|
||||||
_sync_credentials() {
|
_sync_credentials() {
|
||||||
case "$CREDENTIAL_SOURCE" in
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
none)
|
||||||
|
_emit '{"event":"credential_source","source":"none"}' "Credential source: none"
|
||||||
|
;;
|
||||||
bitwarden)
|
bitwarden)
|
||||||
_sync_bitwarden
|
_sync_bitwarden
|
||||||
;;
|
;;
|
||||||
@@ -920,7 +1198,14 @@ if $STATUS_MODE; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if $CONFIGURE_LOGIN_MODE; then
|
||||||
|
_ensure_username
|
||||||
|
_configure_login
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if $CONFIGURE_KEYCHAIN_MODE; then
|
if $CONFIGURE_KEYCHAIN_MODE; then
|
||||||
|
_ensure_username
|
||||||
_configure_keychain
|
_configure_keychain
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -936,32 +1221,64 @@ else
|
|||||||
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
|
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_sync_credentials
|
case "$AUTH_PROFILE" in
|
||||||
_ensure_keychain_credentials
|
manual)
|
||||||
|
_emit '{"event":"auth_profile","profile":"manual","message":"Manual mode skips saved credential checks"}' \
|
||||||
|
"Manual mode: open the visible browser and enter login, password, and one-time code manually."
|
||||||
|
;;
|
||||||
|
saved-login)
|
||||||
|
_ensure_username
|
||||||
|
_sync_credentials
|
||||||
|
_ensure_keychain_login
|
||||||
|
;;
|
||||||
|
saved-totp|auto)
|
||||||
|
_ensure_username
|
||||||
|
_sync_credentials
|
||||||
|
_ensure_keychain_credentials
|
||||||
|
;;
|
||||||
|
esac
|
||||||
_patch_oc
|
_patch_oc
|
||||||
|
|
||||||
_emit '{"event":"connecting"}' "Connecting to VPN (lemanapro)..."
|
_emit '{"event":"connecting"}' "Connecting to VPN (lemanapro)..."
|
||||||
_write_status "{\"pid\":$$,\"state\":\"connecting\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
_write_status "{\"pid\":$$,\"state\":\"connecting\",\"updated_at\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}"
|
||||||
_prepare_connection_log
|
_prepare_connection_log
|
||||||
_emit '{"event":"log","message":"openconnect-lite log is enabled"}' "openconnect-lite log: $CONNECT_LOG_FILE"
|
_emit '{"event":"log","message":"openconnect-lite log is enabled"}' "openconnect-lite log: $CONNECT_LOG_FILE"
|
||||||
trap '_stop_connect_progress; _dns_cleanup; _clear_status' EXIT
|
trap '_stop_connect_progress; _dns_cleanup; _cleanup_manual_openconnect_config; _clear_status' EXIT
|
||||||
|
|
||||||
display_mode="hidden"
|
display_mode="shown"
|
||||||
log_level=""
|
log_level=""
|
||||||
autofill_debug="${LEMANA_VPN_AUTOFILL_DEBUG:-0}"
|
autofill_debug="${LEMANA_VPN_AUTOFILL_DEBUG:-0}"
|
||||||
autofill_disable="${LEMANA_VPN_AUTOFILL_DISABLE:-0}"
|
autofill_disable="${LEMANA_VPN_AUTOFILL_DISABLE:-1}"
|
||||||
autofill_click="${LEMANA_VPN_AUTOFILL_CLICK:-1}"
|
autofill_click="${LEMANA_VPN_AUTOFILL_CLICK:-0}"
|
||||||
if [[ "$CONNECT_MODE" == "manual" ]]; then
|
autofill_totp="${LEMANA_VPN_AUTOFILL_TOTP:-0}"
|
||||||
display_mode="shown"
|
case "$AUTH_PROFILE" in
|
||||||
autofill_disable="0"
|
manual)
|
||||||
autofill_click="0"
|
|
||||||
_emit '{"event":"manual_sso","autofill":true,"submit":false}' "Manual mode: browser is visible, fields are auto-filled, submit is not pressed."
|
|
||||||
elif [[ "$CONNECT_MODE" == "manual-full" ]]; then
|
|
||||||
display_mode="shown"
|
|
||||||
autofill_disable="1"
|
autofill_disable="1"
|
||||||
autofill_click="0"
|
autofill_click="0"
|
||||||
_emit '{"event":"manual_sso","autofill":false,"submit":false}' "Full manual mode: browser is visible, auto-fill is disabled, submit is not pressed."
|
autofill_totp="0"
|
||||||
fi
|
_prepare_manual_openconnect_config
|
||||||
|
_emit '{"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false}' "Manual mode: browser is visible, auto-fill is disabled, submit is not pressed."
|
||||||
|
;;
|
||||||
|
saved-login)
|
||||||
|
autofill_disable="0"
|
||||||
|
autofill_click="0"
|
||||||
|
autofill_totp="0"
|
||||||
|
_emit '{"event":"manual_sso","auth_profile":"saved-login","autofill":true,"totp":false,"submit":false}' "Saved-login mode: browser is visible, login/password are filled, one-time code is manual."
|
||||||
|
;;
|
||||||
|
saved-totp)
|
||||||
|
autofill_disable="0"
|
||||||
|
autofill_click="0"
|
||||||
|
autofill_totp="1"
|
||||||
|
_emit '{"event":"manual_sso","auth_profile":"saved-totp","autofill":true,"totp":true,"submit":false}' "Saved-TOTP mode: browser is visible, saved fields are filled, submit is not pressed."
|
||||||
|
;;
|
||||||
|
auto)
|
||||||
|
display_mode="hidden"
|
||||||
|
autofill_disable="0"
|
||||||
|
autofill_click="1"
|
||||||
|
autofill_totp="1"
|
||||||
|
_emit '{"event":"auto_sso","auth_profile":"auto","autofill":true,"submit":true}' "Auto mode: hidden browser, auto-fill and auto-submit."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
if $DEBUG; then
|
if $DEBUG; then
|
||||||
display_mode="shown"
|
display_mode="shown"
|
||||||
log_level="--log-level debug"
|
log_level="--log-level debug"
|
||||||
@@ -972,9 +1289,11 @@ reconnect_count=0
|
|||||||
while true; do
|
while true; do
|
||||||
_start_connect_progress
|
_start_connect_progress
|
||||||
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu" \
|
QTWEBENGINE_CHROMIUM_FLAGS="--disable-gpu" \
|
||||||
|
XDG_CONFIG_HOME="$OC_RUNTIME_XDG_CONFIG_HOME" \
|
||||||
LEMANA_VPN_AUTOFILL_DEBUG="$autofill_debug" \
|
LEMANA_VPN_AUTOFILL_DEBUG="$autofill_debug" \
|
||||||
LEMANA_VPN_AUTOFILL_DISABLE="$autofill_disable" \
|
LEMANA_VPN_AUTOFILL_DISABLE="$autofill_disable" \
|
||||||
LEMANA_VPN_AUTOFILL_CLICK="$autofill_click" \
|
LEMANA_VPN_AUTOFILL_CLICK="$autofill_click" \
|
||||||
|
LEMANA_VPN_AUTOFILL_TOTP="$autofill_totp" \
|
||||||
"$OC_BIN" --browser-display-mode "$display_mode" $log_level 2>&1 \
|
"$OC_BIN" --browser-display-mode "$display_mode" $log_level 2>&1 \
|
||||||
| _filter_output
|
| _filter_output
|
||||||
exit_code=${PIPESTATUS[0]}
|
exit_code=${PIPESTATUS[0]}
|
||||||
|
|||||||
13
docs/goals/manual-first-auth-flow/GOAL.md
Normal file
13
docs/goals/manual-first-auth-flow/GOAL.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Goal: Manual-First Auth Flow
|
||||||
|
|
||||||
|
Use Krypton Execution to execute `docs/goals/manual-first-auth-flow/PLAN.md`.
|
||||||
|
|
||||||
|
Core rules:
|
||||||
|
- Treat PLAN.md as the source plan.
|
||||||
|
- Preserve intent, ownership, contract, cutover, evidence, and kill criteria.
|
||||||
|
- Make fresh install and default `vpn` / app connect manual-first.
|
||||||
|
- Keep the installer understandable for a non-IT user: plain wording, manual mode on Enter, optional conveniences explained as choices.
|
||||||
|
- Keep Bitwarden, Touch ID, saved TOTP, autosubmit, and headless mode as explicit opt-in features.
|
||||||
|
- Do not add a new dominant path without deleting, redirecting, demoting, or shimming the displaced Bitwarden/auto-first path.
|
||||||
|
- Capture acceptance evidence from a clean temporary HOME and the menu-bar app contract.
|
||||||
|
- Say "implemented but unproven" if that evidence cannot be captured.
|
||||||
440
docs/goals/manual-first-auth-flow/PLAN.md
Normal file
440
docs/goals/manual-first-auth-flow/PLAN.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# Manual-First Auth Flow Implementation Plan
|
||||||
|
|
||||||
|
**Intent:** Перевернуть Lemana VPN из auto/Bitwarden-first в manual-first: новая установка и действие по умолчанию открывают видимый SSO-браузер без сохранённых секретов, а удобства включаются отдельными функциями.
|
||||||
|
**Current Behavior:** Fresh install по умолчанию выбирает `credential_source=bitwarden`, ставит Bitwarden/Touch ID, `vpn` запускает hidden browser с autofill+submit, а `manual-full` существует как отдельный диагностический режим.
|
||||||
|
**Expected Outcome:** Базовый путь не требует Bitwarden, Keychain, TOTP seed или автоотправки. Пользователь может поэтапно включить сохранение LDAP login/password в macOS Keychain, затем TOTP seed/autofill, затем Bitwarden sync, Touch ID, autosubmit/headless.
|
||||||
|
**Target-Perspective Output:** Новый пользователь запускает installer, ничего специально не выбирает, нажимает `vpn` или основную кнопку в `LemanaVPN.app`, видит Keycloak форму и вводит LDAP, пароль и одноразовый код руками. Если он включил сохранение LDAP в Keychain, username/password подставляются, а одноразовый код он вводит руками.
|
||||||
|
**Truth Owner:** `bin/vpn-lemanapro.sh` владеет runtime-решением режима; `~/.config/lemana-vpn/env` хранит выбранные capabilities; macOS Keychain хранит только секреты; Swift app только вызывает CLI и читает JSON.
|
||||||
|
**Contract Boundary:** Installer пишет env/config, CLI читает env/flags/Keychain и отдаёт JSON Lines/status JSON, Swift app потребляет только этот CLI-контракт. `openconnect-lite` остаётся SSO/VPN engine.
|
||||||
|
**Cutover:** Fresh installs become manual-first. Existing configs with `LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden` keep working, but docs/UI stop presenting Bitwarden as default. Legacy aliases `vpn-auto`, `vpn-manual`, `vpn-manual-full` stay as compatibility shims.
|
||||||
|
**Displaced Path:** The displaced dominant path is `Bitwarden + Touch ID + hidden autofill + autosubmit by default`. It becomes an explicit opt-in feature set, not the default route.
|
||||||
|
**Value Density:** First slice removes the biggest onboarding failure: users without Bitwarden can connect manually immediately, then add only the conveniences they need.
|
||||||
|
**Acceptance Evidence:** In a clean temporary HOME, `sh install.sh --dry-run --non-interactive` reports manual/default auth with Bitwarden and Touch ID off; the interactive installer copy reads like a step-by-step setup for a non-IT user; `bash bin/vpn-lemanapro.sh --json` without credentials reaches a visible browser launch without `username_required` or `keychain_required`; a Keychain-login-only fixture fills username/password but leaves OTP manual and submit disabled; the Swift app primary action maps to the manual-first path.
|
||||||
|
**Evidence Lane:** Non-live smoke ladder first: `sh -n`, `bash -n`, `tests/smoke.sh`, `swift build`, `git diff --check`. Live VPN is optional after status check and should start with visible manual mode only.
|
||||||
|
**Kill Criteria:** No fresh-install default may select Bitwarden, require a TOTP seed, autosubmit, or hide the browser. No docs may state Bitwarden is enabled by default. No default CLI/app path may require saved credentials before opening manual SSO.
|
||||||
|
**Architecture Slice:** Installer defaults + CLI auth profile/capability model + Swift launch mode labels + README/smoke coverage.
|
||||||
|
**Plan Review Gate:** Requires PRE review before execution.
|
||||||
|
|
||||||
|
## Короткий План
|
||||||
|
|
||||||
|
🎯 Цель: fresh install, `vpn` и основная кнопка приложения стартуют с полностью ручного SSO; сохранение LDAP, TOTP, Bitwarden, autosubmit и headless включаются отдельно.
|
||||||
|
|
||||||
|
📦 Репозитории: только `/Users/dokril/git/home/lemana-vpn`.
|
||||||
|
|
||||||
|
🧩 Модули: `install.sh`, `bin/vpn-lemanapro.sh`, `app/Sources/LemanaVPN`, `tests/smoke.sh`, `README.md`; `templates/openconnect-lite-config.toml` только если login-only autofill потребует правки правил.
|
||||||
|
|
||||||
|
🛠 Основные изменения:
|
||||||
|
- Переключить fresh defaults с Bitwarden/auto-first на manual-first.
|
||||||
|
- Ввести явный auth profile/capability contract: manual, saved-login, saved-totp, auto.
|
||||||
|
- Разрешить Keychain хранить только LDAP username/password без обязательного TOTP seed.
|
||||||
|
- Оставить Bitwarden, Touch ID, autosubmit/headless как opt-in функции.
|
||||||
|
- Сделать Swift app CLI-wrapper поверх тех же режимов, с manual-first primary action.
|
||||||
|
- Зафиксировать всё в smoke-тестах и README.
|
||||||
|
|
||||||
|
✅ Проверка: `sh -n install.sh`, `sh -n uninstall.sh`, `bash -n bin/vpn-lemanapro.sh`, `sh -n tests/smoke.sh`, `tests/smoke.sh`, `swift build -c release --package-path app`, `git diff --check`.
|
||||||
|
|
||||||
|
⚠️ Риски: старые пользователи могут ожидать auto-connect на `vpn`; partial Keychain без TOTP надо проверить fake launch тестом и, при необходимости, видимым manual smoke.
|
||||||
|
|
||||||
|
➡️ Следующий шаг: начать с `install.sh` и зафиксировать новый env/flag contract для manual-first defaults.
|
||||||
|
|
||||||
|
## Architecture Slice
|
||||||
|
|
||||||
|
Files to create:
|
||||||
|
|
||||||
|
- None required for runtime. This goal package is the planning artifact.
|
||||||
|
|
||||||
|
Files to modify:
|
||||||
|
|
||||||
|
- `install.sh` - defaults, wizard wording, flags, env rendering, dry-run output.
|
||||||
|
- `bin/vpn-lemanapro.sh` - auth profile/capability model, default mode, partial Keychain support, JSON events.
|
||||||
|
- `app/Sources/LemanaVPN/VPNManager.swift` - launch mode enum/default, JSON handling if new events are added.
|
||||||
|
- `app/Sources/LemanaVPN/LemanaVPNApp.swift` - menu labels/order so manual-first is primary and auto is secondary.
|
||||||
|
- `tests/smoke.sh` - regression coverage for fresh defaults, manual launch without credentials, partial Keychain, app wiring.
|
||||||
|
- `README.md` - user-facing feature matrix and migration/update instructions.
|
||||||
|
- `templates/openconnect-lite-config.toml` only if login-only filling needs rule adjustments; otherwise avoid.
|
||||||
|
|
||||||
|
Files to avoid:
|
||||||
|
|
||||||
|
- `libexec/lemana-vpn-dns-cleanup` unless DNS behavior breaks.
|
||||||
|
- `uninstall.sh` except if new Keychain account names require cleanup.
|
||||||
|
- Direct edits inside installed `openconnect-lite`; use the existing runtime patch path from `bin/vpn-lemanapro.sh`.
|
||||||
|
|
||||||
|
Source of truth:
|
||||||
|
|
||||||
|
- Repo source for behavior.
|
||||||
|
- Installed env file for user-selected capabilities.
|
||||||
|
- Keychain for LDAP password and optional TOTP seed.
|
||||||
|
|
||||||
|
Read path:
|
||||||
|
|
||||||
|
- `install.sh` writes `~/.config/lemana-vpn/env`.
|
||||||
|
- `bin/vpn-lemanapro.sh` sources env, applies CLI flag overrides, checks Keychain capability presence, then launches `openconnect-lite`.
|
||||||
|
- `LemanaVPN.app` calls `~/bin/vpn-lemanapro.sh --json <mode>`.
|
||||||
|
|
||||||
|
Write path:
|
||||||
|
|
||||||
|
- `vpn --configure-login` or equivalent writes username/password only.
|
||||||
|
- `vpn --configure-keychain` remains the full saved-credentials path and writes password plus permanent TOTP seed.
|
||||||
|
- Bitwarden sync, when enabled, writes password plus optional TOTP seed into Keychain.
|
||||||
|
|
||||||
|
Contract boundary:
|
||||||
|
|
||||||
|
- CLI flags and JSON events are the public contract for the app and tests.
|
||||||
|
- Suggested new runtime contract:
|
||||||
|
- `auth_profile=manual`: visible browser, no preflight, no autofill, no submit.
|
||||||
|
- `auth_profile=saved-login`: visible browser, username/password autofill, OTP manual, no submit.
|
||||||
|
- `auth_profile=saved-totp`: visible browser, username/password/TOTP autofill, no submit.
|
||||||
|
- `auth_profile=auto`: hidden or visible automatic mode, full autofill and submit; explicit only.
|
||||||
|
- `credential_source=none|keychain|bitwarden`: where saved values come from, separate from whether submit/headless are enabled.
|
||||||
|
|
||||||
|
Integration points:
|
||||||
|
|
||||||
|
- macOS Keychain service `openconnect-lite`.
|
||||||
|
- Optional Bitwarden CLI item `LM LDAP`.
|
||||||
|
- `openconnect-lite` config autofill rules.
|
||||||
|
- Swift app menu and reconnect behavior.
|
||||||
|
- Existing aliases from shell install block.
|
||||||
|
|
||||||
|
Migration/cutover:
|
||||||
|
|
||||||
|
- Fresh installs write `credential_source=none` and `auth_profile=manual`.
|
||||||
|
- Existing `credential_source=bitwarden` installs continue as saved-totp/auto-capable, but `vpn` default should still prefer manual-visible unless the user explicitly chooses auto.
|
||||||
|
- `vpn-auto` remains the explicit old behavior.
|
||||||
|
- `--manual-full` remains compatibility alias for the new default manual profile.
|
||||||
|
|
||||||
|
Displaced path:
|
||||||
|
|
||||||
|
- Installer Enter=Bitwarden.
|
||||||
|
- CLI `CONNECT_MODE=auto`.
|
||||||
|
- Full password+TOTP seed as mandatory preflight for non-`manual-full`.
|
||||||
|
- App primary/default connect action pointing at `.auto`.
|
||||||
|
|
||||||
|
Acceptance evidence gate:
|
||||||
|
|
||||||
|
- A clean temp HOME proves fresh defaults.
|
||||||
|
- Captured interactive installer output proves that the default path is understandable without knowing Bitwarden, Keychain, TOTP seed, or `credential_source`.
|
||||||
|
- Fake `openconnect-lite` launch capture proves display/autofill/click env for each profile.
|
||||||
|
- App source/smoke proves primary UI wiring.
|
||||||
|
- Installed-bundle verification is required only when refreshing the local app after implementation.
|
||||||
|
|
||||||
|
## Пользовательский Процесс Установки
|
||||||
|
|
||||||
|
Цель этого сценария: человек, далёкий от IT, должен понимать, что сейчас произойдёт, что от него требуется, и что можно пропустить. Installer не должен начинать с технического выбора credential source.
|
||||||
|
|
||||||
|
### Первый Экран
|
||||||
|
|
||||||
|
Текст должен быть коротким:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Lemana VPN установит приложение для подключения к корпоративному VPN.
|
||||||
|
|
||||||
|
По умолчанию ничего не сохраняется: при подключении откроется окно входа,
|
||||||
|
а вы введёте логин, пароль и одноразовый код сами.
|
||||||
|
|
||||||
|
Дополнительные удобства можно включить сейчас или позже.
|
||||||
|
```
|
||||||
|
|
||||||
|
Основное действие по Enter: продолжить с ручным безопасным режимом.
|
||||||
|
|
||||||
|
Запрещено на первом экране:
|
||||||
|
|
||||||
|
- `credential_source`
|
||||||
|
- `auth_profile`
|
||||||
|
- `TOTP seed`
|
||||||
|
- `openconnect-lite`
|
||||||
|
- `Bitwarden item`
|
||||||
|
- `headless`
|
||||||
|
|
||||||
|
### Шаг 1: Что Будет Установлено
|
||||||
|
|
||||||
|
Installer показывает не список пакетов, а понятные пункты:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Будет установлено:
|
||||||
|
- приложение Lemana VPN в меню macOS;
|
||||||
|
- команда vpn для подключения из Terminal;
|
||||||
|
- VPN-компонент, который открывает корпоративную страницу входа;
|
||||||
|
- безопасная очистка DNS после отключения.
|
||||||
|
```
|
||||||
|
|
||||||
|
Технические зависимости (`openconnect`, `pipx`, `openconnect-lite`) можно показывать только в подробном логе ниже или в debug/dry-run.
|
||||||
|
|
||||||
|
### Шаг 2: Базовый Режим Входа
|
||||||
|
|
||||||
|
Вопрос:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Как подключаться по умолчанию?
|
||||||
|
|
||||||
|
1) Вводить всё вручную (рекомендуется)
|
||||||
|
Откроется окно входа. Вы сами введёте логин, пароль и одноразовый код.
|
||||||
|
|
||||||
|
2) Запомнить логин и пароль на этом Mac
|
||||||
|
При входе логин и пароль подставятся сами. Одноразовый код вы введёте вручную.
|
||||||
|
|
||||||
|
3) Настроить автоматический вход
|
||||||
|
Нужно заранее сохранить пароль и постоянный код настройки 2FA.
|
||||||
|
|
||||||
|
Выбор [1/2/3, Enter=1]:
|
||||||
|
```
|
||||||
|
|
||||||
|
Поведение:
|
||||||
|
|
||||||
|
- `1` ставит `auth_profile=manual`, `credential_source=none`.
|
||||||
|
- `2` ставит `auth_profile=saved-login`, `credential_source=keychain`, после установки запускает настройку LDAP username/password.
|
||||||
|
- `3` открывает следующий уточняющий шаг и не должен быть default.
|
||||||
|
|
||||||
|
### Шаг 3: Автоматический Вход Только Для Продвинутого Пути
|
||||||
|
|
||||||
|
Если выбран пункт 3, installer объясняет риск простым языком:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Автоматический вход работает только если на Mac сохранены:
|
||||||
|
- корпоративный пароль;
|
||||||
|
- постоянный код настройки 2FA.
|
||||||
|
|
||||||
|
Это не текущие 6 цифр из приложения-аутентификатора.
|
||||||
|
Если вы не знаете, что это такое, выберите ручной режим.
|
||||||
|
```
|
||||||
|
|
||||||
|
Варианты:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1) Вернуться к ручному режиму
|
||||||
|
2) Сохранить пароль и постоянный код 2FA в macOS Keychain
|
||||||
|
3) Взять пароль и код 2FA из Bitwarden
|
||||||
|
```
|
||||||
|
|
||||||
|
Поведение:
|
||||||
|
|
||||||
|
- `1` возвращает к `auth_profile=manual`.
|
||||||
|
- `2` ставит `auth_profile=saved-totp`, `credential_source=keychain`, запускает full Keychain setup.
|
||||||
|
- `3` ставит `credential_source=bitwarden`; Touch ID спрашивается отдельным простым вопросом.
|
||||||
|
|
||||||
|
### Шаг 4: Bitwarden Как Дополнительная Функция
|
||||||
|
|
||||||
|
Bitwarden не должен выглядеть обязательным. Вопрос появляется только после явного выбора Bitwarden:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Использовать Bitwarden для хранения VPN-пароля?
|
||||||
|
|
||||||
|
Подойдёт, если у вас уже есть запись с корпоративным VPN-паролем
|
||||||
|
и постоянным кодом настройки 2FA.
|
||||||
|
```
|
||||||
|
|
||||||
|
Если Bitwarden CLI не найден:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Bitwarden не найден на этом Mac.
|
||||||
|
Можно установить его сейчас или вернуться к ручному режиму.
|
||||||
|
```
|
||||||
|
|
||||||
|
Default: вернуться к ручному режиму, если пользователь не уверен.
|
||||||
|
|
||||||
|
### Шаг 5: Touch ID Только После Bitwarden
|
||||||
|
|
||||||
|
Вопрос:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Разрешить Touch ID для открытия Bitwarden?
|
||||||
|
|
||||||
|
Это не VPN-пароль. Touch ID только помогает достать данные из Bitwarden.
|
||||||
|
```
|
||||||
|
|
||||||
|
Default: нет, если пользователь не выбирал Bitwarden; да/нет можно спрашивать только в Bitwarden path.
|
||||||
|
|
||||||
|
### Шаг 6: Понятный Прогресс Установки
|
||||||
|
|
||||||
|
Progress log должен говорить человеческим языком:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Проверяю компоненты для VPN...
|
||||||
|
Устанавливаю приложение Lemana VPN...
|
||||||
|
Добавляю команду vpn...
|
||||||
|
Настраиваю безопасное отключение VPN...
|
||||||
|
Готово.
|
||||||
|
```
|
||||||
|
|
||||||
|
Если macOS просит пароль администратора, installer заранее объясняет:
|
||||||
|
|
||||||
|
```text
|
||||||
|
macOS может запросить пароль от компьютера.
|
||||||
|
Он нужен, чтобы разрешить VPN подключаться и корректно очищать DNS после отключения.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Финальный Экран
|
||||||
|
|
||||||
|
Для ручного режима:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Готово. Как подключиться:
|
||||||
|
1. Откройте Lemana VPN из Applications или меню macOS.
|
||||||
|
2. Нажмите "Подключить".
|
||||||
|
3. В открывшемся окне введите корпоративный логин, пароль и одноразовый код.
|
||||||
|
|
||||||
|
Проверить состояние: vpn --status
|
||||||
|
```
|
||||||
|
|
||||||
|
Для режима с сохранённым логином и паролем:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Готово. При подключении логин и пароль будут подставлены автоматически.
|
||||||
|
Одноразовый код нужно ввести вручную.
|
||||||
|
```
|
||||||
|
|
||||||
|
Для автоматического режима:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Готово. Автоматический вход включён.
|
||||||
|
Если он не сработает, используйте ручной режим: vpn --manual-full
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ошибки И Подсказки
|
||||||
|
|
||||||
|
Ошибки должны говорить, что делать дальше:
|
||||||
|
|
||||||
|
- Нет сохранённого пароля: `Запустите vpn --configure-login или подключитесь вручную.`
|
||||||
|
- Нет постоянного кода 2FA для auto mode: `Автоматический вход не настроен. Выберите ручной режим или добавьте постоянный код настройки 2FA.`
|
||||||
|
- Bitwarden заблокирован в приложении без Terminal: `Откройте Terminal и выполните vpn, либо подключитесь вручную.`
|
||||||
|
- Пользователь выбрал непонятный вариант: `Введите 1, 2 или 3. Если не уверены, нажмите Enter.`
|
||||||
|
|
||||||
|
### Acceptance Evidence Для Installer UX
|
||||||
|
|
||||||
|
- Interactive dry-run через `expect` должен захватить первый экран, выбор режима и финальную подсказку.
|
||||||
|
- Smoke должен проверять, что Enter выбирает ручной режим.
|
||||||
|
- README должен показывать тот же сценарий, что и installer.
|
||||||
|
- В обычном wizard не должно быть слов `credential_source`, `auth_profile`, `TOTP seed`; они допустимы только в help/debug/docs for advanced.
|
||||||
|
|
||||||
|
## Дополнительные Функции
|
||||||
|
|
||||||
|
1. **Core manual SSO (default)**
|
||||||
|
- Что делает: открывает видимый браузер Keycloak, не читает Bitwarden/Keychain, не заполняет поля, не нажимает submit.
|
||||||
|
- Когда включается: всегда на fresh install и при `vpn` без дополнительных настроек.
|
||||||
|
- Когда не включается: если пользователь явно запускает `vpn-auto` или выбрал auto profile.
|
||||||
|
|
||||||
|
2. **Saved LDAP login/password in macOS Keychain**
|
||||||
|
- Что делает: сохраняет LDAP username/password в Keychain и подставляет их в видимую форму.
|
||||||
|
- Когда включается: пользователь явно запускает настройку сохранения LDAP или выбирает этот пункт в installer.
|
||||||
|
- OTP: текущий одноразовый код вводится руками; TOTP seed не требуется.
|
||||||
|
|
||||||
|
3. **Saved TOTP seed**
|
||||||
|
- Что делает: сохраняет постоянный TOTP seed и позволяет `openconnect-lite` генерировать текущий код для autofill.
|
||||||
|
- Когда включается: пользователь явно добавил seed через full Keychain setup или Bitwarden sync.
|
||||||
|
- Ограничение: текущий 6-значный код нельзя сохранять как настройку.
|
||||||
|
|
||||||
|
4. **Autofill**
|
||||||
|
- Что делает: заполняет те поля, для которых есть capability: username/password, а затем optional TOTP.
|
||||||
|
- Когда включается: при `saved-login` или `saved-totp`.
|
||||||
|
- Submit: по умолчанию выключен; пользователь нажимает вход сам.
|
||||||
|
|
||||||
|
5. **Autosubmit/headless**
|
||||||
|
- Что делает: запускает старый удобный путь с hidden browser, полным autofill и нажатием submit.
|
||||||
|
- Когда включается: только явным `vpn-auto`, `--auto`, profile `auto` или отдельным opt-in флагом.
|
||||||
|
- Предусловие: saved login/password и TOTP seed должны быть доступны; без TOTP seed этот режим должен fail fast с понятной ошибкой.
|
||||||
|
|
||||||
|
6. **Bitwarden sync**
|
||||||
|
- Что делает: читает LDAP password и TOTP seed из Bitwarden item, нормализует TOTP seed и перекладывает в Keychain.
|
||||||
|
- Когда включается: только если пользователь выбрал Bitwarden provider.
|
||||||
|
- Fallback: если `bw` отсутствует или locked в GUI/no-tty, использовать уже сохранённый Keychain либо manual path, не блокировать default.
|
||||||
|
|
||||||
|
7. **Touch ID helper**
|
||||||
|
- Что делает: локально защищает чтение Bitwarden master password.
|
||||||
|
- Когда включается: только вместе с Bitwarden provider и явным согласием пользователя.
|
||||||
|
- Не делает: не заменяет LDAP password и не является хранением VPN credentials.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Task 1: Rewrite user-facing contract and docs
|
||||||
|
|
||||||
|
- Scope: `README.md`.
|
||||||
|
- Change: describe manual-first default, feature matrix, migration from old Bitwarden-first install, and the non-IT installation scenario from this plan.
|
||||||
|
- Expected output: docs no longer say Bitwarden/auto are default; examples show `vpn` as manual and `vpn-auto` as opt-in; installation instructions use the same wording as the installer.
|
||||||
|
- Verification: `rg -n "по умолчанию|Bitwarden|vpn-auto|manual-full|одноразовый код|ручной режим" README.md`.
|
||||||
|
- Acceptance evidence: a new user can read the first install section and understand what will be installed, what they will type during first connect, and that Bitwarden is optional.
|
||||||
|
- Parallel: yes, but must be reconciled with installer flag names from Task 2.
|
||||||
|
|
||||||
|
### Task 2: Change installer defaults and capability selection
|
||||||
|
|
||||||
|
- Scope: `install.sh`.
|
||||||
|
- Change: default credential source becomes none/manual; Bitwarden and Touch ID default off; wizard Enter keeps manual; add or reuse flags for saved LDAP, full Keychain, Bitwarden provider, and auto profile; rewrite interactive copy using the "Пользовательский Процесс Установки" section.
|
||||||
|
- Expected output: dry-run shows manual/default auth and no Bitwarden/Touch ID install unless selected; interactive prompts say "вводить всё вручную", "запомнить логин и пароль", and "автоматический вход" instead of exposing low-level config names.
|
||||||
|
- Verification: `sh -n install.sh`; `sh install.sh --dry-run --non-interactive`; interactive dry-run through `expect` if available.
|
||||||
|
- Acceptance evidence: clean install dry-run does not try to install Bitwarden and does not schedule Keychain credential prompts by default; captured interactive output is understandable without IT vocabulary and Enter chooses manual mode.
|
||||||
|
- Parallel: no, because it defines env contract consumed by CLI/tests.
|
||||||
|
|
||||||
|
### Task 3: Refactor CLI auth profiles and partial Keychain support
|
||||||
|
|
||||||
|
- Scope: `bin/vpn-lemanapro.sh`.
|
||||||
|
- Change: replace auto-first/default credential preflight with an auth-profile resolver; add partial Keychain support for saved login without TOTP; keep full Keychain and Bitwarden sync as opt-ins.
|
||||||
|
- Expected output: no-credential default reaches visible manual browser; saved-login fills username/password and leaves OTP manual; saved-totp can fill OTP but still does not submit by default.
|
||||||
|
- Verification: `bash -n bin/vpn-lemanapro.sh`; fake `openconnect-lite` capture in `tests/smoke.sh`.
|
||||||
|
- Acceptance evidence: JSON events clearly state active auth profile and missing optional capabilities without treating them as fatal in manual profiles.
|
||||||
|
- Parallel: no, central truth owner.
|
||||||
|
|
||||||
|
### Task 4: Preserve automatic compatibility without keeping it dominant
|
||||||
|
|
||||||
|
- Scope: `bin/vpn-lemanapro.sh`, `install.sh`, shell aliases.
|
||||||
|
- Change: keep `vpn-auto`/`--auto` as explicit old behavior; guard autosubmit/headless behind full saved credentials; keep `--manual-full` compatibility alias for the default manual profile.
|
||||||
|
- Expected output: current power users can still run the old path intentionally.
|
||||||
|
- Verification: `tests/smoke.sh` assertions for `vpn-auto`, `--auto`, `--manual-full`, launch env.
|
||||||
|
- Acceptance evidence: default path and auto path are both test-covered and visibly separate.
|
||||||
|
- Parallel: can run after Task 3.
|
||||||
|
|
||||||
|
### Task 5: Update Swift menu-bar app surface
|
||||||
|
|
||||||
|
- Scope: `app/Sources/LemanaVPN/VPNManager.swift`, `app/Sources/LemanaVPN/LemanaVPNApp.swift`.
|
||||||
|
- Change: make the primary menu action manual-first; demote automatic connect to an explicit secondary action; keep app as a CLI wrapper; surface missing saved credentials as setup guidance, not hidden prompts.
|
||||||
|
- Expected output: app default action does not trigger Bitwarden or autosubmit unless the user chooses an explicit auto/saved mode.
|
||||||
|
- Verification: `swift build -c release --package-path app`; smoke greps for the new labels/mode wiring.
|
||||||
|
- Acceptance evidence: app source and built binary contain the manual-first label and map it to the manual profile.
|
||||||
|
- Parallel: yes after CLI mode names are stable.
|
||||||
|
|
||||||
|
### Task 6: Expand smoke coverage
|
||||||
|
|
||||||
|
- Scope: `tests/smoke.sh`.
|
||||||
|
- Change: update old assertions and add cases for fresh manual default, saved-login-only, saved-totp, Bitwarden opt-in, and app wiring.
|
||||||
|
- Expected output: smoke becomes the executable spec for the feature matrix and installer UX.
|
||||||
|
- Verification: `sh -n tests/smoke.sh`; `tests/smoke.sh`.
|
||||||
|
- Acceptance evidence: smoke fails if Bitwarden or auto become fresh defaults again, or if the basic wizard no longer defaults to manual mode with non-technical wording.
|
||||||
|
- Parallel: yes after Task 2 and Task 3 contracts are known.
|
||||||
|
|
||||||
|
### Task 7: Final verification and local update notes
|
||||||
|
|
||||||
|
- Scope: whole repo; installed app only if doing a local refresh.
|
||||||
|
- Change: run non-live ladder, then optionally install/update local checkout with `sh install.sh` after user approval for real local changes.
|
||||||
|
- Expected output: plan implemented with evidence and a clear "live VPN not run" or live manual evidence.
|
||||||
|
- Verification:
|
||||||
|
- `sh -n install.sh`
|
||||||
|
- `sh -n uninstall.sh`
|
||||||
|
- `bash -n bin/vpn-lemanapro.sh`
|
||||||
|
- `sh -n tests/smoke.sh`
|
||||||
|
- `tests/smoke.sh`
|
||||||
|
- `swift build -c release --package-path app`
|
||||||
|
- `git diff --check`
|
||||||
|
- Acceptance evidence: final answer includes how to run, how to check, how to update installed setup, and what live checks were intentionally skipped.
|
||||||
|
- Parallel: no, final integration gate.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Do not replace `openconnect-lite` or implement a separate VPN client in Swift.
|
||||||
|
- Do not store current 6-digit OTP codes.
|
||||||
|
- Do not make the GUI collect secrets directly.
|
||||||
|
- Do not remove existing automatic mode; demote it to explicit opt-in.
|
||||||
|
- Do not change DNS cleanup behavior as part of this refactor.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Existing users may expect `vpn` to auto-connect. Mitigation: keep `vpn-auto` and document the cutover clearly.
|
||||||
|
- Partial Keychain support may need careful `openconnect-lite` behavior verification when TOTP is absent. Mitigation: fake launch tests first, then one visible manual smoke if needed.
|
||||||
|
- Swift app could lag behind installed source again. Mitigation: after implementation refresh installed bundle via `sh install.sh` and verify with app source/build or `strings` if doing local install.
|
||||||
|
- Credential language can confuse Bitwarden master password, LDAP password, TOTP seed, and current OTP. Mitigation: keep labels explicit in CLI and README.
|
||||||
|
|
||||||
|
## Execution Order
|
||||||
|
|
||||||
|
1. Task 2 defines the installer/env contract.
|
||||||
|
2. Task 3 implements the CLI truth owner.
|
||||||
|
3. Task 4 preserves explicit auto compatibility.
|
||||||
|
4. Task 5 updates the app wrapper.
|
||||||
|
5. Task 6 turns the matrix into smoke coverage.
|
||||||
|
6. Task 1 updates docs with final flag/profile names.
|
||||||
|
7. Task 7 verifies and prepares local update instructions.
|
||||||
@@ -772,7 +772,7 @@ LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
|
|||||||
For the keychain source, add this separate example:
|
For the keychain source, add this separate example:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
LEMANA_VPN_USERNAME="60103293"
|
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
|
||||||
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
|
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
|
||||||
LEMANA_VPN_USE_BITWARDEN="0"
|
LEMANA_VPN_USE_BITWARDEN="0"
|
||||||
LEMANA_VPN_USE_TOUCHID="0"
|
LEMANA_VPN_USE_TOUCHID="0"
|
||||||
|
|||||||
322
install.sh
322
install.sh
@@ -9,27 +9,31 @@ INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
|
|||||||
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
||||||
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
||||||
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||||
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
USERNAME="${LEMANA_VPN_USERNAME:-}"
|
||||||
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||||
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
|
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-none}"
|
||||||
USE_BITWARDEN=1
|
AUTH_PROFILE="${LEMANA_VPN_AUTH_PROFILE:-manual}"
|
||||||
USE_TOUCHID=1
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
INSTALL_SUDOERS=1
|
INSTALL_SUDOERS=1
|
||||||
INSTALL_ALIASES=1
|
INSTALL_ALIASES=1
|
||||||
INSTALL_APP=1
|
INSTALL_APP=1
|
||||||
INSTALL_AUTOSTART=1
|
INSTALL_AUTOSTART=1
|
||||||
|
CONFIGURE_LOGIN=0
|
||||||
CONFIGURE_KEYCHAIN=0
|
CONFIGURE_KEYCHAIN=0
|
||||||
DRY_RUN=0
|
DRY_RUN=0
|
||||||
FORCE=0
|
FORCE=0
|
||||||
INTERACTIVE=auto
|
INTERACTIVE=auto
|
||||||
BITWARDEN_FORCED=0
|
BITWARDEN_FORCED=0
|
||||||
CREDENTIAL_SOURCE_FORCED=0
|
CREDENTIAL_SOURCE_FORCED=0
|
||||||
|
AUTH_PROFILE_FORCED=0
|
||||||
TOUCHID_FORCED=0
|
TOUCHID_FORCED=0
|
||||||
SUDOERS_FORCED=0
|
SUDOERS_FORCED=0
|
||||||
SHELL_FORCED=0
|
SHELL_FORCED=0
|
||||||
APP_FORCED=0
|
APP_FORCED=0
|
||||||
AUTOSTART_FORCED=0
|
AUTOSTART_FORCED=0
|
||||||
CONFIGURE_KEYCHAIN_FORCED=0
|
CONFIGURE_KEYCHAIN_FORCED=0
|
||||||
|
CONFIGURE_LOGIN_FORCED=0
|
||||||
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
||||||
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
|
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
|
||||||
|
|
||||||
@@ -75,13 +79,15 @@ Usage:
|
|||||||
sh install.sh [options]
|
sh install.sh [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--with-bitwarden Install/use Bitwarden CLI module (default)
|
--with-bitwarden Install/use Bitwarden CLI module as an optional credential source
|
||||||
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
--without-bitwarden Do not install/use Bitwarden CLI (default)
|
||||||
--credential-source VALUE Credential source: bitwarden or keychain
|
--credential-source VALUE Credential source: none, keychain, or bitwarden
|
||||||
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
--auth-profile VALUE Auth profile: manual, saved-login, saved-totp, or auto
|
||||||
|
--with-touchid Install/use keychain-fingerprint Touch ID helper with Bitwarden
|
||||||
--without-touchid Do not install/use Touch ID helper
|
--without-touchid Do not install/use Touch ID helper
|
||||||
--configure-keychain Prompt for LDAP password and TOTP secret after install
|
--configure-login Prompt for LDAP username/password after install
|
||||||
--username VALUE Corporate LDAP username (default: 60103293)
|
--configure-keychain Prompt for LDAP password and permanent 2FA setup code after install
|
||||||
|
--username VALUE Corporate LDAP username (otherwise asked on first Terminal connection)
|
||||||
--bw-item VALUE Bitwarden item name (default: LM LDAP)
|
--bw-item VALUE Bitwarden item name (default: LM LDAP)
|
||||||
--raw-base-url URL Raw file base URL for curl installs
|
--raw-base-url URL Raw file base URL for curl installs
|
||||||
--no-sudoers Do not install sudoers rules
|
--no-sudoers Do not install sudoers rules
|
||||||
@@ -92,15 +98,15 @@ Options:
|
|||||||
--without-autostart Do not install LaunchAgent
|
--without-autostart Do not install LaunchAgent
|
||||||
--interactive Ask before installing optional missing modules
|
--interactive Ask before installing optional missing modules
|
||||||
--non-interactive Use selected/default modules without prompts
|
--non-interactive Use selected/default modules without prompts
|
||||||
--minimal Same as --without-bitwarden --without-touchid
|
--minimal Manual-first install without optional credential helpers
|
||||||
--dry-run Print actions without changing files
|
--dry-run Print actions without changing files
|
||||||
--force Reinstall files even when present
|
--force Reinstall files even when present
|
||||||
-h, --help Show this help
|
-h, --help Show this help
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
sh install.sh
|
sh install.sh
|
||||||
sh install.sh --minimal --configure-keychain
|
sh install.sh --configure-login
|
||||||
sh install.sh --credential-source keychain --configure-keychain
|
sh install.sh --credential-source keychain --auth-profile saved-totp --configure-keychain
|
||||||
sh install.sh --without-touchid
|
sh install.sh --without-touchid
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
@@ -112,29 +118,53 @@ while [ "$#" -gt 0 ]; do
|
|||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
BITWARDEN_FORCED=1
|
BITWARDEN_FORCED=1
|
||||||
CREDENTIAL_SOURCE_FORCED=1
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
|
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "manual" ]; then
|
||||||
|
AUTH_PROFILE="saved-totp"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
--without-bitwarden)
|
--without-bitwarden)
|
||||||
CREDENTIAL_SOURCE="keychain"
|
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ]; then
|
||||||
|
CREDENTIAL_SOURCE="none"
|
||||||
|
fi
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
BITWARDEN_FORCED=1
|
BITWARDEN_FORCED=1
|
||||||
CREDENTIAL_SOURCE_FORCED=1
|
|
||||||
;;
|
;;
|
||||||
--credential-source)
|
--credential-source)
|
||||||
shift
|
shift
|
||||||
[ "$#" -gt 0 ] || { echo "--credential-source requires bitwarden or keychain" >&2; exit 1; }
|
[ "$#" -gt 0 ] || { echo "--credential-source requires none, keychain, or bitwarden" >&2; exit 1; }
|
||||||
CREDENTIAL_SOURCE="$1"
|
CREDENTIAL_SOURCE="$1"
|
||||||
CREDENTIAL_SOURCE_FORCED=1
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
case "$CREDENTIAL_SOURCE" in
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
none)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
bitwarden)
|
bitwarden)
|
||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
|
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "manual" ]; then
|
||||||
|
AUTH_PROFILE="saved-totp"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
keychain)
|
keychain)
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "--credential-source requires bitwarden or keychain" >&2
|
echo "--credential-source requires none, keychain, or bitwarden" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
--auth-profile)
|
||||||
|
shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "--auth-profile requires manual, saved-login, saved-totp, or auto" >&2; exit 1; }
|
||||||
|
AUTH_PROFILE="$1"
|
||||||
|
AUTH_PROFILE_FORCED=1
|
||||||
|
case "$AUTH_PROFILE" in
|
||||||
|
manual|saved-login|saved-totp|auto) ;;
|
||||||
|
*)
|
||||||
|
echo "--auth-profile requires manual, saved-login, saved-totp, or auto" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -147,9 +177,31 @@ while [ "$#" -gt 0 ]; do
|
|||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
TOUCHID_FORCED=1
|
TOUCHID_FORCED=1
|
||||||
;;
|
;;
|
||||||
|
--configure-login)
|
||||||
|
CONFIGURE_LOGIN=1
|
||||||
|
CONFIGURE_LOGIN_FORCED=1
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
|
if [ "$AUTH_PROFILE_FORCED" -eq 0 ]; then
|
||||||
|
AUTH_PROFILE="saved-login"
|
||||||
|
fi
|
||||||
|
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
--configure-keychain)
|
--configure-keychain)
|
||||||
CONFIGURE_KEYCHAIN=1
|
CONFIGURE_KEYCHAIN=1
|
||||||
|
CONFIGURE_LOGIN=0
|
||||||
CONFIGURE_KEYCHAIN_FORCED=1
|
CONFIGURE_KEYCHAIN_FORCED=1
|
||||||
|
if [ "$AUTH_PROFILE_FORCED" -eq 0 ]; then
|
||||||
|
AUTH_PROFILE="saved-totp"
|
||||||
|
fi
|
||||||
|
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
--username)
|
--username)
|
||||||
shift
|
shift
|
||||||
@@ -195,11 +247,13 @@ while [ "$#" -gt 0 ]; do
|
|||||||
--interactive) INTERACTIVE=1 ;;
|
--interactive) INTERACTIVE=1 ;;
|
||||||
--non-interactive) INTERACTIVE=0 ;;
|
--non-interactive) INTERACTIVE=0 ;;
|
||||||
--minimal)
|
--minimal)
|
||||||
CREDENTIAL_SOURCE="keychain"
|
CREDENTIAL_SOURCE="none"
|
||||||
|
AUTH_PROFILE="manual"
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
BITWARDEN_FORCED=1
|
BITWARDEN_FORCED=1
|
||||||
CREDENTIAL_SOURCE_FORCED=1
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
|
AUTH_PROFILE_FORCED=1
|
||||||
TOUCHID_FORCED=1
|
TOUCHID_FORCED=1
|
||||||
;;
|
;;
|
||||||
--dry-run) DRY_RUN=1 ;;
|
--dry-run) DRY_RUN=1 ;;
|
||||||
@@ -329,54 +383,121 @@ yes_no() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
choose_credential_source_interactive() {
|
print_install_intro() {
|
||||||
log_step "Настраиваю способ хранения credentials"
|
log_step "Lemana VPN установит приложение для подключения к корпоративному VPN"
|
||||||
log_detail "Можно выбрать Bitwarden или бесплатный macOS Keychain flow с ручным вводом LDAP password и TOTP seed."
|
log_detail "По умолчанию ничего не сохраняется: при подключении откроется окно входа, а логин, пароль и одноразовый код вводятся вручную."
|
||||||
|
log_detail "Дополнительные удобства можно включить сейчас или позже."
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_advanced_signin_interactive() {
|
||||||
while :; do
|
while :; do
|
||||||
prompt_printf '\n'
|
prompt_printf '\n'
|
||||||
prompt_printf '%sКак хранить VPN credentials?%s\n' "$C_BOLD" "$C_RESET"
|
prompt_printf '%sАвтоматический вход требует сохранённых данных.%s\n' "$C_BOLD" "$C_RESET"
|
||||||
prompt_printf ' 1) Bitwarden -> macOS Keychain\n'
|
prompt_printf ' Автоматический вход работает только если на Mac сохранены корпоративный пароль\n'
|
||||||
prompt_printf ' 2) macOS Keychain: ввести LDAP password и TOTP seed сейчас\n'
|
prompt_printf ' и постоянный код настройки 2FA. Это не текущие 6 цифр из приложения.\n\n'
|
||||||
prompt_printf ' 3) macOS Keychain: настрою вручную позже\n'
|
prompt_printf ' 1) Вернуться к ручному режиму\n'
|
||||||
|
prompt_printf ' 2) Сохранить пароль и постоянный код 2FA в macOS Keychain\n'
|
||||||
|
prompt_printf ' 3) Взять пароль и код 2FA из Bitwarden\n'
|
||||||
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
|
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
|
||||||
|
|
||||||
prompt_read_answer
|
prompt_read_answer
|
||||||
case "$answer" in
|
case "$answer" in
|
||||||
""|1)
|
""|1)
|
||||||
CREDENTIAL_SOURCE=bitwarden
|
AUTH_PROFILE=manual
|
||||||
USE_BITWARDEN=1
|
CREDENTIAL_SOURCE=none
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_LOGIN=0
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
|
AUTH_PROFILE=saved-totp
|
||||||
CREDENTIAL_SOURCE=keychain
|
CREDENTIAL_SOURCE=keychain
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_LOGIN=0
|
||||||
CONFIGURE_KEYCHAIN=1
|
CONFIGURE_KEYCHAIN=1
|
||||||
;;
|
;;
|
||||||
3)
|
3)
|
||||||
CREDENTIAL_SOURCE=keychain
|
AUTH_PROFILE=saved-totp
|
||||||
USE_BITWARDEN=0
|
CREDENTIAL_SOURCE=bitwarden
|
||||||
USE_TOUCHID=0
|
USE_BITWARDEN=1
|
||||||
|
CONFIGURE_LOGIN=0
|
||||||
CONFIGURE_KEYCHAIN=0
|
CONFIGURE_KEYCHAIN=0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
prompt_printf 'Введите 1, 2 или 3.\n'
|
prompt_printf 'Введите 1, 2 или 3. Если не уверены, нажмите Enter.\n'
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
break
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
choose_auth_profile_interactive() {
|
||||||
|
log_step "Выбираю режим подключения"
|
||||||
|
log_detail "Если не уверены, просто нажмите Enter: будет ручной режим без сохранения паролей."
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
prompt_printf '\n'
|
||||||
|
prompt_printf '%sКак подключаться по умолчанию?%s\n' "$C_BOLD" "$C_RESET"
|
||||||
|
prompt_printf ' 1) Вводить всё вручную (рекомендуется)\n'
|
||||||
|
prompt_printf ' Откроется окно входа. Вы сами введёте логин, пароль и одноразовый код.\n\n'
|
||||||
|
prompt_printf ' 2) Запомнить логин и пароль на этом Mac\n'
|
||||||
|
prompt_printf ' При входе логин и пароль подставятся сами. Одноразовый код вы введёте вручную.\n\n'
|
||||||
|
prompt_printf ' 3) Настроить автоматический вход\n'
|
||||||
|
prompt_printf ' Нужно заранее сохранить пароль и постоянный код настройки 2FA.\n'
|
||||||
|
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
|
||||||
|
|
||||||
|
prompt_read_answer
|
||||||
|
case "$answer" in
|
||||||
|
""|1)
|
||||||
|
AUTH_PROFILE=manual
|
||||||
|
CREDENTIAL_SOURCE=none
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_LOGIN=0
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
|
if yes_no "Сохранить логин и пароль в macOS Keychain для подстановки при входе?" n; then
|
||||||
|
AUTH_PROFILE=saved-login
|
||||||
|
CREDENTIAL_SOURCE=keychain
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_LOGIN=1
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
AUTH_PROFILE=saved-login
|
||||||
|
CREDENTIAL_SOURCE=keychain
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_LOGIN=1
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
choose_advanced_signin_interactive
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
prompt_printf 'Введите 1, 2 или 3. Если не уверены, нажмите Enter.\n'
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
break
|
break
|
||||||
done
|
done
|
||||||
|
|
||||||
case "$CREDENTIAL_SOURCE" in
|
case "$AUTH_PROFILE" in
|
||||||
bitwarden)
|
manual)
|
||||||
log_info "Credential source: Bitwarden sync into macOS Keychain"
|
log_info "Режим подключения: ручной ввод логина, пароля и одноразового кода"
|
||||||
;;
|
;;
|
||||||
keychain)
|
saved-login)
|
||||||
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
log_info "Режим подключения: логин и пароль будут сохранены в macOS Keychain, одноразовый код вводится вручную"
|
||||||
log_info "Credential source: macOS Keychain, credentials будут запрошены после установки"
|
;;
|
||||||
else
|
saved-totp)
|
||||||
log_info "Credential source: macOS Keychain, credentials можно настроить позже через vpn --configure-keychain"
|
log_info "Режим подключения: сохранённые данные будут подставляться в форму входа"
|
||||||
fi
|
;;
|
||||||
|
auto)
|
||||||
|
log_info "Режим подключения: автоматический вход включается только через явный auto-запуск"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -413,13 +534,31 @@ print_detected_state() {
|
|||||||
log " Menu Bar app: $(bool_word test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
log " Menu Bar app: $(bool_word test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
||||||
log " LaunchAgent: $(bool_word test -f "$LAUNCH_AGENT")"
|
log " LaunchAgent: $(bool_word test -f "$LAUNCH_AGENT")"
|
||||||
log " Keychain password: $(bool_word keychain_has openconnect-lite "$USERNAME")"
|
log " Keychain password: $(bool_word keychain_has openconnect-lite "$USERNAME")"
|
||||||
log " Keychain TOTP seed: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")"
|
log " Keychain 2FA setup: $(bool_word keychain_has openconnect-lite "totp/$USERNAME")"
|
||||||
}
|
}
|
||||||
|
|
||||||
choose_modules() {
|
choose_modules() {
|
||||||
|
if interactive_enabled; then
|
||||||
|
print_install_intro
|
||||||
|
fi
|
||||||
print_detected_state
|
print_detected_state
|
||||||
|
|
||||||
|
case "$AUTH_PROFILE" in
|
||||||
|
manual|saved-login|saved-totp|auto) ;;
|
||||||
|
*)
|
||||||
|
die "Unknown auth profile: $AUTH_PROFILE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "none" ] && [ "$AUTH_PROFILE" != "manual" ]; then
|
||||||
|
CREDENTIAL_SOURCE=keychain
|
||||||
|
fi
|
||||||
|
|
||||||
case "$CREDENTIAL_SOURCE" in
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
none)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
bitwarden)
|
bitwarden)
|
||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
;;
|
;;
|
||||||
@@ -438,18 +577,19 @@ choose_modules() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "Interactive prompts: on"
|
log_info "Interactive prompts: on"
|
||||||
log_detail "Установщик проведёт по выбору credential source и отсутствующих опциональных модулей; флаги командной строки имеют приоритет."
|
log_detail "Установщик проведёт по понятному выбору режима входа; флаги командной строки имеют приоритет."
|
||||||
|
|
||||||
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
if [ "$AUTH_PROFILE_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||||
choose_credential_source_interactive
|
choose_auth_profile_interactive
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
|
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
|
||||||
if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then
|
if yes_no "Bitwarden не найден на этом Mac. Установить его сейчас?" n; then
|
||||||
CREDENTIAL_SOURCE=bitwarden
|
CREDENTIAL_SOURCE=bitwarden
|
||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
else
|
else
|
||||||
CREDENTIAL_SOURCE=keychain
|
AUTH_PROFILE=manual
|
||||||
|
CREDENTIAL_SOURCE=none
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
fi
|
fi
|
||||||
@@ -458,7 +598,7 @@ choose_modules() {
|
|||||||
if [ "$TOUCHID_FORCED" -eq 0 ]; then
|
if [ "$TOUCHID_FORCED" -eq 0 ]; then
|
||||||
if [ "$USE_BITWARDEN" -eq 1 ]; then
|
if [ "$USE_BITWARDEN" -eq 1 ]; then
|
||||||
if ! [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ]; then
|
if ! [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ]; then
|
||||||
if yes_no "Touch ID helper не найден. Собрать и установить?" y; then
|
if yes_no "Разрешить Touch ID для открытия Bitwarden?" n; then
|
||||||
USE_TOUCHID=1
|
USE_TOUCHID=1
|
||||||
else
|
else
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
@@ -504,9 +644,17 @@ choose_modules() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
|
if [ "$CONFIGURE_LOGIN_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "saved-login" ] && [ "$CONFIGURE_LOGIN" -eq 0 ]; then
|
||||||
|
if ! keychain_has openconnect-lite "$USERNAME"; then
|
||||||
|
if yes_no "Запомнить логин и пароль в macOS Keychain после установки?" y; then
|
||||||
|
CONFIGURE_LOGIN=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$AUTH_PROFILE" = "saved-totp" ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
|
||||||
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
|
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
|
||||||
if yes_no "Bitwarden отключён, а Keychain credentials неполные. Записать LDAP-пароль и TOTP seed после установки?" y; then
|
if yes_no "Сохранить пароль и постоянный код настройки 2FA в macOS Keychain после установки?" y; then
|
||||||
CONFIGURE_KEYCHAIN=1
|
CONFIGURE_KEYCHAIN=1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -516,7 +664,13 @@ choose_modules() {
|
|||||||
script_dir() {
|
script_dir() {
|
||||||
case "$0" in
|
case "$0" in
|
||||||
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
|
*/*) cd "$(dirname "$0")" 2>/dev/null && pwd ;;
|
||||||
*) return 1 ;;
|
*)
|
||||||
|
if [ -f "$PWD/$0" ]; then
|
||||||
|
pwd
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,7 +723,7 @@ install_homebrew_packages() {
|
|||||||
log_ok "Homebrew package already installed: bitwarden-cli"
|
log_ok "Homebrew package already installed: bitwarden-cli"
|
||||||
else
|
else
|
||||||
log_info "Installing Homebrew package: bitwarden-cli"
|
log_info "Installing Homebrew package: bitwarden-cli"
|
||||||
log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и TOTP seed из item '$BW_ITEM'."
|
log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и постоянный код настройки 2FA из item '$BW_ITEM'."
|
||||||
run brew install bitwarden-cli
|
run brew install bitwarden-cli
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -615,7 +769,7 @@ install_cli() {
|
|||||||
install_config() {
|
install_config() {
|
||||||
tmp="$1"
|
tmp="$1"
|
||||||
log_step "Записываю конфигурацию"
|
log_step "Записываю конфигурацию"
|
||||||
log_detail "Здесь сохраняются выбранные модули, LDAP username и профиль openconnect-lite для Keycloak SSO."
|
log_detail "Здесь сохраняются выбранный режим входа, LDAP username если он задан, и профиль openconnect-lite для Keycloak SSO."
|
||||||
|
|
||||||
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
|
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"
|
||||||
|
|
||||||
@@ -628,6 +782,7 @@ install_config() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=\"$AUTH_PROFILE\"
|
||||||
LEMANA_VPN_CREDENTIAL_SOURCE=\"$CREDENTIAL_SOURCE\"
|
LEMANA_VPN_CREDENTIAL_SOURCE=\"$CREDENTIAL_SOURCE\"
|
||||||
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
|
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
|
||||||
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
|
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
|
||||||
@@ -878,7 +1033,7 @@ EOF
|
|||||||
|
|
||||||
maybe_login_bitwarden() {
|
maybe_login_bitwarden() {
|
||||||
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
|
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
|
||||||
log_skip "Credential source is keychain; пропускаю Bitwarden login."
|
log_skip "Bitwarden не выбран; пропускаю Bitwarden login."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! command -v bw >/dev/null 2>&1; then
|
if ! command -v bw >/dev/null 2>&1; then
|
||||||
@@ -899,6 +1054,43 @@ maybe_login_bitwarden() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth_profile_label() {
|
||||||
|
case "$AUTH_PROFILE" in
|
||||||
|
manual) printf 'ручной ввод без сохранения паролей' ;;
|
||||||
|
saved-login) printf 'сохранить логин и пароль, одноразовый код вводить вручную' ;;
|
||||||
|
saved-totp) printf 'подставлять сохранённые данные в окно входа' ;;
|
||||||
|
auto) printf 'автоматический вход только при явном запуске' ;;
|
||||||
|
*) printf '%s' "$AUTH_PROFILE" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
print_final_next_steps() {
|
||||||
|
log ""
|
||||||
|
log_ok "Готово."
|
||||||
|
log_info "Open a new shell or run: exec zsh"
|
||||||
|
case "$AUTH_PROFILE" in
|
||||||
|
manual)
|
||||||
|
log_info "Как подключиться: откройте LemanaVPN.app и нажмите \"Подключить\", или выполните: vpn"
|
||||||
|
log_detail "Откроется окно входа. Введите корпоративный логин, пароль и одноразовый код вручную."
|
||||||
|
;;
|
||||||
|
saved-login)
|
||||||
|
log_info "Как подключиться: Lemana VPN подставит логин и пароль, одноразовый код введите вручную."
|
||||||
|
log_detail "Команда подключения: vpn"
|
||||||
|
;;
|
||||||
|
saved-totp)
|
||||||
|
log_info "Как подключиться: Lemana VPN подставит сохранённые данные в видимое окно входа."
|
||||||
|
log_detail "Для полностью автоматического запуска используйте явно: vpn-auto"
|
||||||
|
;;
|
||||||
|
auto)
|
||||||
|
log_info "Автоматический вход включён явно. Если он не сработает, используйте ручной режим: vpn --manual-full"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
log_info "Status: vpn --status"
|
||||||
|
if [ "$INSTALL_APP" -eq 1 ]; then
|
||||||
|
log_info "App: open '$APP_DIR'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
|
[ "$(uname -s)" = "Darwin" ] || die "This installer supports macOS only"
|
||||||
|
|
||||||
@@ -909,7 +1101,12 @@ main() {
|
|||||||
|
|
||||||
log_step "Начинаю установку Lemana VPN"
|
log_step "Начинаю установку Lemana VPN"
|
||||||
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
|
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
|
||||||
log_info "Modules: credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
if interactive_enabled; then
|
||||||
|
log_info "Выбран режим: $(auth_profile_label)"
|
||||||
|
log_detail "Bitwarden и Touch ID будут установлены только если они выбраны явно."
|
||||||
|
else
|
||||||
|
log_info "Modules: auth_profile=$AUTH_PROFILE credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
||||||
|
fi
|
||||||
|
|
||||||
install_homebrew_packages
|
install_homebrew_packages
|
||||||
install_openconnect_lite
|
install_openconnect_lite
|
||||||
@@ -924,20 +1121,17 @@ main() {
|
|||||||
install_shell_aliases "$tmp"
|
install_shell_aliases "$tmp"
|
||||||
maybe_login_bitwarden
|
maybe_login_bitwarden
|
||||||
|
|
||||||
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
if [ "$CONFIGURE_LOGIN" -eq 1 ]; then
|
||||||
|
log_step "Записываю логин и пароль в macOS Keychain"
|
||||||
|
log_detail "Будет запрошен корпоративный LDAP-пароль. Одноразовый код при подключении останется ручным."
|
||||||
|
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-login
|
||||||
|
elif [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||||
log_step "Записываю credentials в macOS Keychain"
|
log_step "Записываю credentials в macOS Keychain"
|
||||||
log_detail "Будут запрошены LDAP-пароль и постоянный TOTP seed, не текущий 30-секундный код."
|
log_detail "Будут запрошены LDAP-пароль и постоянный код настройки 2FA, не текущий 30-секундный код."
|
||||||
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
|
run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log ""
|
print_final_next_steps
|
||||||
log_ok "Done."
|
|
||||||
log_info "Open a new shell or run: exec zsh"
|
|
||||||
log_info "Connect: vpn"
|
|
||||||
log_info "Status: vpn --status"
|
|
||||||
if [ "$INSTALL_APP" -eq 1 ]; then
|
|
||||||
log_info "App: open '$APP_DIR'"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
454
tests/smoke.sh
454
tests/smoke.sh
@@ -15,13 +15,18 @@ output="$(cd "$ROOT" && sh install.sh --dry-run --non-interactive --minimal)"
|
|||||||
|
|
||||||
printf '%s\n' "$output" | grep -q 'Detected state:'
|
printf '%s\n' "$output" | grep -q 'Detected state:'
|
||||||
printf '%s\n' "$output" | grep -q 'Interactive prompts: off'
|
printf '%s\n' "$output" | grep -q 'Interactive prompts: off'
|
||||||
printf '%s\n' "$output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
printf '%s\n' "$output" | grep -q 'Modules: auth_profile=manual credential_source=none bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
||||||
printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости'
|
printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости'
|
||||||
printf '%s\n' "$output" | grep -q 'Swift build может занять минуту'
|
printf '%s\n' "$output" | grep -q 'Swift build может занять минуту'
|
||||||
printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin'
|
printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin'
|
||||||
printf '%s\n' "$output" | grep -q 'swift build -c release --package-path'
|
printf '%s\n' "$output" | grep -q 'swift build -c release --package-path'
|
||||||
printf '%s\n' "$output" | grep -q 'launchctl load'
|
printf '%s\n' "$output" | grep -q 'launchctl load'
|
||||||
printf '%s\n' "$output" | grep -q 'restart LemanaVPN.app if running'
|
printf '%s\n' "$output" | grep -q 'restart LemanaVPN.app if running'
|
||||||
|
printf '%s\n' "$output" | grep -q '+ cp .*/bin/vpn-lemanapro.sh '
|
||||||
|
if printf '%s\n' "$output" | grep -q 'curl -fsSL .*raw/branch/main'; then
|
||||||
|
echo "checkout install dry-run should use local files, not remote main" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
esc="$(printf '\033')"
|
esc="$(printf '\033')"
|
||||||
if printf '%s\n' "$output" | grep -q "$esc"; then
|
if printf '%s\n' "$output" | grep -q "$esc"; then
|
||||||
@@ -33,34 +38,135 @@ if command -v expect >/dev/null 2>&1; then
|
|||||||
interactive_output="$(
|
interactive_output="$(
|
||||||
ROOT="$ROOT" expect <<'EXPECT'
|
ROOT="$ROOT" expect <<'EXPECT'
|
||||||
set timeout 30
|
set timeout 30
|
||||||
spawn sh $env(ROOT)/install.sh --dry-run --interactive --without-app --no-shell --no-sudoers
|
spawn sh $env(ROOT)/install.sh --dry-run --interactive --without-app --no-shell --no-sudoers
|
||||||
expect "Как хранить VPN credentials?"
|
expect "Как подключаться по умолчанию?"
|
||||||
expect "Выбор"
|
expect "Выбор"
|
||||||
send "2\r"
|
send "\r"
|
||||||
expect eof
|
expect "Сохранить логин и пароль"
|
||||||
|
send "y\r"
|
||||||
|
expect eof
|
||||||
EXPECT
|
EXPECT
|
||||||
)"
|
)"
|
||||||
|
|
||||||
printf '%s\n' "$interactive_output" | grep -q 'Как хранить VPN credentials?'
|
printf '%s\n' "$interactive_output" | grep -q 'По умолчанию ничего не сохраняется'
|
||||||
printf '%s\n' "$interactive_output" | grep -q '1) Bitwarden'
|
printf '%s\n' "$interactive_output" | grep -q 'Как подключаться по умолчанию?'
|
||||||
printf '%s\n' "$interactive_output" | grep -q '2) macOS Keychain: ввести LDAP password и TOTP seed сейчас'
|
printf '%s\n' "$interactive_output" | grep -q '1) Вводить всё вручную'
|
||||||
printf '%s\n' "$interactive_output" | grep -q '3) macOS Keychain: настрою вручную позже'
|
printf '%s\n' "$interactive_output" | grep -q '2) Запомнить логин и пароль'
|
||||||
printf '%s\n' "$interactive_output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=0 shell=0 app=0 autostart=0'
|
printf '%s\n' "$interactive_output" | grep -q '3) Настроить автоматический вход'
|
||||||
printf '%s\n' "$interactive_output" | grep -q "vpn-lemanapro.sh --configure-keychain"
|
printf '%s\n' "$interactive_output" | grep -q 'Сохранить логин и пароль в macOS Keychain для подстановки при входе?'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q 'Выбран режим: сохранить логин и пароль, одноразовый код вводить вручную'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q '+ .*/vpn-lemanapro.sh --configure-login'
|
||||||
|
if printf '%s\n' "$interactive_output" | grep -Eq 'credential_source|auth_profile|TOTP seed'; then
|
||||||
|
echo "basic interactive wizard should not expose technical credential wording" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
printf '%s\n' "$status_json" | grep -q '"modules":'
|
printf '%s\n' "$status_json" | grep -q '"modules":'
|
||||||
printf '%s\n' "$status_json" | grep -q '"app":'
|
printf '%s\n' "$status_json" | grep -q '"app":'
|
||||||
|
printf '%s\n' "$status_json" | grep -q '"app_menu":{"install_label":"Режим: полностью вручную","configured_mode":"manual","available_modes":\["configured"\],"setup_hint":null}'
|
||||||
|
|
||||||
keychain_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=keychain bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
keychain_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=keychain bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","keychain_ready":false}'
|
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","auth_profile":"manual","login_ready":false,"keychain_ready":false}'
|
||||||
printf '%s\n' "$keychain_status_json" | grep -q '"bitwarden":{"enabled":false'
|
printf '%s\n' "$keychain_status_json" | grep -q '"bitwarden":{"enabled":false'
|
||||||
printf '%s\n' "$keychain_status_json" | grep -q '"touchid":{"enabled":false'
|
printf '%s\n' "$keychain_status_json" | grep -q '"touchid":{"enabled":false'
|
||||||
|
|
||||||
bitwarden_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
bitwarden_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
printf '%s\n' "$bitwarden_status_json" | grep -q '"credentials":{"source":"bitwarden","keychain_ready":false}'
|
printf '%s\n' "$bitwarden_status_json" | grep -q '"credentials":{"source":"bitwarden","auth_profile":"manual","login_ready":false,"keychain_ready":false}'
|
||||||
printf '%s\n' "$bitwarden_status_json" | grep -q '"bitwarden":{"enabled":true'
|
printf '%s\n' "$bitwarden_status_json" | grep -q '"bitwarden":{"enabled":true'
|
||||||
|
|
||||||
|
status_password_bin="$TMP_DIR/status-password-bin"
|
||||||
|
mkdir -p "$status_password_bin"
|
||||||
|
cat > "$status_password_bin/security" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "${1:-}" = "find-generic-password" ]; then
|
||||||
|
account=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
if [ "$1" = "-a" ]; then
|
||||||
|
shift
|
||||||
|
account="${1:-}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
case "$account" in
|
||||||
|
""|totp/*) exit 1 ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
SH
|
||||||
|
chmod +x "$status_password_bin/security"
|
||||||
|
|
||||||
|
status_full_bin="$TMP_DIR/status-full-bin"
|
||||||
|
mkdir -p "$status_full_bin"
|
||||||
|
cat > "$status_full_bin/security" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "${1:-}" = "find-generic-password" ]; then
|
||||||
|
account=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
if [ "$1" = "-a" ]; then
|
||||||
|
shift
|
||||||
|
account="${1:-}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[ -n "$account" ]
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
SH
|
||||||
|
chmod +x "$status_full_bin/security"
|
||||||
|
|
||||||
|
saved_login_missing_status="$(
|
||||||
|
LEMANA_VPN_USERNAME="lemana-status-login-missing" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
|
||||||
|
)"
|
||||||
|
printf '%s\n' "$saved_login_missing_status" | grep -q '"app_menu":{"install_label":"Режим: логин и пароль не настроены","configured_mode":"saved-login","available_modes":\["manual-full"\],"setup_hint":"Настройте в Terminal: vpn --configure-login"}'
|
||||||
|
|
||||||
|
saved_login_ready_status="$(
|
||||||
|
PATH="$status_password_bin:$PATH" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-status-login-ready" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
|
||||||
|
)"
|
||||||
|
printf '%s\n' "$saved_login_ready_status" | grep -q '"app_menu":{"install_label":"Режим: логин и пароль сохранены","configured_mode":"saved-login","available_modes":\["configured","manual-full"\],"setup_hint":null}'
|
||||||
|
|
||||||
|
saved_totp_missing_status="$(
|
||||||
|
LEMANA_VPN_USERNAME="lemana-status-totp-missing" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-totp \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
|
||||||
|
)"
|
||||||
|
printf '%s\n' "$saved_totp_missing_status" | grep -q '"app_menu":{"install_label":"Режим: сохранённые данные не настроены","configured_mode":"saved-totp","available_modes":\["manual-full"\],"setup_hint":"Настройте в Terminal: vpn --configure-keychain"}'
|
||||||
|
|
||||||
|
saved_totp_ready_status="$(
|
||||||
|
PATH="$status_full_bin:$PATH" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-status-totp-ready" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-totp \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
|
||||||
|
)"
|
||||||
|
printf '%s\n' "$saved_totp_ready_status" | grep -q '"app_menu":{"install_label":"Режим: сохранённые данные без автоотправки","configured_mode":"saved-totp","available_modes":\["configured","saved-login","manual-full","auto"\],"setup_hint":null}'
|
||||||
|
|
||||||
|
auto_ready_status="$(
|
||||||
|
PATH="$status_full_bin:$PATH" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-status-auto-ready" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=auto \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --status --json
|
||||||
|
)"
|
||||||
|
printf '%s\n' "$auto_ready_status" | grep -q '"app_menu":{"install_label":"Режим: автоматический вход","configured_mode":"auto","available_modes":\["configured","saved-totp","saved-login","manual-full"\],"setup_hint":null}'
|
||||||
|
personal_ldap='601''03293'
|
||||||
|
if grep -R "$personal_ldap" "$ROOT/bin" "$ROOT/install.sh" "$ROOT/uninstall.sh" "$ROOT/README.md" "$ROOT/templates" "$ROOT/app" "$ROOT/.agents" >/dev/null; then
|
||||||
|
echo "personal LDAP username must not be hardcoded in runtime, installer, app, docs, or agent guidance" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
@@ -70,12 +176,32 @@ grep -q -- '--manual-full' "$ROOT/bin/vpn-lemanapro.sh"
|
|||||||
grep -q -- '--manual-sso' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q -- '--manual-sso' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q 'LEMANA_VPN_AUTOFILL_DISABLE' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
|
grep -q 'LEMANA_VPN_AUTOFILL_TOTP' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q 'vpn-auto' "$ROOT/install.sh"
|
grep -q 'vpn-auto' "$ROOT/install.sh"
|
||||||
grep -q 'vpn-manual' "$ROOT/install.sh"
|
grep -q 'vpn-manual' "$ROOT/install.sh"
|
||||||
grep -q 'vpn-manual-full' "$ROOT/install.sh"
|
grep -q 'vpn-manual-full' "$ROOT/install.sh"
|
||||||
grep -q 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
grep -q 'ForEach(vpnManager.availableLaunchModes)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
grep -q 'connect(mode: .manual)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
grep -q 'vpnManager.installModeLabel' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
grep -q 'connect(mode: .manualFull)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
grep -q 'vpnManager.installSetupHint' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
|
grep -q 'Button("Изменить логин и пароль")' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
|
grep -q 'vpnManager.configureLogin()' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
|
if grep -Fq 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"; then
|
||||||
|
echo "disconnected app menu should not hardcode the auto button" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if grep -Fq 'Button("Подставить только логин и пароль")' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"; then
|
||||||
|
echo "disconnected app menu should not hardcode saved-login button visibility" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q 'app_menu' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'available_modes' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'menuTitle' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'Подключить полностью вручную' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'case "auth_profile":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'case "manual_sso", "auto_sso":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'case "username_required", "username_saved":' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'func configureLogin()' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q -- '--configure-login' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
@@ -172,6 +298,8 @@ grep -q 'script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)' "$f
|
|||||||
grep -q 'new Event("input", {{bubbles: true}})' "$fake_webengine"
|
grep -q 'new Event("input", {{bubbles: true}})' "$fake_webengine"
|
||||||
grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$fake_webengine"
|
grep -q 'LEMANA_VPN_AUTOFILL_CLICK' "$fake_webengine"
|
||||||
grep -q 'os.environ.get("LEMANA_VPN_AUTOFILL_CLICK", "1") != "0"' "$fake_webengine"
|
grep -q 'os.environ.get("LEMANA_VPN_AUTOFILL_CLICK", "1") != "0"' "$fake_webengine"
|
||||||
|
grep -q 'LEMANA_VPN_AUTOFILL_TOTP' "$fake_webengine"
|
||||||
|
grep -q 'rule.fill == "totp"' "$fake_webengine"
|
||||||
if grep -q 'ScriptWorldId.MainWorld' "$fake_webengine"; then
|
if grep -q 'ScriptWorldId.MainWorld' "$fake_webengine"; then
|
||||||
echo "patched auto-fill should keep the original ApplicationWorld behavior" >&2
|
echo "patched auto-fill should keep the original ApplicationWorld behavior" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -224,6 +352,7 @@ set +e
|
|||||||
manual_output="$(
|
manual_output="$(
|
||||||
HOME="$HOME" \
|
HOME="$HOME" \
|
||||||
LEMANA_VPN_USERNAME="$missing_user" \
|
LEMANA_VPN_USERNAME="$missing_user" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||||
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
||||||
)"
|
)"
|
||||||
@@ -232,9 +361,31 @@ set -e
|
|||||||
|
|
||||||
[ "$manual_code" -ne 0 ]
|
[ "$manual_code" -ne 0 ]
|
||||||
printf '%s\n' "$manual_output" | grep -q '"event":"keychain_required"'
|
printf '%s\n' "$manual_output" | grep -q '"event":"keychain_required"'
|
||||||
printf '%s\n' "$manual_output" | grep -q 'vpn --configure-keychain'
|
printf '%s\n' "$manual_output" | grep -q 'vpn --configure-login'
|
||||||
if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then
|
if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then
|
||||||
echo "missing manual credentials should fail before VPN cleanup trap is installed" >&2
|
echo "missing saved login should fail before VPN cleanup trap is installed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
missing_username_home="$TMP_DIR/missing-username-home"
|
||||||
|
mkdir -p "$missing_username_home"
|
||||||
|
set +e
|
||||||
|
missing_username_output="$(
|
||||||
|
HOME="$missing_username_home" \
|
||||||
|
LEMANA_VPN_CONFIG_DIR="$missing_username_home/.config/lemana-vpn" \
|
||||||
|
OPENCONNECT_LITE_CONFIG_DIR="$missing_username_home/.config/openconnect-lite" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
||||||
|
)"
|
||||||
|
missing_username_code=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
[ "$missing_username_code" -ne 0 ]
|
||||||
|
printf '%s\n' "$missing_username_output" | grep -q '"event":"username_required"'
|
||||||
|
printf '%s\n' "$missing_username_output" | grep -q 'LDAP username is missing'
|
||||||
|
if printf '%s\n' "$missing_username_output" | grep -q '"event":"keychain_required"'; then
|
||||||
|
echo "missing username should fail before checking Keychain credentials" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -243,7 +394,17 @@ mkdir -p "$fake_path_bin"
|
|||||||
cat > "$fake_path_bin/security" <<'SH'
|
cat > "$fake_path_bin/security" <<'SH'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
if [ "${1:-}" = "find-generic-password" ]; then
|
if [ "${1:-}" = "find-generic-password" ]; then
|
||||||
exit 0
|
account=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
if [ "$1" = "-a" ]; then
|
||||||
|
shift
|
||||||
|
account="${1:-}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[ -n "$account" ]
|
||||||
|
exit $?
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
SH
|
SH
|
||||||
@@ -257,11 +418,241 @@ cat > "$fake_oc_bin" <<'SH'
|
|||||||
printf 'args=%s\n' "$*"
|
printf 'args=%s\n' "$*"
|
||||||
printf 'autofill_disable=%s\n' "${LEMANA_VPN_AUTOFILL_DISABLE:-}"
|
printf 'autofill_disable=%s\n' "${LEMANA_VPN_AUTOFILL_DISABLE:-}"
|
||||||
printf 'autofill_click=%s\n' "${LEMANA_VPN_AUTOFILL_CLICK:-}"
|
printf 'autofill_click=%s\n' "${LEMANA_VPN_AUTOFILL_CLICK:-}"
|
||||||
|
printf 'autofill_totp=%s\n' "${LEMANA_VPN_AUTOFILL_TOTP:-}"
|
||||||
|
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||||
|
config_file="$config_home/openconnect-lite/config.toml"
|
||||||
|
printf 'xdg_config_home=%s\n' "$config_home"
|
||||||
|
if [ -f "$config_file" ]; then
|
||||||
|
if grep -q '^\[credentials\]' "$config_file"; then
|
||||||
|
printf 'credentials_block=1\n'
|
||||||
|
else
|
||||||
|
printf 'credentials_block=0\n'
|
||||||
|
fi
|
||||||
|
if grep -q 'username = ""' "$config_file"; then
|
||||||
|
printf 'empty_username=1\n'
|
||||||
|
else
|
||||||
|
printf 'empty_username=0\n'
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf 'credentials_block=missing\n'
|
||||||
|
printf 'empty_username=missing\n'
|
||||||
|
fi
|
||||||
} > "$LEMANA_VPN_CAPTURE_LAUNCH"
|
} > "$LEMANA_VPN_CAPTURE_LAUNCH"
|
||||||
exit 0
|
exit 0
|
||||||
SH
|
SH
|
||||||
chmod +x "$fake_oc_bin"
|
chmod +x "$fake_oc_bin"
|
||||||
|
|
||||||
|
fake_missing_keychain_bin="$TMP_DIR/fake-missing-keychain-bin"
|
||||||
|
mkdir -p "$fake_missing_keychain_bin"
|
||||||
|
cat > "$fake_missing_keychain_bin/security" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
exit 1
|
||||||
|
SH
|
||||||
|
chmod +x "$fake_missing_keychain_bin/security"
|
||||||
|
|
||||||
|
default_manual_home="$TMP_DIR/default-manual-home"
|
||||||
|
default_manual_capture="$TMP_DIR/default-manual-capture"
|
||||||
|
mkdir -p "$default_manual_home"
|
||||||
|
mkdir -p "$default_manual_home/.config/openconnect-lite"
|
||||||
|
cat > "$default_manual_home/.config/openconnect-lite/config.toml" <<'TOML'
|
||||||
|
on_disconnect = "true"
|
||||||
|
|
||||||
|
[default_profile]
|
||||||
|
address = "vpn.lemanapro.ru"
|
||||||
|
user_group = ""
|
||||||
|
name = "Default-K"
|
||||||
|
|
||||||
|
[credentials]
|
||||||
|
username = ""
|
||||||
|
TOML
|
||||||
|
default_manual_output="$(
|
||||||
|
HOME="$default_manual_home" \
|
||||||
|
PATH="$fake_missing_keychain_bin:$PATH" \
|
||||||
|
LEMANA_VPN_CONFIG_DIR="$default_manual_home/.config/lemana-vpn" \
|
||||||
|
OPENCONNECT_LITE_CONFIG_DIR="$default_manual_home/.config/openconnect-lite" \
|
||||||
|
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
|
||||||
|
LEMANA_VPN_OC_PYTHON=python3 \
|
||||||
|
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
|
||||||
|
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
|
||||||
|
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/default-manual-patch-backups" \
|
||||||
|
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
|
||||||
|
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/default-manual.log" \
|
||||||
|
LEMANA_VPN_CAPTURE_LAUNCH="$default_manual_capture" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$default_manual_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false'
|
||||||
|
if printf '%s\n' "$default_manual_output" | grep -Eq '"event":"(username_required|keychain_required)"'; then
|
||||||
|
echo "default manual mode should open the browser without saved username or Keychain credentials" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q -- '--browser-display-mode shown' "$default_manual_capture"
|
||||||
|
grep -q '^autofill_disable=1$' "$default_manual_capture"
|
||||||
|
grep -q '^autofill_click=0$' "$default_manual_capture"
|
||||||
|
grep -q '^autofill_totp=0$' "$default_manual_capture"
|
||||||
|
grep -q '^credentials_block=0$' "$default_manual_capture"
|
||||||
|
grep -q '^empty_username=0$' "$default_manual_capture"
|
||||||
|
|
||||||
|
fake_login_keychain_bin="$TMP_DIR/fake-login-keychain-bin"
|
||||||
|
mkdir -p "$fake_login_keychain_bin"
|
||||||
|
cat > "$fake_login_keychain_bin/security" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
if [ "${1:-}" = "find-generic-password" ]; then
|
||||||
|
account=""
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
if [ "$1" = "-a" ]; then
|
||||||
|
shift
|
||||||
|
account="${1:-}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
case "$account" in
|
||||||
|
""|totp/*) exit 1 ;;
|
||||||
|
*) exit 0 ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
SH
|
||||||
|
chmod +x "$fake_login_keychain_bin/security"
|
||||||
|
|
||||||
|
saved_login_capture="$TMP_DIR/saved-login-capture"
|
||||||
|
saved_login_output="$(
|
||||||
|
HOME="$HOME" \
|
||||||
|
PATH="$fake_login_keychain_bin:$PATH" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-saved-login-$$" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
|
||||||
|
LEMANA_VPN_OC_PYTHON=python3 \
|
||||||
|
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
|
||||||
|
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
|
||||||
|
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/saved-login-patch-backups" \
|
||||||
|
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
|
||||||
|
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/saved-login.log" \
|
||||||
|
LEMANA_VPN_CAPTURE_LAUNCH="$saved_login_capture" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$saved_login_output" | grep -q '"event":"manual_sso","auth_profile":"saved-login","autofill":true,"totp":false,"submit":false'
|
||||||
|
if printf '%s\n' "$saved_login_output" | grep -q '"event":"keychain_required"'; then
|
||||||
|
echo "saved-login should not require a TOTP seed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q -- '--browser-display-mode shown' "$saved_login_capture"
|
||||||
|
grep -q '^autofill_disable=0$' "$saved_login_capture"
|
||||||
|
grep -q '^autofill_click=0$' "$saved_login_capture"
|
||||||
|
grep -q '^autofill_totp=0$' "$saved_login_capture"
|
||||||
|
|
||||||
|
saved_totp_capture="$TMP_DIR/saved-totp-capture"
|
||||||
|
saved_totp_output="$(
|
||||||
|
HOME="$HOME" \
|
||||||
|
PATH="$fake_path_bin:$PATH" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-saved-totp-$$" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-totp \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
|
||||||
|
LEMANA_VPN_OC_PYTHON=python3 \
|
||||||
|
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
|
||||||
|
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
|
||||||
|
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/saved-totp-patch-backups" \
|
||||||
|
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
|
||||||
|
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/saved-totp.log" \
|
||||||
|
LEMANA_VPN_CAPTURE_LAUNCH="$saved_totp_capture" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$saved_totp_output" | grep -q '"event":"manual_sso","auth_profile":"saved-totp","autofill":true,"totp":true,"submit":false'
|
||||||
|
grep -q -- '--browser-display-mode shown' "$saved_totp_capture"
|
||||||
|
grep -q '^autofill_disable=0$' "$saved_totp_capture"
|
||||||
|
grep -q '^autofill_click=0$' "$saved_totp_capture"
|
||||||
|
grep -q '^autofill_totp=1$' "$saved_totp_capture"
|
||||||
|
|
||||||
|
manual_full_no_credentials_home="$TMP_DIR/manual-full-no-credentials-home"
|
||||||
|
manual_full_no_credentials_capture="$TMP_DIR/manual-full-no-credentials-capture"
|
||||||
|
mkdir -p "$manual_full_no_credentials_home"
|
||||||
|
mkdir -p "$manual_full_no_credentials_home/.config/openconnect-lite"
|
||||||
|
cat > "$manual_full_no_credentials_home/.config/openconnect-lite/config.toml" <<'TOML'
|
||||||
|
on_disconnect = "true"
|
||||||
|
|
||||||
|
[default_profile]
|
||||||
|
address = "vpn.lemanapro.ru"
|
||||||
|
user_group = ""
|
||||||
|
name = "Default-K"
|
||||||
|
|
||||||
|
[credentials]
|
||||||
|
username = ""
|
||||||
|
TOML
|
||||||
|
manual_full_no_credentials_output="$(
|
||||||
|
HOME="$manual_full_no_credentials_home" \
|
||||||
|
PATH="$fake_missing_keychain_bin:$PATH" \
|
||||||
|
LEMANA_VPN_CONFIG_DIR="$manual_full_no_credentials_home/.config/lemana-vpn" \
|
||||||
|
OPENCONNECT_LITE_CONFIG_DIR="$manual_full_no_credentials_home/.config/openconnect-lite" \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
LEMANA_VPN_OC_BIN="$fake_oc_bin" \
|
||||||
|
LEMANA_VPN_OC_PYTHON=python3 \
|
||||||
|
LEMANA_VPN_WEBENGINE_PROCESS="$fake_webengine" \
|
||||||
|
LEMANA_VPN_AUTHENTICATOR="$fake_authenticator" \
|
||||||
|
LEMANA_VPN_PATCH_BACKUP_DIR="$TMP_DIR/manual-full-no-credentials-patch-backups" \
|
||||||
|
LEMANA_VPN_DNS_CLEANUP="$TMP_DIR/no-dns-cleanup" \
|
||||||
|
LEMANA_VPN_CONNECT_LOG="$TMP_DIR/manual-full-no-credentials.log" \
|
||||||
|
LEMANA_VPN_CAPTURE_LAUNCH="$manual_full_no_credentials_capture" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$manual_full_no_credentials_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false'
|
||||||
|
if printf '%s\n' "$manual_full_no_credentials_output" | grep -Eq '"event":"(username_required|keychain_required)"'; then
|
||||||
|
echo "manual-full should not require saved username or Keychain credentials before opening the browser" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
grep -q -- '--browser-display-mode shown' "$manual_full_no_credentials_capture"
|
||||||
|
grep -q '^autofill_disable=1$' "$manual_full_no_credentials_capture"
|
||||||
|
grep -q '^autofill_click=0$' "$manual_full_no_credentials_capture"
|
||||||
|
grep -q '^autofill_totp=0$' "$manual_full_no_credentials_capture"
|
||||||
|
grep -q '^credentials_block=0$' "$manual_full_no_credentials_capture"
|
||||||
|
grep -q '^empty_username=0$' "$manual_full_no_credentials_capture"
|
||||||
|
|
||||||
|
if command -v expect >/dev/null 2>&1; then
|
||||||
|
first_username_home="$TMP_DIR/first-username-home"
|
||||||
|
mkdir -p "$first_username_home/.config/lemana-vpn" "$first_username_home/.config/openconnect-lite"
|
||||||
|
printf 'LEMANA_VPN_CREDENTIAL_SOURCE="keychain"\nLEMANA_VPN_USE_BITWARDEN="0"\nLEMANA_VPN_USE_TOUCHID="0"\n' > "$first_username_home/.config/lemana-vpn/env"
|
||||||
|
printf 'username = ""\n' > "$first_username_home/.config/openconnect-lite/config.toml"
|
||||||
|
|
||||||
|
FIRST_USERNAME_HOME="$first_username_home" \
|
||||||
|
ROOT="$ROOT" \
|
||||||
|
FAKE_PATH_BIN="$fake_path_bin" \
|
||||||
|
FAKE_OC_BIN="$fake_oc_bin" \
|
||||||
|
FAKE_WEBENGINE="$fake_webengine" \
|
||||||
|
FAKE_AUTHENTICATOR="$fake_authenticator" \
|
||||||
|
MANUAL_FULL_CAPTURE="$manual_full_capture" \
|
||||||
|
TMP_DIR="$TMP_DIR" \
|
||||||
|
expect <<'EXPECT'
|
||||||
|
set timeout 30
|
||||||
|
log_user 0
|
||||||
|
set home $env(FIRST_USERNAME_HOME)
|
||||||
|
spawn env \
|
||||||
|
HOME=$home \
|
||||||
|
PATH=$env(FAKE_PATH_BIN):$env(PATH) \
|
||||||
|
LEMANA_VPN_CONFIG_DIR=$home/.config/lemana-vpn \
|
||||||
|
OPENCONNECT_LITE_CONFIG_DIR=$home/.config/openconnect-lite \
|
||||||
|
LEMANA_VPN_OC_BIN=$env(FAKE_OC_BIN) \
|
||||||
|
LEMANA_VPN_OC_PYTHON=python3 \
|
||||||
|
LEMANA_VPN_WEBENGINE_PROCESS=$env(FAKE_WEBENGINE) \
|
||||||
|
LEMANA_VPN_AUTHENTICATOR=$env(FAKE_AUTHENTICATOR) \
|
||||||
|
LEMANA_VPN_PATCH_BACKUP_DIR=$env(TMP_DIR)/first-username-patch-backups \
|
||||||
|
LEMANA_VPN_DNS_CLEANUP=$env(TMP_DIR)/no-dns-cleanup \
|
||||||
|
LEMANA_VPN_CONNECT_LOG=$env(TMP_DIR)/first-username.log \
|
||||||
|
LEMANA_VPN_CAPTURE_LAUNCH=$env(MANUAL_FULL_CAPTURE) \
|
||||||
|
bash $env(ROOT)/bin/vpn-lemanapro.sh --manual --json
|
||||||
|
expect "Corporate LDAP username:"
|
||||||
|
send "lemana-first-user\r"
|
||||||
|
expect eof
|
||||||
|
EXPECT
|
||||||
|
|
||||||
|
grep -q 'LEMANA_VPN_USERNAME="lemana-first-user"' "$first_username_home/.config/lemana-vpn/env"
|
||||||
|
grep -q 'username = "lemana-first-user"' "$first_username_home/.config/openconnect-lite/config.toml"
|
||||||
|
fi
|
||||||
|
|
||||||
manual_full_output="$(
|
manual_full_output="$(
|
||||||
HOME="$HOME" \
|
HOME="$HOME" \
|
||||||
PATH="$fake_path_bin:$PATH" \
|
PATH="$fake_path_bin:$PATH" \
|
||||||
@@ -278,17 +669,18 @@ manual_full_output="$(
|
|||||||
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
|
bash "$ROOT/bin/vpn-lemanapro.sh" --manual-full --json
|
||||||
)"
|
)"
|
||||||
|
|
||||||
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","autofill":false,"submit":false'
|
printf '%s\n' "$manual_full_output" | grep -q '"event":"manual_sso","auth_profile":"manual","autofill":false,"submit":false'
|
||||||
grep -q -- '--browser-display-mode shown' "$manual_full_capture"
|
grep -q -- '--browser-display-mode shown' "$manual_full_capture"
|
||||||
grep -q '^autofill_disable=1$' "$manual_full_capture"
|
grep -q '^autofill_disable=1$' "$manual_full_capture"
|
||||||
grep -q '^autofill_click=0$' "$manual_full_capture"
|
grep -q '^autofill_click=0$' "$manual_full_capture"
|
||||||
|
grep -q '^autofill_totp=0$' "$manual_full_capture"
|
||||||
|
|
||||||
fake_oc_python="$TMP_DIR/fake-oc-python"
|
fake_oc_python="$TMP_DIR/fake-oc-python"
|
||||||
captured_totp="$TMP_DIR/captured-totp"
|
captured_totp="$TMP_DIR/captured-totp"
|
||||||
cat > "$fake_oc_python" <<'SH'
|
cat > "$fake_oc_python" <<'SH'
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
cat >/dev/null
|
cat >/dev/null
|
||||||
printf '%s\n' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
|
printf '%s' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
|
||||||
SH
|
SH
|
||||||
chmod +x "$fake_oc_python"
|
chmod +x "$fake_oc_python"
|
||||||
|
|
||||||
@@ -305,6 +697,24 @@ configure_output="$(
|
|||||||
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
|
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
|
||||||
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
|
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
|
||||||
|
|
||||||
|
captured_login_totp="$TMP_DIR/captured-login-totp"
|
||||||
|
configure_login_output="$(
|
||||||
|
printf 'ldap-password\n' |
|
||||||
|
HOME="$HOME" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-login-$$" \
|
||||||
|
LEMANA_VPN_AUTH_PROFILE=saved-login \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
LEMANA_VPN_OC_PYTHON="$fake_oc_python" \
|
||||||
|
LEMANA_VPN_CAPTURE_TOTP="$captured_login_totp" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --configure-login
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$configure_login_output" | grep -q 'Login and password are ready in macOS Keychain'
|
||||||
|
if [ -s "$captured_login_totp" ]; then
|
||||||
|
echo "configure-login must not store a TOTP seed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
fake_pwd="$TMP_DIR/fake-pwd"
|
fake_pwd="$TMP_DIR/fake-pwd"
|
||||||
mkdir -p "$fake_pwd/bin"
|
mkdir -p "$fake_pwd/bin"
|
||||||
printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh"
|
printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh"
|
||||||
|
|||||||
17
uninstall.sh
17
uninstall.sh
@@ -3,10 +3,17 @@ set -eu
|
|||||||
|
|
||||||
INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
|
INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
|
||||||
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
|
||||||
|
CONFIG_FILE="$CONFIG_DIR/env"
|
||||||
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
||||||
OC_VENV="${LEMANA_VPN_OC_VENV:-$HOME/.local/pipx/venvs/openconnect-lite}"
|
OC_VENV="${LEMANA_VPN_OC_VENV:-$HOME/.local/pipx/venvs/openconnect-lite}"
|
||||||
DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}"
|
DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}"
|
||||||
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
|
||||||
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONFIG_FILE"
|
||||||
|
fi
|
||||||
|
[ "${_ENV_LEMANA_VPN_USERNAME%"${_ENV_LEMANA_VPN_USERNAME#?}"}" = "x" ] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME#?}"
|
||||||
|
USERNAME="${LEMANA_VPN_USERNAME:-}"
|
||||||
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
||||||
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
|
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
|
||||||
DRY_RUN=0
|
DRY_RUN=0
|
||||||
@@ -203,15 +210,23 @@ remove_keychain_entries() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log_step "Удаляю VPN-записи из macOS Keychain"
|
log_step "Удаляю VPN-записи из macOS Keychain"
|
||||||
|
if [ -n "$USERNAME" ]; then
|
||||||
log_detail "Удаляются только записи openconnect-lite для пользователя $USERNAME и кэш Bitwarden Lemana VPN."
|
log_detail "Удаляются только записи openconnect-lite для пользователя $USERNAME и кэш Bitwarden Lemana VPN."
|
||||||
|
else
|
||||||
|
log_detail "LDAP username не задан; удаляется только кэш Bitwarden Lemana VPN."
|
||||||
|
fi
|
||||||
if [ "$DRY_RUN" -eq 1 ]; then
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
if [ -n "$USERNAME" ]; then
|
||||||
run security delete-generic-password -s openconnect-lite -a "$USERNAME"
|
run security delete-generic-password -s openconnect-lite -a "$USERNAME"
|
||||||
run security delete-generic-password -s openconnect-lite -a "totp/$USERNAME"
|
run security delete-generic-password -s openconnect-lite -a "totp/$USERNAME"
|
||||||
|
fi
|
||||||
run security delete-generic-password -s vpn-lemanapro -a bw-session
|
run security delete-generic-password -s vpn-lemanapro -a bw-session
|
||||||
run security delete-generic-password -s vpn-lemanapro -a bw-master
|
run security delete-generic-password -s vpn-lemanapro -a bw-master
|
||||||
else
|
else
|
||||||
|
if [ -n "$USERNAME" ]; then
|
||||||
security delete-generic-password -s openconnect-lite -a "$USERNAME" >/dev/null 2>&1 || true
|
security delete-generic-password -s openconnect-lite -a "$USERNAME" >/dev/null 2>&1 || true
|
||||||
security delete-generic-password -s openconnect-lite -a "totp/$USERNAME" >/dev/null 2>&1 || true
|
security delete-generic-password -s openconnect-lite -a "totp/$USERNAME" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
security delete-generic-password -s vpn-lemanapro -a bw-session >/dev/null 2>&1 || true
|
security delete-generic-password -s vpn-lemanapro -a bw-session >/dev/null 2>&1 || true
|
||||||
security delete-generic-password -s vpn-lemanapro -a bw-master >/dev/null 2>&1 || true
|
security delete-generic-password -s vpn-lemanapro -a bw-master >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user