docs(license): document activation flow, tier system, dev bypass

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 16:54:30 +00:00
parent e435103113
commit e612c751a8
5 changed files with 141 additions and 4 deletions

View File

@@ -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) | 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 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 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 ## 8. Re-lock triggers

View File

@@ -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. - 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. - 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_<name>`` 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=<shipping-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 <blob> --name ...``.
### Add a format-standardizer field type ### Add a format-standardizer field type
1. Add value to `FieldType` enum in `core/format_standardize.py`. 1. Add value to `FieldType` enum in `core/format_standardize.py`.

View File

@@ -174,11 +174,13 @@ and proceeds.
- **Dev**: pytest, tox. - **Dev**: pytest, tox.
## 16. Test coverage ## 16. Test coverage
- 1,916 tests passing, 0 skipped, 0 xfailed. - 1,995 tests passing, 0 skipped, 0 xfailed.
- 1,777 core + CLI tests (run with `pytest -m 'not gui'` for a quick loop). - 1,843 core + CLI tests (run with `pytest -m 'not gui'` for a quick loop).
- 139 GUI tests under `tests/gui/` driving Streamlit pages via `AppTest` 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, (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. - 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). - 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]`. - Run: `python run_tests.py [--tool …] [--fixtures] [--coverage]`.
@@ -189,6 +191,29 @@ and proceeds.
- Original input never modified. - Original input never modified.
- Audit logs: `logs/` next to each run (timestamped). - 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 ## 18. Error handling
- Structured hierarchy: `DataToolsError` → `InputValidationError`, `ConfigError`, `FileFormatError`, `FileAccessError`. - Structured hierarchy: `DataToolsError` → `InputValidationError`, `ConfigError`, `FileFormatError`, `FileAccessError`.
- Subclasses extend stdlib `ValueError` / `OSError` so existing handlers still catch them. - Subclasses extend stdlib `ValueError` / `OSError` so existing handlers still catch them.

View File

@@ -4,6 +4,21 @@
**Versión**: 1.6 · **Actualizado**: 2026-05-13 **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\<tú>\.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 ## 1. Instalación
No necesitas tener Python instalado — el paquete es autocontenido. No necesitas tener Python instalado — el paquete es autocontenido.

View File

@@ -4,6 +4,21 @@
**Version**: 1.6 · **Updated**: 2026-05-01 **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\<you>\.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 ## 1. Install
You don't need Python — the bundle is self-contained. You don't need Python — the bundle is self-contained.