Metadata-Version: 2.4
Name: sms-verifier
Version: 0.1.0
Summary: Agent-oriented SMS OTP verification lifecycle: reserve a number, wait for the code, reconcile the ledger — with a fail-closed cost guard
Author: ameobius-ai
License: MIT
Project-URL: Homepage, https://gitlab.com/ameobius-ai/sms-verifier
Keywords: sms,otp,verification,agents,sms-activate
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: curl_cffi>=0.6
Requires-Dist: typing_extensions
Dynamic: license-file

# sms-verifier

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
[![Python](https://img.shields.io/badge/Python-%3E%3D3.10-3776AB.svg)](https://www.python.org)
[![Platform](https://img.shields.io/badge/Platform-POSIX-E44C30.svg)](https://en.wikipedia.org/wiki/POSIX)

**Agent-oriented SMS OTP verification with a fail-closed cost guard.**

sms-verifier drives the full phone-verification lifecycle against **any
SMS-Activate-compatible provider** — reserve a disposable number, wait for
the OTP, finish or cancel the activation — and keeps a crash-safe local
ledger so agents never lose track of what they ordered and what they spent.
No paid solving farm, no vendor lock-in, no shipped provider defaults: the
only provider that works out of the box is an offline **mock**.

```
                       ┌──────────────────────────────────────┐
                       │           your agent / CLI           │
                       └──────────────────┬───────────────────┘
                                          │
                          sms-verify price/order/wait/auto/
                          list/cancel/finish/reconcile
                                          │
                       ┌──────────────────▼───────────────────┐
                       │        sms_verifier.core             │
                       │  lifecycle + fail-closed cost-guard  │
                       │  + flock/atomic JSON ledger          │
                       │  (SMS_STATE_FILE)                    │
                       └──────────────────┬───────────────────┘
                                          │ provider registry dispatch
                    ┌─────────────────────┼─────────────────────────┐
                    │                     │                         │
          ┌─────────▼─────────┐ ┌─────────▼──────────┐ ┌────────────▼───────────┐
          │      mock         │ │    sms-activate    │ │   rental_example       │
          │ offline demo, the │ │ generic adapter:   │ │ EXAMPLE rental-portal  │
          │ only default      │ │ env-configured     │ │ adapter (fragile,      │
          │                   │ │ base URL + API key │ │ check provider ToS)    │
          └───────────────────┘ └─────────┬──────────┘ └────────────┬───────────┘
                                          │ GET ?api_key=&action=   │ form login
                                          ▼                         ▼
                          any SMS-Activate-compatible      rental portal web API
                          provider endpoint                (normalized to the same
                          (SMS_PROVIDER_BASE_URL)          string protocol)
```

## What you get

| Piece | What it is |
| --- | --- |
| `sms-verify` CLI | `price` / `order` / `wait` / `auto` / `list` / `cancel` / `finish` / `reconcile` lifecycle commands, JSON on stdout, errors on stderr |
| Fail-closed cost-guard | `order`/`auto` refuse without `--max-price` (exit 3); the only deliberate bypass is `--no-price-check` |
| Crash-safe ledger | atomic writes (temp + fsync + rename) under `flock`; corrupted JSON is backed up and never blocks work |
| `reconcile` drift guard | post-run sweep: verifies stale `waiting` records against the provider, cancels refundable ones, spends nothing |
| Provider registry | `mock` (default), `sms-activate` (generic, env-configured), `rental` (documented example adapter) |
| `sms-pool` CLI | raw protocol actions (`balance`, `prices`, `get-number`, `status`, …) for debugging providers |

## Quickstart

Requirements: **Python ≥ 3.10**, **POSIX** system (the ledger uses
`fcntl.flock`; there is no native Windows support).

```sh
git clone <this-repo> sms-verifier && cd sms-verifier
pip install -e .            # or: pipx install .
```

**1. Try the full lifecycle offline** (mock provider, the default — no
keys, no network, no cost):

```sh
export SMS_STATE_FILE="$HOME/.local/state/sms-verifier/activations.json"  # this is already the default

sms-verify auto --service tg --country 0 --max-price 5 --timeout 30 --interval 1
# {"activation": "1", "phone": "1555000001", "provider": "mock", "price": 1.0}
# {"activation": "1", "phone": "1555000001", "code": "123456"}
# {"activation": "1", "provider_resp": "ACCESS_ACTIVATION"}

sms-verify list
sms-verify reconcile
```

**2. Point it at a real provider.** Any SMS-Activate-compatible endpoint
works; bring your own account:

```sh
export SMS_PROVIDER_BASE_URL="https://your-provider.example.com/stubs/handler_api.php"
export SMS_PROVIDER_API_KEY="your-api-key"

sms-verify price    --service tg --country 1 --provider sms-activate
sms-verify order    --service tg --country 1 --provider sms-activate --max-price 2.0
sms-verify wait     --last --timeout 300 --interval 5
sms-verify finish   --last
```

There are intentionally **no default provider URLs and no default keys** —
real providers exist only after you explicitly configure them.

## Commands

| Command | Purpose |
| --- | --- |
| `price` | read-only price lookup (also what the cost-guard uses) |
| `order` | cost-guard check + reserve a number (`--dry-run` supported) |
| `wait` | poll for the OTP (`--activation ID` or `--last`) |
| `auto` | `order` → `wait` → `finish` in one shot |
| `list` | print the local ledger |
| `cancel` | cancel at the provider (refund on refundable providers) |
| `finish` | mark the activation done at the provider (setStatus 6) |
| `reconcile` | drift guard: verify stale `waiting` records, cancel refundable strays, never spend (`--dry-run`, `--json`) |

## Configuration

| Env var | Default | Purpose |
| --- | --- | --- |
| `SMS_PROVIDER_BASE_URL` | — (required for `sms-activate`) | Any SMS-Activate-compatible endpoint |
| `SMS_PROVIDER_API_KEY` | — (required for `sms-activate`) | API key for that endpoint |
| `SMS_STATE_FILE` | `~/.local/state/sms-verifier/activations.json` | Activation ledger path |
| `SMS_DEFAULT_PROVIDER` | `mock` | Default `--provider` for lifecycle commands |
| `SMS_SOCKS_PROXY` | — | Optional SOCKS proxy for all provider traffic, e.g. `socks5h://host:port` |
| `SMS_GEO_BLOCK_MARKERS` | — | Extra comma-separated geo-block page markers |
| `SMS_RENTAL_BASE_URL` | — (required for `rental`) | Example rental adapter: portal base URL |
| `SMS_RENTAL_LOGIN` | — (required for `rental`) | Example rental adapter: portal login |
| `SMS_RENTAL_PASSWORD` | — (required for `rental`) | Example rental adapter: portal password |
| `SMS_RENTAL_SESSION_CACHE` | `~/.local/state/sms-verifier/rental_session.json` | Example rental adapter: session cache (0600) |
| `SMS_MOCK_BALANCE` / `SMS_MOCK_PRICE` / `SMS_MOCK_CODE` | `100.0` / `1.0` / `123456` | Mock provider knobs for demos and tests |

## Exit-code contract

| Code | Meaning |
| --- | --- |
| `0` | success |
| `2` | bad arguments / bad state / geo-block / HTML instead of protocol |
| `3` | cost-guard: no `--max-price`, price over the limit, or price unavailable at the provider |
| `4` | provider error: cancelled activation, fatal `getStatus`, `setStatus` refusal, network error (for `reconcile`: unresolved records remain) |
| `5` | OTP wait timeout |

JSON results go to **stdout**; warnings, logs and error JSON go to
**stderr**. The contract is covered by the test suite.

## Provider protocol

The wire format is the de-facto SMS-Activate standard:

```
GET <base_url>?api_key=<key>&action=<action>&<params...>
```

| Action | Response |
| --- | --- |
| `getBalance` | `ACCESS_BALANCE:<float>` |
| `getCountries` / `getServicesList` / `getPrices` | JSON |
| `getNumber` | `ACCESS_NUMBER:<id>:<phone>` |
| `getStatus` | `STATUS_OK:<code>` · `STATUS_WAIT_CODE` · `STATUS_CANCEL` · `NO_ACTIVATION` |
| `setStatus` (`6` finish / `8` cancel) | `ACCESS_ACTIVATION` · `ACCESS_CANCEL` |

Non-SMS-Activate providers can still plug in: the bundled `rental`
example adapter normalizes a rental-portal web flow (form login + JSON
API) into exactly this string protocol. Adapters register through
`sms_verifier.protocol.register_provider(name, call, refundable=...)`.

**A note on transports.** HTTP goes through `curl_cffi` with Chrome TLS
impersonation. This is a **reliability/compatibility choice** — some
provider endpoints sit behind TLS-fingerprint filters that reject stock
Python clients — not an anti-detection measure.

## The example rental adapter

`providers/rental_example.py` is a reverse-engineered **example** of
normalizing a Laravel-style rental portal (form login → XSRF session →
`/api/portal/*` JSON) into the protocol above. It is labeled and treated
as fragile on purpose:

- any frontend change (routes, CSRF flow, response shapes) can break it;
- rented numbers have **no cancel and no refund** — `reconcile` only
  records them;
- check the portal's Terms of Service before automating it.

## Browser Extension (localhost daemon + MV3)

This repository also ships a **localhost bridge** for driving the
lifecycle from Chrome: a loopback-only HTTP daemon (`sms-verify-daemon`)
and a Manifest V3 extension that detects phone/OTP fields on pages you
explicitly allow and fills them — provider credentials, the activation
ledger and every real network call stay on your machine inside the daemon.

```sh
pip install -e .           # the library, if not already installed
pip install ./daemon       # the loopback daemon (installs sms-verify-daemon)
sms-verify-daemon          # bearer token generated 0600 on first start
```

Then in the browser: `chrome://extensions` → **Developer mode** →
**Load unpacked** → select the [`extension/`](./extension) directory;
open the extension's options page, paste the bearer token, and add the
domains you trust.

Safe by default:

- the domain allowlist ships **empty** — nothing is injected anywhere
  until you add a domain;
- **confirm mode** is the default: every step is one explicit click, and
  even `auto` mode only fills fields — the page's own submit controls
  are never touched;
- the daemon is loopback-only, bearer-token authenticated and
  Host-header validated; `POST /order` is fail-closed on cost
  (`max_price` is mandatory, refused with `402` before any subprocess
  is spawned);
- HTTP statuses mirror the CLI exit-code contract:
  `0→200`, `2→400`, `3→402`, `4→502`, `5→504`;
- **sideload-only by design** — the extension is deliberately not
  published to any extension store; you load code you can read, from a
  checkout you control.

See [SECURITY.md](./SECURITY.md) for the daemon/extension security scope
and vulnerability-reporting policy.

## Responsible Use

Retrieving SMS OTPs for phone numbers you do not control is subject to the
terms of service of the provider and of the target site, and may be
illegal outside an explicit authorization. Before you point this tool at
anything:

- **Authorized targets only.** Your own services, an explicit testing
  scope, or a program that permits it. Never real-user accounts, never
  accounts that belong to other people, never 2FA on personal mailboxes
  or bank accounts.
- **Cost-guard is mandatory.** The tool refuses to order without
  `--max-price` (exit 3) by design; `--no-price-check` exists only as a
  deliberate, logged bypass. Keep it that way in your automations.
- **Mock-first.** A fresh install only talks to the offline `mock`
  provider. Real providers activate only after you export credentials for
  them — an accidental run cannot spend money.
- **No shared public numbers.** This tool deliberately does not support
  shared-inbox "receive SMS publicly" sites: those numbers are
  OTP-blocked by nearly all serious services and provide no privacy.

## Development

```sh
pip install -e . pytest
python3 -m pytest tests/ -q     # fully offline: all provider I/O is mocked

# extension + daemon suites (also fully offline, mock provider):
make test                       # pytest (library + daemon) + node:test units
node --test tests/extension/    # field-detector + allowlist units (jsdom)
make manifest PORT=19999        # regenerate the manifest for a non-default daemon port
make zip                        # build/sms-verifier-extension.zip for sideloading
```

## License

[MIT](./LICENSE)
