@ksl101/vulnreaper 0.1.0-beta.1 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ksl101/vulnreaper might be problematic. Click here for more details.

package/README.md CHANGED
@@ -1,200 +1,221 @@
1
- # VulnReaper — Plateforme d'analyse statique pour la sécurité Android
2
-
3
- **VulnReaper** est une plateforme d'analyse statique **offline-first** destinée au
4
- bug-bounty et à la recherche sécurité sur les **jeux Android** (APK natifs,
5
- Unity et IL2CPP). Elle tourne en **pur Python** (bibliothèque standard
6
- uniquement), sans dépendance runtime, et est **conçue pour Windows en priorité**,
7
- tout en restant compatible POSIX.
8
-
9
- Elle transforme un APK en un rapport exploitable : surface d'attaque
10
- (manifest), chaînes et secrets, endpoints/hosts, bibliothèques natives et
11
- métadonnées IL2CPP — le tout corrélé par un moteur de règles JSON.
12
-
13
- ---
14
-
15
- ## Points clés
16
-
17
- - **Aucune dépendance runtime** : uniquement la bibliothèque standard.
18
- - **Jamais de résultats fabriqués** : un outil externe absent est marqué
19
- `missing` et l'étape correspondante est `skipped`, jamais simulée.
20
- - **Windows-first** : encodage UTF-8, couleurs ANSI (VT), gestion des chemins et
21
- des sous-processus adaptée à Windows.
22
- - **Analyses natives sans décompilateur** : parseurs intégrés DEX, AXML
23
- (manifest compilé), ELF et `global-metadata.dat` (IL2CPP v24–v31).
24
- - **Rapports** JSON, Markdown et HTML prêts à joindre à un rapport de bug bounty.
25
-
26
- ---
27
-
28
- ## Installation
29
-
30
- ### Bêta publique (npm)
31
-
32
- ```sh
33
- npm install -g @ksl101/vulnreaper@beta
34
- vr version
35
- ```
36
-
37
- Prérequis : Node.js ≥ 18 et Python ≥ 3.10 (le paquet embarque le cœur Python
38
- complet, exécuté par le lanceur `vr` / `vulnreaper`).
39
-
40
- VulnReaper est sous licence nominative : activez votre clé avec
41
- `vr login --key VR-...`, puis consultez `vr whoami`. Les commandes `scan` et
42
- `report` exigent une permission (`scan.run`, `report.export`) ; les permissions
43
- `recon.run` et `audit.run` sont réservées aux futures commandes `recon`/`audit`.
44
- Voir **[docs/BETA_TESTERS.md](docs/BETA_TESTERS.md)** pour le guide complet des
45
- testeurs.
46
-
47
- ### Depuis les sources
48
-
49
- Aucune installation n'est requise pour utiliser VulnReaper depuis les sources.
50
-
51
- ```bat
52
- :: Windows (invité de commandes)
53
- vulnreaper.bat doctor
54
- vulnreaper.bat scan C:\path\to\game.apk --profile apk-static
55
- ```
56
-
57
- ```sh
58
- # Git Bash / Linux / macOS
59
- ./vulnreaper.sh doctor
60
- ./vulnreaper.sh scan /path/to/game.apk --profile apk-static
61
- ```
62
-
63
- Ou directement via Python (à la racine du dépôt) :
64
-
65
- ```bat
66
- python -m vulnreaper doctor
67
- python -m vulnreaper scan C:\path\to\game.apk
68
- ```
69
-
70
- Installation optionnelle comme commande globale :
71
-
72
- ```sh
73
- pip install -e .
74
- vulnreaper --version
75
- ```
76
-
77
- ---
78
-
79
- ## Commandes
80
-
81
- | Commande | Description |
82
- | --- | --- |
83
- | `vulnreaper doctor` | Diagnostic de l'environnement et des outils externes |
84
- | `vulnreaper version` | Affiche la version |
85
- | `vulnreaper profiles` | Liste les profils de scan |
86
- | `vulnreaper rules [--json]` | Liste les règles chargées |
87
- | `vulnreaper tools [--missing\|--available] [--json]` | Inspecte les outils externes |
88
- | `vulnreaper project create\|list\|show\|delete <nom>` | Gère les projets/workspaces |
89
- | `vulnreaper scan <apk> [--profile P] [--project N] [--json]` | Lance un scan |
90
- | `vulnreaper runs [--project N]` | Liste les runs |
91
- | `vulnreaper findings [--run ID] [--severity S]` | Affiche les findings |
92
- | `vulnreaper report --run ID` | Chemin des rapports générés |
93
- | `vulnreaper config show\|init` | Affiche / initialise la configuration |
94
- | `vulnreaper login --key <VR-…> [--base-url URL]` | Active une licence et ouvre une session |
95
- | `vulnreaper logout` | Ferme la session locale |
96
- | `vulnreaper whoami [--json]` | Affiche licence, rôle, statut, permissions |
97
- | `vulnreaper admin license\|device\|audit …` | Administration des licences/appareils (rôle `admin`) |
98
- | `vulnreaper ui` | Shell interactif (REPL conversationnel) |
99
-
100
- Sans commande, `vulnreaper` ouvre le shell interactif (`ui`).
101
-
102
- ---
103
-
104
- ## Profils de scan
105
-
106
- | Profil | Usage |
107
- | --- | --- |
108
- | `apk-static` *(défaut)* | Analyse statique complète : import, extraction, manifest, dex, natif, Unity, IL2CPP, assets, endpoints, scanners, rapport |
109
- | `quick` | Version rapide sans extraction ni décompilation |
110
- | `android-full` | Ajoute apktool, Il2CppDumper et jadx quand ils sont présents |
111
- | `il2cpp` | Orienté Unity/IL2CPP |
112
- | `recon` | Reconnaissance endpoints/hosts uniquement |
113
- | `device` | Énumération des appareils adb |
114
- | `report-only` | Régénère un rapport à partir de données existantes |
115
-
116
- Lister les profils : `vulnreaper profiles`.
117
-
118
- ---
119
-
120
- ## Outils externes (optionnels)
121
-
122
- VulnReaper fonctionne sans eux ; s'ils sont présents, il en tire parti :
123
-
124
- | Outil | Rôle |
125
- | --- | --- |
126
- | `adb` | Énumération d'appareils / interaction device |
127
- | `apktool` | Décodage des ressources |
128
- | `jadx` | Décompilation Java |
129
- | `apksigner` | Vérification de signature |
130
- | `aapt` | Inspection d'APK |
131
- | `frida` | Instrumentation dynamique |
132
- | `il2cppdumper` | Dump IL2CPP (`.cs` reconstruits) |
133
- | `cpp2il` | Reconstruction IL2CPP alternative |
134
- | `ghidra` | Analyse native avancée |
135
-
136
- Statut : `vulnreaper tools`. Chemins explicites et répertoires de recherche se
137
- définissent dans la configuration (`vulnreaper config show`).
138
-
139
- ---
140
-
141
- ## Structure du dépôt
142
-
143
- ```
144
- vulnreaper/ paquet principal
145
- cli/ point d'entrée, shell interactif (REPL), thème + composants
146
- formats/ parseurs strings, AXML, DEX, ELF, métadonnées IL2CPP
147
- android/ import APK, modèle manifest, analyse dex/natif, adb
148
- unity/ il2cpp/ détection moteur + adaptateurs de dump
149
- web/ découverte et classification d'endpoints
150
- scanners/ moteur de règles + scanners (secrets, crypto, réseau, natif)
151
- findings/ modèle Finding + rapports JSON/MD/HTML
152
- rules/ packs de règles JSON
153
- config.py workspace.py database.py project.py pipeline.py engine.py tools.py
154
- tests/ suite pytest + fixtures synthétiques + smoke end-to-end
155
- docs/ architecture, règles, utilisation
156
- ```
157
-
158
- ---
159
-
160
- ## Tests
161
-
162
- ```bat
163
- python -m pytest tests/ -q :: cœur Python + UI (164 tests)
164
- python tests/smoke.py :: scan end-to-end d'un APK synthétique
165
- cd server && npm test :: API de licence (11 tests, store mémoire)
166
- cd server && VULNREAPER_E2E=1 npm test :: + E2E CLI ↔ API
167
- ```
168
-
169
- Le smoke construit un APK structurellement valide (manifest AXML compilé, DEX,
170
- `.so` natif, `global-metadata.dat`) dans un dossier temporaire, exécute le profil
171
- `quick` et vérifie que le run se termine, écrit les 3 formats de rapport et
172
- produit findings et endpoints.
173
-
174
- Le test E2E inter-stack pilote réellement la CLI contre l'API de licence
175
- (activation, `whoami`, gating, révocation). Détail de la couverture et
176
- prérequis : **[docs/TESTING.md](docs/TESTING.md)**.
177
-
178
- ---
179
-
180
- ## Documentation
181
-
182
- - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — conception et flux de données
183
- - [`docs/USAGE.md`](docs/USAGE.md) — prise en main et workflow bug-bounty
184
- - [`docs/UI.md`](docs/UI.md) — interface CLI/REPL (thème, composants, raccourcis)
185
- - [`docs/RULES.md`](docs/RULES.md) — packs de règles et écriture de règles
186
- - [`docs/BETA_TESTERS.md`](docs/BETA_TESTERS.md) — guide des testeurs bêta (npm, licence, gating)
187
- - [`docs/TESTING.md`](docs/TESTING.md) — stratégie et couverture des tests
188
-
189
- ---
190
-
191
- ## Licence et usage
192
-
193
- > Distribution sous licence nominative propriétaire. L'utilisation du logiciel
194
- > vaut acceptation du [contrat de licence utilisateur final (EULA)](LICENSE).
195
- > Le guide d'activation et le détail des permissions se trouvent dans
196
- > [docs/BETA_TESTERS.md](docs/BETA_TESTERS.md).
197
-
198
- Outil destiné aux professionnels autorisés. Ne scannez que des applications que
199
- vous êtes autorisé à tester (vos propres cibles, programmes de bug bounty, ou
200
- cibles explicitement autorisées).
1
+ # VulnReaper — Security platform for research and bug-bounty
2
+
3
+ **VulnReaper** is an **offline-first** security-analysis platform for bug-bounty
4
+ and security research. It is built around a **target-agnostic core** (projects,
5
+ runs, findings, rule engine, JSON/Markdown/HTML reports, licence/account) and
6
+ around **per-target modules**:
7
+ the **Android** module (APK/APKS/XAPK, Unity and IL2CPP) is available today,
8
+ while web, Windows and game-engine project targets are already detected and
9
+ routed, with their engines still to be integrated.
10
+
11
+ It runs on **pure Python** (standard library only), with no runtime dependency,
12
+ and is **Windows-first**, while remaining POSIX-compatible.
13
+
14
+ On an Android target, it turns an APK into an actionable report: attack surface
15
+ (manifest), strings and secrets, endpoints/hosts, native libraries and IL2CPP
16
+ metadata — all correlated by a JSON rule engine.
17
+
18
+ ---
19
+
20
+ ## Highlights
21
+
22
+ - **No runtime dependency**: standard library only.
23
+ - **Never fabricate results**: a missing external tool is marked `missing` and
24
+ its step is `skipped`, never simulated.
25
+ - **Windows-first**: UTF-8 encoding, ANSI (VT) colours, Windows-aware path and
26
+ subprocess handling.
27
+ - **Native analysis without a decompiler**: built-in DEX, AXML (compiled
28
+ manifest), ELF and `global-metadata.dat` (IL2CPP v24–v31) parsers.
29
+ - **Reports** in JSON, Markdown and HTML, ready to attach to a bug-bounty report.
30
+
31
+ ---
32
+
33
+ ## Core and per-target modules
34
+
35
+ | Element | Scope |
36
+ | --- | --- |
37
+ | **Core** (target-agnostic) | Workspaces/projects, runs, findings, JSON rule engine, JSON/Markdown/HTML reports, licence/account management — **always available**. |
38
+ | **Android module** | APK/APKS/XAPK: manifest, DEX, native, Unity/IL2CPP, assets, endpoints — **available**. |
39
+ | **Web module** | http(s) URL: detected and routed; engine coming. |
40
+ | **Windows module** | EXE/DLL/SYS/MSI: detected and routed; engine coming. |
41
+ | **Unity / Unreal module** | Project folders: detected and routed; engines coming. |
42
+
43
+ A target whose module is not yet integrated is **honestly refused**
44
+ ("not supported yet" message): VulnReaper never simulates a scan.
45
+
46
+ ---
47
+
48
+ ## Installation
49
+
50
+ ### Public beta (npm)
51
+
52
+ ```sh
53
+ npm install -g @ksl101/vulnreaper@beta
54
+ vr version
55
+ ```
56
+
57
+ Requirements: Node.js ≥ 18 and Python ≥ 3.10 (the package embeds the complete
58
+ Python core, executed by the `vr` / `vulnreaper` launcher).
59
+
60
+ VulnReaper is nominative-licence software: activate your key with
61
+ `vr login --key VR-...`, then check `vr whoami`. The `scan` and
62
+ `report` commands require a permission (`scan.run`, `report.export`); the
63
+ `recon.run` and `audit.run` permissions are reserved for the future `recon`/`audit`
64
+ commands.
65
+ See **[docs/BETA_TESTERS.md](docs/BETA_TESTERS.md)** for the full tester guide.
66
+
67
+ ### From source
68
+
69
+ No installation is required to use VulnReaper from source.
70
+
71
+ ```bat
72
+ :: Windows (command prompt)
73
+ vulnreaper.bat doctor
74
+ vulnreaper.bat scan C:\path\to\game.apk --profile apk-static
75
+ ```
76
+
77
+ ```sh
78
+ # Git Bash / Linux / macOS
79
+ ./vulnreaper.sh doctor
80
+ ./vulnreaper.sh scan /path/to/game.apk --profile apk-static
81
+ ```
82
+
83
+ Or directly through Python (at the repository root):
84
+
85
+ ```bat
86
+ python -m vulnreaper doctor
87
+ python -m vulnreaper scan C:\path\to\game.apk
88
+ ```
89
+
90
+ Optional installation as a global command:
91
+
92
+ ```sh
93
+ pip install -e .
94
+ vulnreaper --version
95
+ ```
96
+
97
+ ---
98
+
99
+ ## Commands
100
+
101
+ | Command | Description |
102
+ | --- | --- |
103
+ | `vulnreaper doctor` | Diagnose the environment and external tools |
104
+ | `vulnreaper version` | Print the version |
105
+ | `vulnreaper profiles` | List scan profiles |
106
+ | `vulnreaper rules [--json]` | List loaded rules |
107
+ | `vulnreaper tools [--missing\|--available] [--json]` | Inspect external tools |
108
+ | `vulnreaper project create\|list\|show\|delete <name>` | Manage projects/workspaces |
109
+ | `vulnreaper scan <target> [--profile P] [--project N] [--json]` | Analyze a security target (APK, URL, EXE, project) |
110
+ | `vulnreaper runs [--project N]` | List runs |
111
+ | `vulnreaper findings [--run ID] [--severity S]` | Show findings |
112
+ | `vulnreaper report --run ID` | Path of the generated reports |
113
+ | `vulnreaper config show\|init` | Show / initialise the configuration |
114
+ | `vulnreaper login --key <VR-…> [--base-url URL]` | Activate a licence and open a session |
115
+ | `vulnreaper logout` | Close the local session |
116
+ | `vulnreaper whoami [--json]` | Show licence, role, status, permissions |
117
+ | `vulnreaper admin license\|device\|audit …` | Licence/device administration (`admin` role) |
118
+ | `vulnreaper self-update [--check] [--yes] [--force] [--json]` | Update VulnReaper itself (npm `@beta`) |
119
+ | `vulnreaper ui` | Interactive shell (conversational REPL) |
120
+
121
+ With no command, `vulnreaper` opens the interactive shell (`ui`).
122
+
123
+ ---
124
+
125
+ ## Scan profiles
126
+
127
+ | Profile | Usage |
128
+ | --- | --- |
129
+ | `apk-static` *(default)* | Full static analysis: import, extraction, manifest, dex, native, Unity, IL2CPP, assets, endpoints, scanners, report |
130
+ | `quick` | Fast version without extraction or decompilation |
131
+ | `android-full` | Adds apktool, Il2CppDumper and jadx when present |
132
+ | `il2cpp` | Unity/IL2CPP-oriented |
133
+ | `recon` | Endpoint/host reconnaissance only |
134
+ | `device` | Enumerate adb devices |
135
+ | `report-only` | Regenerate a report from existing data |
136
+
137
+ List profiles: `vulnreaper profiles`.
138
+
139
+ ---
140
+
141
+ ## External tools (optional)
142
+
143
+ VulnReaper works without them; when present, it takes advantage of them:
144
+
145
+ | Tool | Role |
146
+ | --- | --- |
147
+ | `adb` | Device enumeration / device interaction |
148
+ | `apktool` | Resource decoding |
149
+ | `jadx` | Java decompilation |
150
+ | `apksigner` | Signature verification |
151
+ | `aapt` | APK inspection |
152
+ | `frida` | Dynamic instrumentation |
153
+ | `il2cppdumper` | IL2CPP dump (reconstructed `.cs`) |
154
+ | `cpp2il` | Alternative IL2CPP reconstruction |
155
+ | `ghidra` | Advanced native analysis |
156
+
157
+ Status: `vulnreaper tools`. Explicit paths and search directories are
158
+ defined in the configuration (`vulnreaper config show`).
159
+
160
+ ---
161
+
162
+ ## Repository layout
163
+
164
+ ```
165
+ vulnreaper/ main package
166
+ cli/ entry point, interactive shell (REPL), theme + components
167
+ formats/ strings, AXML, DEX, ELF, IL2CPP metadata parsers
168
+ android/ APK import, manifest model, dex/native analysis, adb
169
+ unity/ il2cpp/ engine detection + dump adapters
170
+ web/ endpoint discovery and classification
171
+ scanners/ rule engine + scanners (secrets, crypto, network, native)
172
+ findings/ Finding model + JSON/MD/HTML reports
173
+ rules/ JSON rule packs
174
+ config.py workspace.py database.py project.py pipeline.py engine.py tools.py
175
+ tests/ pytest suite + synthetic fixtures + end-to-end smoke
176
+ docs/ architecture, rules, usage
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Tests
182
+
183
+ ```bat
184
+ python -m pytest tests/ -q :: Python core + UI
185
+ python tests/smoke.py :: end-to-end scan of a synthetic APK
186
+ cd server && npm test :: licence API (in-memory store)
187
+ cd server && VULNREAPER_E2E=1 npm test :: + CLI ↔ API E2E
188
+ ```
189
+
190
+ The smoke builds a structurally valid APK (compiled AXML manifest, DEX,
191
+ native `.so`, `global-metadata.dat`) in a temporary folder, runs the `quick`
192
+ profile and checks that the run completes, writes the 3 report formats and
193
+ produces findings and endpoints.
194
+
195
+ The cross-stack E2E test actually drives the CLI against the licence API
196
+ (activation, `whoami`, gating, revocation). Coverage details and
197
+ prerequisites: **[docs/TESTING.md](docs/TESTING.md)**.
198
+
199
+ ---
200
+
201
+ ## Documentation
202
+
203
+ - [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — design and data flow
204
+ - [`docs/USAGE.md`](docs/USAGE.md) — getting started and bug-bounty workflow
205
+ - [`docs/UI.md`](docs/UI.md) — CLI/REPL interface (theme, components, shortcuts)
206
+ - [`docs/RULES.md`](docs/RULES.md) — rule packs and writing rules
207
+ - [`docs/BETA_TESTERS.md`](docs/BETA_TESTERS.md) — beta tester guide (npm, licence, gating)
208
+ - [`docs/TESTING.md`](docs/TESTING.md) — test strategy and coverage
209
+
210
+ ---
211
+
212
+ ## Licence and usage
213
+
214
+ > Distributed under a proprietary nominative licence. Using the software
215
+ > constitutes acceptance of the [end-user licence agreement (EULA)](LICENSE).
216
+ > The activation guide and the details of the permissions are in
217
+ > [docs/BETA_TESTERS.md](docs/BETA_TESTERS.md).
218
+
219
+ Tool intended for authorised professionals. Only scan applications you are
220
+ authorised to test (your own targets, bug-bounty programs, or explicitly
221
+ authorised targets).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ksl101/vulnreaper",
3
- "version": "0.1.0-beta.1",
4
- "description": "VulnReaper - authorised security auditing & bug-bounty CLI for Android/Unity/IL2CPP game packages",
3
+ "version": "0.1.0-beta.2",
4
+ "description": "VulnReaper - authorised security-auditing & bug-bounty CLI with a target-agnostic core and per-target modules (Android today; web, Windows and game-engine projects detected next)",
5
5
  "keywords": [
6
6
  "security",
7
7
  "bug-bounty",
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
 
16
16
  __all__ = ["__version__", "PRODUCT"]
17
17
 
18
- __version__ = "0.1.0-beta.1"
18
+ __version__ = "0.1.0-beta.2"
19
19
  PRODUCT = "VulnReaper"
20
20
 
21
21
 
@@ -0,0 +1,140 @@
1
+ """Account helpers shared by the CLI entrypoint and the REPL.
2
+
3
+ A VulnReaper *account* is the username + subscription tag + free-form profile
4
+ blob carried on the license view returned by the licensing API. These helpers
5
+ keep the interpretation of that view in one place and tolerate both the
6
+ snake_case the API emits and the camelCase the Node store uses internally.
7
+
8
+ The username is deliberately **not** a credential: authentication stays with
9
+ the license key / server session. The username only identifies the tester,
10
+ personalises the interface and enables administration/search.
11
+
12
+ Human-facing expiry wording is produced through the i18n catalog so it follows
13
+ the active interface language; the underlying arithmetic (seconds until
14
+ ``expiresAt``) is language-independent.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from datetime import datetime, timezone
20
+
21
+ from ..i18n import t
22
+
23
+ # Must stay in sync with ``USERNAME_RE`` in the licensing server.
24
+ USERNAME_RE = re.compile(r"^[A-Za-z0-9_-]{3,32}$")
25
+ # Static English fallback (kept for callers importing the constant). Prefer
26
+ # :func:`username_rule` so the hint tracks the active language.
27
+ USERNAME_RULE = "3-32 characters (letters, digits, underscore or hyphen)"
28
+
29
+
30
+ def username_rule() -> str:
31
+ """The username policy hint, localised."""
32
+ return t("account.username_rule", default=USERNAME_RULE)
33
+
34
+
35
+ def username(lic: dict | None) -> str:
36
+ """The account username, or '' when it has not been chosen yet."""
37
+ return str((lic or {}).get("username") or "").strip()
38
+
39
+
40
+ def tag(lic: dict | None) -> str:
41
+ """The subscription tag, upper-cased, defaulting to BETA."""
42
+ raw = (lic or {}).get("subscriptionTag") or (lic or {}).get("subscription_tag") or "BETA"
43
+ return str(raw).upper()
44
+
45
+
46
+ def expires_at(lic: dict | None):
47
+ return (lic or {}).get("expiresAt") or (lic or {}).get("expires_at")
48
+
49
+
50
+ def _expiry_datetime(lic: dict | None) -> datetime | None:
51
+ """Parse ``expiresAt`` into an aware UTC datetime, or None when absent."""
52
+ value = expires_at(lic)
53
+ if not value:
54
+ return None
55
+ try:
56
+ when = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
57
+ except ValueError:
58
+ return None
59
+ if when.tzinfo is None:
60
+ when = when.replace(tzinfo=timezone.utc)
61
+ return when
62
+
63
+
64
+ def _seconds_left(lic: dict | None, now: datetime | None = None) -> float | None:
65
+ when = _expiry_datetime(lic)
66
+ if when is None:
67
+ return None
68
+ now = now or datetime.now(timezone.utc)
69
+ return (when - now).total_seconds()
70
+
71
+
72
+ def days_left(lic: dict | None) -> int | None:
73
+ """Whole days until expiry, or None when the license never expires."""
74
+ seconds = _seconds_left(lic)
75
+ if seconds is None:
76
+ return None
77
+ return max(0, int(seconds // 86400))
78
+
79
+
80
+ def expiry_phrase(lic: dict | None, now: datetime | None = None) -> str:
81
+ """The value form for tables/cards, without the verb.
82
+
83
+ Examples: ``never``, ``expired``, ``in 42 minutes``, ``in 18 hours``,
84
+ ``in 27 days``. Everything is derived from ``expiresAt`` — nothing
85
+ time-relative is ever persisted.
86
+ """
87
+ seconds = _seconds_left(lic, now)
88
+ if seconds is None:
89
+ return t("expiry.never")
90
+ if seconds <= 0:
91
+ return t("expiry.expired")
92
+ if seconds < 60:
93
+ return t("expiry.less_than_minute")
94
+ if seconds < 3600:
95
+ return t("expiry.minutes", count=int(seconds // 60))
96
+ if seconds < 86400:
97
+ return t("expiry.hours", count=int(seconds // 3600))
98
+ return t("expiry.days", count=int(seconds // 86400))
99
+
100
+
101
+ def expiry_text(lic: dict | None, now: datetime | None = None) -> str:
102
+ """A full, human sentence describing the remaining lifetime.
103
+
104
+ Examples: ``never expires``, ``expired``, ``expires in 42 minutes``,
105
+ ``expires in 18 hours``, ``expires in 27 days``.
106
+ """
107
+ seconds = _seconds_left(lic, now)
108
+ if seconds is None:
109
+ return t("expiry.text_never")
110
+ if seconds <= 0:
111
+ return t("expiry.text_expired")
112
+ return t("expiry.text", phrase=expiry_phrase(lic, now))
113
+
114
+
115
+ def expiry_short(lic: dict | None, now: datetime | None = None) -> str:
116
+ """A compact expiry label for tables: ``27d`` / ``18h`` / ``42m`` /
117
+ ``never`` / ``expired``."""
118
+ seconds = _seconds_left(lic, now)
119
+ if seconds is None:
120
+ return t("expiry.short_never")
121
+ if seconds <= 0:
122
+ return t("expiry.short_expired")
123
+ if seconds < 3600:
124
+ return t("expiry.short_minutes", count=int(seconds // 60))
125
+ if seconds < 86400:
126
+ return t("expiry.short_hours", count=int(seconds // 3600))
127
+ return t("expiry.short_days", count=int(seconds // 86400))
128
+
129
+
130
+ def devices_label(lic: dict | None, used: int | None = None) -> str:
131
+ """``used/max`` device usage, tolerating snake_case and camelCase."""
132
+ max_devices = (lic or {}).get("maxDevices")
133
+ if max_devices is None:
134
+ max_devices = (lic or {}).get("max_devices")
135
+ max_devices = max_devices if max_devices is not None else "?"
136
+ return f"{used if used is not None else '?'}/{max_devices}"
137
+
138
+
139
+ def valid_username(name: str) -> bool:
140
+ return bool(USERNAME_RE.match((name or "").strip()))