From e612c751a8bb4bcdf36f0242ddec7a8c8976aa0a Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 13 May 2026 16:54:30 +0000 Subject: [PATCH] docs(license): document activation flow, tier system, dev bypass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - USER-GUIDE EN + ES gain a §0 "First launch — activation" section covering paid blob activation, 1-year trial, renewal, file location, and device-swap. - REQUIREMENTS §17a "Licensing" — storage path, activation model, lifetime, tier list, dev bypass env var. Test count: 1995. - DEVELOPER gains a "Licensing" recipe in the Extension recipes section: public API, feature-flag add, tier add, minting via the creator-only script. - DECISIONS §9b — log the offline-HMAC choice with the threat-model trade-off (motivated piracy not stopped; honor-system + 30-day refund covers casual sharing). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/DECISIONS.md | 27 +++++++++++++++++++++ docs/DEVELOPER.md | 55 +++++++++++++++++++++++++++++++++++++++++++ docs/REQUIREMENTS.md | 33 ++++++++++++++++++++++---- docs/USER-GUIDE.es.md | 15 ++++++++++++ docs/USER-GUIDE.md | 15 ++++++++++++ 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 0111c9f..1f97b33 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -175,6 +175,33 @@ $49-79/bundle · $149 full suite (when 3+ exist). | May 1 (v1.6) | Mark Format Standardizer **Ready** | 199-row buyer corpus passing; Tier 1 + most Tier 2 built. | | May 1 (v1.6) | Add `src/core/errors.py` structured hierarchy | Uniform helpful messages across CLI + GUI. See TECHNICAL §7. | | May 13 (v1.6) | Ship in-house JSON i18n + EN/ES packs | Expand addressable market (Spanish-first buyers, LatAm bookkeepers) without a `gettext` build step. JSON packs editable by non-devs; parity test prevents drift. See TECHNICAL §10b. | +| May 13 (v1.6) | Ship licensing: 1-year HMAC-signed blobs, name+email registration, offline verification, tier-scaffolded for future SKUs | Unlock the lifetime-update business model without recurring infra. Honor-system DRM (HMAC + 30-day refund) — sufficient at $49. See §9b below. | + +## 9b. Licensing model + +**Decision (v1.6)**: offline HMAC-signed license blobs, 1-year lifetime, name + email registration required. Tier-scaffolded so future SKUs (PRO, ENTERPRISE) can carve per-tool feature sets without code changes. + +| Option | Verdict | +|---|---| +| **Offline HMAC blob (chosen)** | **CHOSEN.** No server, no internet, fits the no-touch constraint. Honor-system at this price point. | +| Online activation check | Rejected. Conflicts with the "your data never leaves your computer" promise; introduces support load (server downtime, network issues). | +| No license at all | Rejected. The lifetime-update value prop requires *some* gating to make renewal meaningful. | +| Time-bombed binary (PyInstaller --no-license) | Rejected. Can't deliver renewals without re-shipping the installer. | +| Hardware-locked license | Rejected. Friction on legitimate device-swaps; doesn't match the buyer persona's tolerance. | + +**Threat model**: a motivated reverse engineer can pull the HMAC secret out of the binary, mint their own licenses, and bypass the check. That's acceptable — the goal is to discourage casual blob-sharing among non-technical buyers, not stop targeted piracy. The 30-day refund window covers the same gap from a different angle (anyone who shares their blob is implicitly authorizing the buyer to issue them a refund-on-demand). + +**What's enforced**: +- License blob signature must match (HMAC-SHA256 with the build secret). +- Buyer-entered name + email must match the values embedded in the blob. +- Expiry date must be in the future. +- Tier must include the requested feature. + +**What's NOT enforced**: +- Number of devices the same blob is used on (no concurrent-use detection). +- Reverse-engineered re-signing of expired blobs (would require RSA / online check). + +**Future SKUs**: the ``FEATURES_BY_TIER`` table in ``src/license/features.py`` is the single source of truth for "which tools each tier unlocks". Adding a PRO SKU that excludes the pipeline runner is a 1-line edit there + a 1-line edit at the gate site. No consumer-code churn. ## 8. Re-lock triggers diff --git a/docs/DEVELOPER.md b/docs/DEVELOPER.md index 8846f4f..71b5d41 100644 --- a/docs/DEVELOPER.md +++ b/docs/DEVELOPER.md @@ -126,6 +126,61 @@ st.warning(t("gate.warning", name=filename)) # {name} interpolated via str.for - Do **not** put strings inside the farewell-overlay JS payload without going through `_js_html_safe()` in `src/gui/components/_legacy.py`; the helper escapes both the JS string terminator and HTML special chars. The test `TestFarewellEscape` pins that contract. - The sidebar picker is mounted by `hide_streamlit_chrome()`, so every page that calls that helper automatically gets the picker. Pages that don't call it (rare) can call `render_language_selector()` directly. +### Licensing + +The license layer lives at ``src/license/``. The public API: + +```python +from src.license import ( + get_manager, require_feature, current_state, + FeatureFlag, Tier, License, +) + +mgr = get_manager() +if not mgr.is_valid(): + raise RuntimeError("Not licensed") +require_feature(FeatureFlag.DEDUPLICATOR) +``` + +**Storage**: ``~/.datatools/license.json`` (override via +``DATATOOLS_LICENSE_PATH``). Signed locally with HMAC-SHA256 using a +secret read from ``DATATOOLS_LICENSE_SECRET`` (build-time replace; the +in-repo default is a development placeholder). + +**Dev bypass**: ``DATATOOLS_DEV_MODE=1`` short-circuits every check. +The test suite's autouse fixture sets this so existing tests don't +need their own license fixtures. Tests that need the real check +explicitly use ``isolated_license_path`` / +``activated_license_manager`` / ``unactivated_license_manager``. + +**Adding a feature flag**: + +1. Add the enum value to ``FeatureFlag`` in ``src/license/schema.py``. +2. Add it to the relevant tier's set in + ``FEATURES_BY_TIER`` in ``src/license/features.py``. +3. Gate at the call site: ``require_feature(FeatureFlag.YOUR_FLAG)``. + +**Adding a new tier**: + +1. Add the enum value to ``Tier``. +2. Add a row to ``FEATURES_BY_TIER`` listing the unlocked flags. +3. Add ``license.tier_`` translation keys to every i18n pack. +4. The activation flow, sidebar status badge, and feature gate all + pick up the new tier automatically. + +**Minting a license** (creator-only): + +```bash +DATATOOLS_LICENSE_SECRET= \ + python scripts/generate_license.py \ + --name "Jane Doe" --email jane@example.com \ + --tier core --years 1 +``` + +The script prints a ``DTLIC1:`` blob to stdout — deliver this in the +Gumroad / purchase email. The buyer pastes it into the activation +page or runs ``python -m src.license_cli activate --name ...``. + ### Add a format-standardizer field type 1. Add value to `FieldType` enum in `core/format_standardize.py`. diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md index cd2d786..bdeb562 100644 --- a/docs/REQUIREMENTS.md +++ b/docs/REQUIREMENTS.md @@ -174,11 +174,13 @@ and proceeds. - **Dev**: pytest, tox. ## 16. Test coverage -- 1,916 tests passing, 0 skipped, 0 xfailed. - - 1,777 core + CLI tests (run with `pytest -m 'not gui'` for a quick loop). - - 139 GUI tests under `tests/gui/` driving Streamlit pages via `AppTest` +- 1,995 tests passing, 0 skipped, 0 xfailed. + - 1,843 core + CLI tests (run with `pytest -m 'not gui'` for a quick loop). + Includes 40 license-layer unit tests + 26 license-CLI tests. + - 152 GUI tests under `tests/gui/` driving Streamlit pages via `AppTest` (smoke + EN/ES localization, chrome, gate, workflows, dedup review, - advanced panels, error paths, findings panel). Marked `gui`. + advanced panels, error paths, findings panel, activation + license + gate). Marked `gui`. - Includes 15 perf-shape regression tests. - Fixture corpora: text-cleaner (21), encodings (31), reference UTF-8 (9), format-cleaner (199 buyer cases + 20-row international stress fixture), missing-handler (3 use cases + 16 edge cases), column-mapper (3 use cases + 5 edge cases). - Run: `python run_tests.py [--tool …] [--fixtures] [--coverage]`. @@ -189,6 +191,29 @@ and proceeds. - Original input never modified. - Audit logs: `logs/` next to each run (timestamped). +## 17a. Licensing +- **Storage**: ``~/.datatools/license.json`` (or + ``$DATATOOLS_LICENSE_PATH`` override). Signed locally with + HMAC-SHA256. +- **Activation**: buyer pastes a base64-encoded license blob + (``DTLIC1:...``) on first launch; app verifies the signature + offline + matches the buyer-entered name/email to the embedded + values. +- **Lifetime**: every license is 1 year by default. Renewal applies a + fresh blob without losing the embedded buyer identity. +- **Tiers**: ``trial``, ``core`` (the v1 SKU — all 9 tools), ``pro``, + ``enterprise``. PRO and ENTERPRISE are scaffolded for future SKUs; + they currently unlock the same feature set as CORE. +- **Feature flags**: every tool has a stable feature id matching its + ``tool_id`` in :mod:`src.gui.tools_registry`. Adding a future per- + tool SKU is a one-line change to ``FEATURES_BY_TIER`` — no consumer + code edits. +- **Dev bypass**: ``DATATOOLS_DEV_MODE=1`` skips every check (used by + the test suite and during development). +- **No internet**: signature verification is fully offline. The + shipped binary embeds the verification secret; see + ``docs/DECISIONS.md`` for the threat-model discussion. + ## 18. Error handling - Structured hierarchy: `DataToolsError` → `InputValidationError`, `ConfigError`, `FileFormatError`, `FileAccessError`. - Subclasses extend stdlib `ValueError` / `OSError` so existing handlers still catch them. diff --git a/docs/USER-GUIDE.es.md b/docs/USER-GUIDE.es.md index 3e42f16..c7ee1fe 100644 --- a/docs/USER-GUIDE.es.md +++ b/docs/USER-GUIDE.es.md @@ -4,6 +4,21 @@ **Versión**: 1.6 · **Actualizado**: 2026-05-13 +## 0. Primer arranque — activación + +DataTools debe activarse antes de desbloquear cualquier herramienta. En el primer arranque verás la pantalla **Activar**. + +| Si tienes… | Haz esto | +|---|---| +| Un código de licencia de pago (del correo de compra) | Introduce tu nombre completo y correo, pega el código completo (empieza con `DTLIC1:`), pulsa **Activar**. | +| Nada todavía, quieres evaluar | Introduce nombre y correo, pulsa **Iniciar prueba de 1 año**. La app emite localmente una licencia de prueba de 1 año — sin pago. | + +La renovación funciona igual: pega el código de renovación, pulsa **Aplicar renovación**. La fecha de caducidad se reinicia a un año desde la renovación. + +El archivo de licencia vive en `~/.datatools/license.json` (Windows: `C:\Users\\.datatools\license.json`). La barra lateral muestra tu nivel y los días restantes en todo momento. Aparece un aviso de renovación 30 días antes de la caducidad. + +Si quieres usar la misma licencia en otro equipo, desactiva éste (página Activar → **Desactivar este dispositivo**) y vuelve a pegar tu código en el nuevo. + ## 1. Instalación No necesitas tener Python instalado — el paquete es autocontenido. diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md index 5d543b9..ad9e886 100644 --- a/docs/USER-GUIDE.md +++ b/docs/USER-GUIDE.md @@ -4,6 +4,21 @@ **Version**: 1.6 · **Updated**: 2026-05-01 +## 0. First launch — activation + +DataTools must be activated before any tools unlock. On first launch you'll see the **Activate** screen. + +| You have… | Do this | +|---|---| +| A paid license blob (from your purchase email) | Enter your full name + email, paste the entire blob (starts with `DTLIC1:`), click **Activate**. | +| Nothing yet, want to evaluate | Enter your name + email, click **Start 1-year trial**. The app self-issues a 1-year trial license — no payment required. | + +Renewal works the same way: paste the renewal blob, click **Apply renewal**. The expiry resets to one year from the renewal date. + +The license file lives at `~/.datatools/license.json` (Windows: `C:\Users\\.datatools\license.json`). The sidebar shows your tier and days remaining at all times. A renewal warning appears 30 days before expiry. + +If you ever want to use the same license on a different machine, deactivate this one (Activate page → **Deactivate this device**) and re-paste your blob on the new machine. + ## 1. Install You don't need Python — the bundle is self-contained.