freemyid 0.2.0.dev1__py3-none-any.whl

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.
Files changed (100) hide show
  1. freemyid/__init__.py +2 -0
  2. freemyid/_data/brokers/LICENSE-DATA.md +28 -0
  3. freemyid/_data/brokers/registry.sample.yaml +674 -0
  4. freemyid/_data/brokers/state_laws.yaml +448 -0
  5. freemyid/_data/schema/registry.schema.json +668 -0
  6. freemyid/_data/schema/setup-config.schema.json +538 -0
  7. freemyid/_data/schema/state_laws.schema.json +222 -0
  8. freemyid/cli.py +725 -0
  9. freemyid/cli_json.py +198 -0
  10. freemyid/config.py +43 -0
  11. freemyid/consent.py +82 -0
  12. freemyid/crypto.py +197 -0
  13. freemyid/doctor.py +374 -0
  14. freemyid/engine/__init__.py +0 -0
  15. freemyid/engine/_dev/__init__.py +101 -0
  16. freemyid/engine/_dev/devrun.py +101 -0
  17. freemyid/engine/_dev/inbox/devauto.eml +7 -0
  18. freemyid/engine/_dev/registry.yaml +106 -0
  19. freemyid/engine/_dev/site/devauto.example/confirm.html +1 -0
  20. freemyid/engine/_dev/site/devauto.example/name/jane-testperson/il.html +17 -0
  21. freemyid/engine/_dev/site/devauto.example/name/jane-testperson/nc.html +4 -0
  22. freemyid/engine/_dev/site/devauto.example/name/jane-testperson/oh.html +4 -0
  23. freemyid/engine/_dev/site/devauto.example/optout/done.html +1 -0
  24. freemyid/engine/_dev/site/devauto.example/optout.html +6 -0
  25. freemyid/engine/_dev/site/devblocked.example/name/jane-testperson/il.html +3 -0
  26. freemyid/engine/_dev/site/devblocked.example/optout.html +3 -0
  27. freemyid/engine/_dev/site/devfill.example/ajax.html +20 -0
  28. freemyid/engine/_dev/site/devfill.example/aria.html +16 -0
  29. freemyid/engine/_dev/site/devfill.example/consent.html +20 -0
  30. freemyid/engine/_dev/site/devfill.example/done.html +3 -0
  31. freemyid/engine/_dev/site/devfill.example/empty.html +1 -0
  32. freemyid/engine/_dev/site/devfill.example/fullname.html +12 -0
  33. freemyid/engine/_dev/site/devfill.example/labels.html +32 -0
  34. freemyid/engine/_dev/site/devfill.example/late.html +22 -0
  35. freemyid/engine/_dev/site/devfind.example/hostile.html +12 -0
  36. freemyid/engine/_dev/site/devfind.example/p/jane-testperson-shelbyville-il.html +6 -0
  37. freemyid/engine/_dev/site/devfind.example/p/jane-testperson-springfield-il.html +6 -0
  38. freemyid/engine/_dev/site/devfind.example/results.html +11 -0
  39. freemyid/engine/_dev/site/devfind.example/search.html +16 -0
  40. freemyid/engine/_dev/site/devweb.example/remove.html +1 -0
  41. freemyid/engine/browser.py +430 -0
  42. freemyid/engine/fill_for_me.py +630 -0
  43. freemyid/engine/filler.py +523 -0
  44. freemyid/engine/handoff.py +237 -0
  45. freemyid/engine/letters.py +125 -0
  46. freemyid/engine/listing_finder.py +538 -0
  47. freemyid/engine/mail.py +406 -0
  48. freemyid/engine/mail_tls.py +198 -0
  49. freemyid/engine/matcher.py +208 -0
  50. freemyid/engine/outbox.py +421 -0
  51. freemyid/engine/plugins.py +68 -0
  52. freemyid/engine/runner.py +791 -0
  53. freemyid/engine/tiers.py +107 -0
  54. freemyid/interfaces.py +164 -0
  55. freemyid/logging_setup.py +152 -0
  56. freemyid/models.py +700 -0
  57. freemyid/netpolicy.py +145 -0
  58. freemyid/notify.py +58 -0
  59. freemyid/paths.py +83 -0
  60. freemyid/registry.py +275 -0
  61. freemyid/scheduler/__init__.py +203 -0
  62. freemyid/scheduler/render.py +91 -0
  63. freemyid/secret_backends.py +562 -0
  64. freemyid/secrets.py +25 -0
  65. freemyid/setup_config.py +311 -0
  66. freemyid/state.py +151 -0
  67. freemyid/storage/__init__.py +3 -0
  68. freemyid/storage/db.py +394 -0
  69. freemyid/submitters/__init__.py +0 -0
  70. freemyid/submitters/assisted.py +171 -0
  71. freemyid/submitters/base.py +119 -0
  72. freemyid/submitters/email.py +40 -0
  73. freemyid/submitters/web_form.py +276 -0
  74. freemyid/templates/_identity.txt.j2 +19 -0
  75. freemyid/templates/_signature.txt.j2 +7 -0
  76. freemyid/templates/ccpa.txt.j2 +16 -0
  77. freemyid/templates/follow_up.txt.j2 +15 -0
  78. freemyid/templates/generic_removal.txt.j2 +12 -0
  79. freemyid/templates/legal_demand.txt.j2 +20 -0
  80. freemyid/templates/phone_script.txt.j2 +25 -0
  81. freemyid/templates/postal_letter.txt.j2 +20 -0
  82. freemyid/templates/state_privacy.txt.j2 +17 -0
  83. freemyid/wizard/__init__.py +0 -0
  84. freemyid/wizard/dashboard.py +401 -0
  85. freemyid/wizard/desktop.py +188 -0
  86. freemyid/wizard/draft.py +627 -0
  87. freemyid/wizard/firstrun.py +42 -0
  88. freemyid/wizard/flows.py +283 -0
  89. freemyid/wizard/report.py +86 -0
  90. freemyid/wizard/server.py +834 -0
  91. freemyid/wizard/static/__init__.py +0 -0
  92. freemyid/wizard/static/app.js +98 -0
  93. freemyid/wizard/static/freemyid.png +0 -0
  94. freemyid/wizard/static/style.css +85 -0
  95. freemyid/wizard/ui.py +343 -0
  96. freemyid-0.2.0.dev1.dist-info/METADATA +396 -0
  97. freemyid-0.2.0.dev1.dist-info/RECORD +100 -0
  98. freemyid-0.2.0.dev1.dist-info/WHEEL +4 -0
  99. freemyid-0.2.0.dev1.dist-info/entry_points.txt +2 -0
  100. freemyid-0.2.0.dev1.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,396 @@
1
+ Metadata-Version: 2.5
2
+ Name: freemyid
3
+ Version: 0.2.0.dev1
4
+ Summary: Local-first personal data broker opt-out tool with scheduled re-checks.
5
+ Project-URL: Homepage, https://freemyid.org
6
+ Project-URL: Source, https://github.com/FreeMyID/freemyid
7
+ Project-URL: Issues, https://github.com/FreeMyID/freemyid/issues
8
+ Author: Free My ID
9
+ License-Expression: AGPL-3.0-or-later
10
+ License-File: LICENSE
11
+ Keywords: ccpa,data-broker,gdpr,opt-out,people-search,privacy
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: Web Environment
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Security
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: cryptography>=42.0
24
+ Requires-Dist: desktop-notifier>=6.0
25
+ Requires-Dist: fastapi>=0.111
26
+ Requires-Dist: httpx>=0.27
27
+ Requires-Dist: jinja2>=3.1
28
+ Requires-Dist: jsonschema>=4.22
29
+ Requires-Dist: keyring>=25.0
30
+ Requires-Dist: platformdirs>=4.2
31
+ Requires-Dist: playwright>=1.45
32
+ Requires-Dist: pydantic>=2.7
33
+ Requires-Dist: python-multipart>=0.0.9
34
+ Requires-Dist: pyyaml>=6.0
35
+ Requires-Dist: rapidfuzz>=3.9
36
+ Requires-Dist: rich>=13.7
37
+ Requires-Dist: typer>=0.12
38
+ Requires-Dist: uvicorn>=0.30
39
+ Provides-Extra: dev
40
+ Requires-Dist: build>=1.2; extra == 'dev'
41
+ Requires-Dist: mypy>=1.10; extra == 'dev'
42
+ Requires-Dist: pillow>=10; extra == 'dev'
43
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
44
+ Requires-Dist: pytest>=8.2; extra == 'dev'
45
+ Requires-Dist: ruff>=0.5; extra == 'dev'
46
+ Requires-Dist: types-pyyaml; extra == 'dev'
47
+ Provides-Extra: package
48
+ Requires-Dist: pyinstaller>=6.8; extra == 'package'
49
+ Requires-Dist: pywebview>=5.0; extra == 'package'
50
+ Provides-Extra: pdf
51
+ Requires-Dist: weasyprint>=62; extra == 'pdf'
52
+ Description-Content-Type: text/markdown
53
+
54
+ # Free My ID
55
+
56
+ **Take your name off the websites that sell it.**
57
+
58
+ Free My ID is a local-first tool that finds your listings on people-search sites (also called data brokers), asks each one to delete your details, and keeps checking so it can ask again when listings come back.
59
+
60
+ No account. No cloud. It never asks for your Social Security number, bank or card details. Free and open source. Uninstall anytime.
61
+
62
+ It runs on your own computer. There is no AI or cloud service at runtime: every step is deterministic Python driven by a broker registry (YAML) and a per-listing state machine. Your personal data stays in an encrypted database on your machine.
63
+
64
+ It works on people-search and data-broker sites only. It does not change what Facebook or Google does with your account.
65
+
66
+ **[Download for Windows](https://freemyid.org/download/)** | **[Website and plain-English guide](https://freemyid.org/)** | **[Version 0.1.0 on GitHub](https://github.com/FreeMyID/freemyid/releases/tag/v0.1.0)**
67
+
68
+ > **Status:** early release (0.1.0) for Windows 10 and 11. It is new, so expect rough edges, and please tell us what confuses you. The installer is not code-signed yet, so Windows shows a blue SmartScreen box the first time; the download page shows what to click. Each release lists a SHA256 checksum so you can confirm your download.
69
+
70
+ Not a developer? You can stop reading here and use the [website](https://freemyid.org/) or the [User Guide](docs/USER_GUIDE.md).
71
+
72
+ **Helping a parent or grandparent?** Install it with them, sit beside them for the six setup screens, and use the website's plain-English guide. Most of the rest of this page is for developers.
73
+
74
+ ## About the broker list
75
+
76
+ This public repository includes `brokers/registry.sample.yaml`, a sample list of brokers (CC BY-NC 4.0). The full, maintained list ships with the Windows app. If you run from source, the sample is a starting point, not the same coverage.
77
+
78
+ ## What it does, and what it doesn't
79
+
80
+ **It does:**
81
+
82
+ - Search each broker in the registry for each person (a "profile"), scoring candidate listings against names, nicknames, previous names, city/state, age, relatives, phone and email.
83
+ - Submit opt-outs through the broker's own channel (web form, email, postal letter, phone script, or a to-do when an account is required), sending only what the listing already shows.
84
+ - Click email verification links in a dedicated removal mailbox, but only links on that broker's own domains.
85
+ - Escalate requests that get ignored: resubmit, then a state-law follow-up email, then a firmer letter (always held for your approval), then a to-do with the right Attorney General or CPPA complaint links.
86
+ - Re-scan on a schedule and file again when a listing reappears.
87
+ - Keep encrypted evidence: screenshots of every listing found and every confirmation page, plus copies of every email sent.
88
+
89
+ **Honest limits:**
90
+
91
+ - **Free My ID works on people-search sites. It does not change what Facebook or Google does with your account.**
92
+ - **A site's "removed" means removed from that one broker.** It does not mean removed from the internet, from search engines, from public records, or from brokers that aren't in the registry.
93
+ - **Listings come back.** Brokers re-ingest data from their sources, and clone sites copy one another. That is why Free My ID keeps re-checking rather than running once.
94
+ - **Some brokers need you.** CAPTCHAs, ID requests, login walls, postal letters and phone calls are handed to a person. Free My ID never solves or bypasses a CAPTCHA or other bot protection.
95
+ - **The registry goes stale.** Brokers move opt-out pages and change forms. Entries carry a `verification_status` (`verified`, `unverified` or `broken`), and a weekly health check flags breakage, but some entries will be wrong at any given time.
96
+ - **Matching is fuzzy.** Only `CONFIRMED` matches (name plus at least 2 other signals) are submitted automatically. `POSSIBLE` matches (name plus 1 signal) wait for you to confirm.
97
+ - **It's not legal advice.** The letter templates cite state privacy laws, but you are responsible for what you send.
98
+
99
+ ## Install
100
+
101
+ **Windows 10 or 11:** The installer is offered on the [download page](https://freemyid.org/download/). It is unsigned for now, so Windows shows a blue SmartScreen box the first time; the download page shows what to click. Linux and macOS builds are not offered yet; run from source instead.
102
+
103
+ **From source (developers):** Requires Python 3.11+ (development uses 3.12). Free My ID drives your installed Chrome or Edge first. It only downloads Playwright's bundled browser if neither is present, and it asks before doing so.
104
+
105
+ ### With pipx (command line)
106
+
107
+ ```bash
108
+ pipx install git+https://github.com/FreeMyID/freemyid.git
109
+ freemyid setup
110
+ ```
111
+
112
+ ## For developers and contributors
113
+
114
+ ### Dev environment
115
+
116
+ ```bash
117
+ python3.12 -m venv .venv # or: uv venv --python 3.12 .venv
118
+ .venv/bin/pip install -e '.[dev]' # or: uv pip install --python .venv -e '.[dev]'
119
+ .venv/bin/freemyid --help
120
+ .venv/bin/pytest -q
121
+ .venv/bin/ruff check src tests
122
+ ```
123
+
124
+ Optional extras: `.[pdf]` (WeasyPrint, for PDF reports) and `.[package]` (PyInstaller).
125
+
126
+ For development, point Free My ID at a throwaway data directory and a test key so you never touch your real data or OS keychain:
127
+
128
+ ```bash
129
+ export FREEMYID_HOME="$(mktemp -d)"
130
+ export FREEMYID_TEST_KEY=dev-only-not-a-secret
131
+ ```
132
+
133
+ `FREEMYID_TEST_KEY` is for tests and development only. Never set it on a real install, because it replaces the keychain-held database key with one derived from that string.
134
+
135
+ ## Commands
136
+
137
+ Run `freemyid --help` or `freemyid <command> --help` for full options. `-v/--verbose` turns on debug logging (still redacted).
138
+
139
+ | Command | What it does |
140
+ |---|---|
141
+ | `freemyid setup [--no-web] [--prefill FILE]` | Add a person. Opens the local web wizard by default; `--no-web` uses terminal prompts. `--prefill` imports a family bundle. |
142
+ | `freemyid dashboard` | Open the local dashboard: status, evidence, to-dos, outbox approvals. |
143
+ | `freemyid scan [-p PROFILE] [-b BROKER ...]` | Search brokers for listings. Never submits anything. |
144
+ | `freemyid run --dry-run [-p ...] [-b ...]` | Full pipeline, but submits nothing. Run this first. |
145
+ | `freemyid run [-p ...] [-b ...]` | Scan, submit opt-outs, check verifications, escalate ignored requests. Refuses to run until real submissions are enabled. |
146
+ | `freemyid status [-p PROFILE]` | Per-broker state, last update, next check and escalation level for each person. |
147
+ | `freemyid report [-p PROFILE] [--format html\|pdf]` | Export a report to the artifacts folder. |
148
+ | `freemyid profile add [--prefill FILE]` | Same as `setup`. |
149
+ | `freemyid profile list` | List people, consent basis and whether a mailbox is configured. |
150
+ | `freemyid profile edit PROFILE` | Edit a person (terminal flow). |
151
+ | `freemyid profile remove PROFILE [--yes]` | Delete a person, all their evidence, and their mailbox password from the keychain. |
152
+ | `freemyid profile export PROFILE DEST` | Write a passphrase-protected `.freemyid-profile` bundle for a family member's install. |
153
+ | `freemyid schedule install\|remove\|status` | Manage the background re-check job. |
154
+ | `freemyid registry show` | List brokers: method, verification, CAPTCHA, `covered_by`. |
155
+ | `freemyid registry validate [PATH]` | Validate a registry file against the model and JSON Schema. |
156
+ | `freemyid registry update` | Fetch and validate a new registry from `registry_url`. |
157
+ | `freemyid todo list [-p PROFILE]` | Items that need a person. |
158
+ | `freemyid todo done ID` | Mark a to-do done (an ID prefix is enough). |
159
+ | `freemyid outbox list` | Emails waiting for your approval. |
160
+ | `freemyid outbox approve ID` | Approve and send one queued email. |
161
+ | `freemyid config set KEY VALUE` | Change a setting in `config.yaml`. Protected keys are rejected. |
162
+ | `freemyid config enable-submissions` | One-time explicit opt-in to real submissions. |
163
+
164
+ `PROFILE` matches an ID prefix or the profile's label (e.g. `Mom`). Profiles that fail the consent gate are skipped with a message.
165
+
166
+ ### Safety gates, in order
167
+
168
+ 1. **Consent gate** (`cli.consent_ok`, re-checked in the runner). Every profile needs an attesting name. A minor needs `guardian_of_minor` consent. An `authorized_agent` needs a signed authorization attached.
169
+ 2. **Dry run first.** `freemyid run` without `--dry-run` exits until `freemyid config enable-submissions` has been confirmed interactively. `real_submissions_enabled` can't be set with `config set`.
170
+ 3. **Email approval.** With `email_mode: approve_each` (the default), every outgoing email waits in the outbox. Legal demands always wait, even in `auto` mode.
171
+ 4. **Match gate.** Only `CONFIRMED` listings are auto-submitted.
172
+ 5. **Permission flags.** ID upload, account creation and phone disclosure are off by default per profile. When a broker needs one of them, you get a `permission_needed` to-do instead.
173
+
174
+ ## Architecture
175
+
176
+ ```
177
+ src/freemyid/
178
+ cli.py Typer CLI (thin; all logic lives in modules)
179
+ models.py Pydantic models: Profile, Broker, Registry, StateLaw, Listing, Request, ...
180
+ state.py Pure state machine + escalation ladder + re-check timing
181
+ interfaces.py Contracts between modules (Submitter, Mailer, HumanHandoff, Notifier, entry points)
182
+ registry.py Load/validate/update brokers/registry.yaml and state_laws.yaml
183
+ netpolicy.py Network allowlist; every request goes through NetPolicy.is_allowed
184
+ crypto.py AES-256-GCM; key in the OS keychain; passphrase export (scrypt)
185
+ secrets.py Mailbox passwords in the OS keychain
186
+ storage/db.py Encrypted SQLite store (Store)
187
+ logging_setup.py PII-redacting log filter
188
+ config.py, paths.py Non-PII settings and file locations
189
+ engine/ runner, matcher, browser (Playwright), mail, handoff, outbox, letters, plugins
190
+ submitters/ web_form, email, postal, phone, account_required + per-broker classes
191
+ templates/ jinja2 letters: generic, ccpa, state_privacy, follow_up, legal_demand, postal_letter, phone_script
192
+ wizard/ FastAPI wizard + dashboard (localhost only), flows, report
193
+ scheduler/ systemd user timer / cron, launchd, schtasks
194
+ notify.py Desktop notifications (PII-free)
195
+ brokers/ registry.yaml, state_laws.yaml, SOURCES.md
196
+ schema/ JSON Schemas generated from models (tools/gen_schema.py)
197
+ tools/ gen_schema.py, registry_health.py
198
+ tests/ pytest suite
199
+ packaging/ PyInstaller spec, installers
200
+ ```
201
+
202
+ ### Module ownership
203
+
204
+ Each area has one owner. Others send change requests rather than editing it. Agent definitions are in `.claude/agents/`.
205
+
206
+ | Owner | Files |
207
+ |---|---|
208
+ | orchestrator | `models.py`, `state.py`, `interfaces.py`, `registry.py`, `netpolicy.py`, `crypto.py`, `secrets.py`, `logging_setup.py`, `config.py`, `paths.py`, `cli.py`, `storage/`, `schema/`, `pyproject.toml`, `PROGRESS.md` |
209
+ | broker-researcher | `brokers/registry.yaml`, `brokers/state_laws.yaml`, `brokers/SOURCES.md` |
210
+ | automation-engineer | `engine/`, `submitters/`, `templates/` |
211
+ | wizard-packager | `wizard/`, `scheduler/`, `notify.py`, `packaging/`, `.github/workflows/`, `FAMILY_QUICK_START.md` |
212
+ | qa-security | `tests/`, `tools/registry_health.py`, `docs/SECURITY_REVIEW.md` |
213
+
214
+ ### How a run works
215
+
216
+ For each consenting profile, the runner goes through enabled brokers by `priority` (highest first), one broker at a time:
217
+
218
+ 1. Load the `(profile, broker)` status. Scheduled runs skip brokers whose `next_check_at` is still in the future.
219
+ 2. **Search** through the broker's submitter, score the listings, then apply `state.after_scan` and `state.transition`.
220
+ 3. **Submit** if the state is `found` or `reappeared`, the listing is `CONFIRMED`, and this isn't a dry run. When a broker has `covered_by` set, the opt-out goes to the parent broker and this site is only re-scanned.
221
+ 4. **Verify** by polling IMAP for confirmation links on the broker's `confirmation_domains`.
222
+ 5. **Escalate** if the request has been ignored. Each rung becomes due `typical_processing_days + 15` days after the last submission or escalation: resubmit → follow-up email → legal demand (always approved by you) → complaint to-do. A removal resets the ladder.
223
+ 6. **Schedule the next check:** every 3–14 days while a request is pending, every `recheck_interval_days` once removed or not found, and at least weekly while something needs attention.
224
+
225
+ A failure at one broker never stops the run. CAPTCHAs and other challenges go to `HumanHandoff`. An interactive run opens a visible browser window and waits for you. A scheduled run creates a to-do and sends a notification without blocking.
226
+
227
+ Listing states: `unknown`, `not_found`, `found`, `submitted`, `awaiting_verification`, `confirmed`, `removed`, `reappeared`, `needs_human`, `error`. Allowed transitions are in `state.TRANSITIONS`.
228
+
229
+ ## Security and privacy model
230
+
231
+ **What's stored, and where.** Everything lives under `~/.freemyid/` (`%APPDATA%\Free My ID` on Windows), or under `FREEMYID_HOME` if set. The directory is created with mode `0700`.
232
+
233
+ | Path | Contents | Protection |
234
+ |---|---|---|
235
+ | `freemyid.db` | Profiles, statuses, listings, requests, to-dos, append-only event log | SQLite. Only ids, enums and timestamps are in clear text. Every model is an AES-256-GCM blob with AAD bound to (table, id). |
236
+ | `evidence/` | Screenshots, HTML, sent/received emails | Encrypted files. The dashboard decrypts them in memory only. |
237
+ | `artifacts/` | Generated letters, call scripts, reports | |
238
+ | `logs/` | Rotating logs | PII-redacted: known profile values plus email/phone patterns. Code logs ids and enums only. |
239
+ | `config.yaml` | Non-PII settings | Mode `0600` |
240
+ | `registry.yaml` | Cached registry update | Public data |
241
+
242
+ **Keys and passwords.** The database key is a random 256-bit key stored in the OS keychain (service `freemyid`) through `keyring`. Mailbox passwords also live only in the keychain (service `freemyid-mailbox`), never in the database or config. If the keychain is locked or unavailable (for example, in a headless session), Free My ID raises `KeyUnavailable` and surfaces it as needing a person; it does not fall back to plaintext.
243
+
244
+ **Family bundles.** `profile export` encrypts a profile with a passphrase (scrypt, then AES-GCM). Share the passphrase by phone, not in the same channel as the file. The family member reviews the imported data and gives their own consent.
245
+
246
+ **Network allowlist.** `NetPolicy` permits only:
247
+
248
+ - domains and confirmation domains of brokers in the registry
249
+ - the profile's own IMAP/SMTP hosts
250
+ - the host of `registry_url`
251
+ - hosts you add to `extra_allowed_hosts`
252
+ - `localhost` and `127.0.0.1`
253
+
254
+ Browser requests go through a Playwright route that aborts anything off the list. httpx, SMTP and IMAP calls check it first.
255
+
256
+ **No telemetry.** No analytics, crash reporting or update pings. The wizard and dashboard load no external assets (no CDNs or web fonts).
257
+
258
+ **Local web UI.** The wizard and dashboard bind to `127.0.0.1` only, use a random per-launch session token, and have CSRF protection on every POST.
259
+
260
+ **Data minimization.** A broker receives only the fields its listing already shows (`Listing.shown_fields`) plus the removal mailbox address. DOB, relatives and personal emails are used only for matching and are never sent. Phone numbers, ID documents and account creation each need their own permission flag.
261
+
262
+ **Registry updates** come only from your configured `registry_url`. They are validated against the Pydantic model and the JSON Schema, can be pinned to a `.sha256` file, and are swapped in atomically. An update that fails validation never replaces the working copy.
263
+
264
+ Security review findings are in `docs/SECURITY_REVIEW.md` (owned by qa-security).
265
+
266
+ ## Adding a broker
267
+
268
+ Most brokers need only a YAML entry. Python is for sites a declarative form can't handle.
269
+
270
+ ### 1. Registry entry (`brokers/registry.yaml`)
271
+
272
+ ```yaml
273
+ - id: examplepeople # ^[a-z0-9_]+$
274
+ name: ExamplePeople
275
+ url: https://www.examplepeople.test/
276
+ category: people_search # people_search | background_check | data_broker | risk_data | family_tree | phone_lookup | other
277
+ domains: [examplepeople.test] # every host the opt-out flow touches (feeds the allowlist)
278
+ search_url_pattern: "https://www.examplepeople.test/name/{first}-{last}/{state}" # only if you've seen the real shape
279
+ opt_out_url: https://www.examplepeople.test/optout
280
+ method: web_form # web_form | email | postal | phone | account_required
281
+ alt_methods: [email]
282
+ opt_out_email: privacy@examplepeople.test
283
+ required_fields: [full_name, listing_url, email]
284
+ needs_email_verification: true
285
+ confirmation_domains: [examplepeople.test]
286
+ has_captcha: false
287
+ needs_id_upload: false
288
+ typical_processing_days: 14
289
+ recheck_interval_days: 30
290
+ feeds_from: [] # broker ids this site copies from
291
+ covered_by: null # broker id whose opt-out also covers this site
292
+ steps:
293
+ - Search for your listing and copy its URL.
294
+ - Paste it into the opt-out form and enter the removal email.
295
+ - Click the link in the confirmation email.
296
+ search: # selectors only from real or saved HTML
297
+ result_selector: ".result-card"
298
+ name_selector: ".result-card h2"
299
+ age_selector: ".result-card .age"
300
+ location_selector: ".result-card .locations li"
301
+ relatives_selector: ".result-card .relatives li"
302
+ link_selector: ".result-card a.profile-link"
303
+ no_results_text: "No results found"
304
+ form:
305
+ - {action: goto, url: "https://www.examplepeople.test/optout"}
306
+ - {action: fill, selector: "#listing-url", value: "{{ listing.url }}"}
307
+ - {action: fill, selector: "#email", value: "{{ mailbox.address }}"}
308
+ - {action: screenshot}
309
+ - {action: submit, selector: "button[type=submit]"}
310
+ - {action: expect_text, value: "check your email"}
311
+ source_urls: [https://www.examplepeople.test/privacy]
312
+ verification_status: unverified # verified only after loading the live page yourself
313
+ verification_notes: "Not yet checked live."
314
+ priority: 50 # 0-100, higher runs first
315
+ ```
316
+
317
+ Form step actions: `goto`, `fill`, `select`, `check`, `click`, `wait_for`, `screenshot`, `submit`, `expect_text`, `human`. A `value` is a jinja2 template rendered with `profile`, `listing`, `mailbox` and `broker`. Only reference fields in `listing.shown_fields`, and use the mailbox address, never a personal email. A `human` step hands the page to a person.
318
+
319
+ Rules for registry entries:
320
+
321
+ - Never invent a URL, email, address or phone number. Cite `source_urls`.
322
+ - Set `verified` only after loading the live page.
323
+ - Leave `search` and `form` selectors empty until you have real or saved HTML.
324
+
325
+ Then validate the entry and regenerate the schemas if you changed models:
326
+
327
+ ```bash
328
+ .venv/bin/freemyid registry validate brokers/registry.yaml
329
+ .venv/bin/python tools/gen_schema.py # only after models.py changes
330
+ ```
331
+
332
+ ### 2. Optional custom submitter
333
+
334
+ If the declarative form isn't enough, subclass `freemyid.interfaces.Submitter`:
335
+
336
+ ```python
337
+ from freemyid.interfaces import RunContext, Submitter
338
+ from freemyid.models import Listing, SubmitOutcome
339
+
340
+ class ExamplePeopleSubmitter(Submitter):
341
+ method = "web_form"
342
+
343
+ async def search(self, ctx: RunContext) -> list[Listing]:
344
+ ... # never submits; screenshot each listing found via ctx.store.save_evidence
345
+
346
+ async def submit(self, ctx: RunContext, listings: list[Listing]) -> SubmitOutcome:
347
+ ... # never called in dry_run; send only listing.shown_fields + mailbox
348
+
349
+ async def verify(self, ctx: RunContext) -> SubmitOutcome | None:
350
+ ... # optional
351
+ ```
352
+
353
+ The engine looks a submitter up in this order:
354
+
355
+ 1. `submitter: "module:Class"` in the registry entry
356
+ 2. an entry point in the `freemyid.submitters` group, keyed by broker id (for third-party packages)
357
+ 3. a `freemyid.submitters.<broker_id>` module containing a class named `Submitter`
358
+ 4. the generic submitter for `broker.method`
359
+
360
+ Write the search parser against saved HTML in `tests/fixtures/<broker_id>/`. If you couldn't test it against the live site, mark it `UNTESTED_AGAINST_LIVE`. Every submitter must follow the rules in the `Submitter` docstring:
361
+
362
+ - no CAPTCHA bypass
363
+ - data minimization
364
+ - `CONFIRMED`-only auto-submit
365
+ - screenshot evidence
366
+ - page text treated as data
367
+
368
+ ## Contributing
369
+
370
+ ### Ground rules
371
+
372
+ These apply to people and agents alike:
373
+
374
+ - **No AI or API keys at runtime.** Everything stays deterministic and local.
375
+ - **No PII anywhere it could leak:** logs, events, exceptions, fixtures, tests or commits. Use fake people only, e.g. "Jane Q Testperson" of Springfield, IL.
376
+ - **No network calls outside `NetPolicy`.** No telemetry.
377
+ - **Never bypass a CAPTCHA.** No solver services and no stealth plugins.
378
+ - **During development:** don't send real email, submit real opt-outs, or install scheduler jobs or browsers system-wide. Use `--dry-run`, fixtures and fakes.
379
+ - **Before a PR:** `.venv/bin/pytest -q` and `.venv/bin/ruff check src tests` must pass. Run `freemyid registry validate` if you touched the registry.
380
+
381
+ ### Agent workflow
382
+
383
+ Free My ID is built by an orchestrator session plus four specialist Claude Code agents (`.claude/agents/`). AI is used only to *build* Free My ID, never inside it at runtime.
384
+
385
+ 1. Every agent reads `.claude/agents/_shared-rules.md` first.
386
+ 2. Agents edit only files they own. To change anything else, an agent files a **CHANGE REQUEST** (file, exact change, reason) in its report, and the orchestrator applies it.
387
+ 3. The contracts (`models.py`, `interfaces.py`, `state.py`, `storage/`, ...) are read-only for everyone except the orchestrator.
388
+ 4. Agents draft tests in `_test_drafts/` folders, and qa-security adopts them into `tests/`.
389
+ 5. qa-security reviews every change and returns **PASS** or **BLOCK**. Anything that weakens a safety gate is a BLOCK.
390
+ 6. A finished task's report lists the files changed, how to test them, the test results, open questions and change requests.
391
+ 7. The orchestrator integrates, updates `PROGRESS.md` and signs off. These need explicit human approval: system-wide installs, the first real submission, any real email, and any `git push`.
392
+
393
+ ## License
394
+
395
+ - **Code:** GNU AGPL v3 or later (see [LICENSE](LICENSE)).
396
+ - **Broker data in `brokers/`** (`registry.sample.yaml`, `state_laws.yaml`): CC BY-NC 4.0, non-commercial use with credit. See [brokers/LICENSE-DATA.md](brokers/LICENSE-DATA.md).
@@ -0,0 +1,100 @@
1
+ freemyid/__init__.py,sha256=Ppe8t4_LIQMqliAibmUZ8Vvf4oqQbxMPxSQ6TVjyYnc,83
2
+ freemyid/cli.py,sha256=f7KKQQM2XK-28Yh1AAY7cDC-JXnFg9bRrLfRYc921eo,28467
3
+ freemyid/cli_json.py,sha256=4jhspczCmERDmvwmGPy4vpz1hQFhC-_v2CLps3Fmmgs,7243
4
+ freemyid/config.py,sha256=tqkOr24Zs9YcNkc9V5Use_hRTdcgJ3Pj4_HOrd7HyI8,1627
5
+ freemyid/consent.py,sha256=VyFtZkMazExzFitmrU7PCIHGtJqxcXKZDz8Ios9Vlow,3495
6
+ freemyid/crypto.py,sha256=eGquFk8tvTNxUvjOAkwII_bJQOy_D-Wugd3q9-rUXkQ,7703
7
+ freemyid/doctor.py,sha256=uRaYMUHYrAd5GojNTZhngaiyGUtIPVpjjeYRS5IfGYM,16049
8
+ freemyid/interfaces.py,sha256=n3wo22KxEWEfjruf6WUU_30JLEWU-z7jCfy7lHore7I,7605
9
+ freemyid/logging_setup.py,sha256=ovcuLBykf4Tv4DB1yHaUlRVShVWgY_J8lGXsbsW8og0,6224
10
+ freemyid/models.py,sha256=ZaTNIPc1DpEKNuuoOJd28rK0NlezFwIftDUErqcdXpw,26825
11
+ freemyid/netpolicy.py,sha256=9i03uQIPXNwjG0djqBVlpMOtML-HaOUcXjuo_OP0g1k,5885
12
+ freemyid/notify.py,sha256=VxptWJWZ0FxF36GPJ6Z7n6Iby-AtKppCV9bUgrjt05c,2060
13
+ freemyid/paths.py,sha256=lCG2ik7aJpdfE66xSrlztGJ24Qx7M_dANHfFv9nsb-g,2751
14
+ freemyid/registry.py,sha256=MxLVQheSMbQMTxv-MMXVSiPhZlEP4a6LPZAi9gAgBh8,11024
15
+ freemyid/secret_backends.py,sha256=82aBW_0Y-Lb5UmkpV0GbYIXhfRkxoaMKxmXn2UvAD1Y,22135
16
+ freemyid/secrets.py,sha256=C3FolVTpt6BkBh6Asx3OjI_OnRkEEGHKSnAFwWwX9Uo,965
17
+ freemyid/setup_config.py,sha256=BumtvdaJWxeqreAcqGZVOE4KR8sUG9hRBAUrpq6SRXA,13792
18
+ freemyid/state.py,sha256=vYCTZcGBJTf96qbqtlSPdxnz0OQVZ99_vJdTR0DQEno,6262
19
+ freemyid/engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ freemyid/engine/browser.py,sha256=QIJhGsWYxOBWoUrD4-T-yn1OiSABKHKdXt2Beb91-pw,19270
21
+ freemyid/engine/fill_for_me.py,sha256=Sn8EPktoWsxQjoaJl1kijrCkamkKr21WBaRi-09cXls,28118
22
+ freemyid/engine/filler.py,sha256=sp113IWiB_fJ66w3i3sPQG081H0Z2nFeu3_xGpvsOik,23352
23
+ freemyid/engine/handoff.py,sha256=Wc1Z94dAl5L1iVuFzetx-kTvzQeXY7vm4Y2UekV9eXE,10386
24
+ freemyid/engine/letters.py,sha256=3_-EZxCnVSIxUY2JmFW1QFlTmBqDg5CGMAwjaO2xdEU,5805
25
+ freemyid/engine/listing_finder.py,sha256=29HyECv1peMyunh7uirPjVMRKB0At2EyBXj3VvW-8YU,23900
26
+ freemyid/engine/mail.py,sha256=aOedaFlsvYkoZ6xkY-s8R75eL4N5HSs-R8DE6DZJnvk,17155
27
+ freemyid/engine/mail_tls.py,sha256=Qs8nTxNzTnEm6nYTwuleTLdseTzk663U8_lIO8DmatQ,9379
28
+ freemyid/engine/matcher.py,sha256=LITOQ3YW-eqDtLylB5SkveLvHjtDezGb-nk-VbOxQJM,7938
29
+ freemyid/engine/outbox.py,sha256=dVgGlUipHSMtXiRG3heG4vjCrkl-M0ZvZiJ3AKDTSZY,19311
30
+ freemyid/engine/plugins.py,sha256=NUzMHadgnCu-2MOWJjK8lO9H_gLTsLJK5iFaYCLgf44,2521
31
+ freemyid/engine/runner.py,sha256=RVvEHdy_GILvkrRW-xSEWZVekdVxA7Yn8HiMlYoLnIg,39449
32
+ freemyid/engine/tiers.py,sha256=xRR-93O5OLNTrtt38ARlI_7ddsFYtSEV0O5I9M1cU80,4191
33
+ freemyid/engine/_dev/__init__.py,sha256=RfheX78Wf-aOg1DLdOfYOacvjMbZqVNqtdu5j2JkSUs,3625
34
+ freemyid/engine/_dev/devrun.py,sha256=ekDxjTn5ORrRGALeQcQVoMd_r6IrTlj4DSZkA-MN2S4,4516
35
+ freemyid/engine/_dev/registry.yaml,sha256=t4jxXx1Mpee_bdoB8NCiEZ4PMLPgwv5E7KhvHb--6Es,4136
36
+ freemyid/engine/_dev/inbox/devauto.eml,sha256=L0U-DwqE2MI4Qj5W7U0BIsAfkExulKNyD9SvWDkq4xA,311
37
+ freemyid/engine/_dev/site/devauto.example/confirm.html,sha256=yofiBZ6OqBvSNPKWUFMbODX6L8y_E7xDHjV3EH3aGuQ,93
38
+ freemyid/engine/_dev/site/devauto.example/optout.html,sha256=cuCBtO9liSARrd_XJNVrslMnaPzc0MTlTLbj8r9s5EA,254
39
+ freemyid/engine/_dev/site/devauto.example/name/jane-testperson/il.html,sha256=s9MM_hYV20QKkx99kIbnmafaRszEPGNw10Ck9vHr3kE,696
40
+ freemyid/engine/_dev/site/devauto.example/name/jane-testperson/nc.html,sha256=ZtVMHuOLhcs80Dg0AbGvbv0F-iwhoiBpxRvGJoDGFBE,161
41
+ freemyid/engine/_dev/site/devauto.example/name/jane-testperson/oh.html,sha256=Vbi6ES_aYkRGdF8I6gl2CCGBPIRARBn7WusOctQV41o,185
42
+ freemyid/engine/_dev/site/devauto.example/optout/done.html,sha256=-zlDuYpaGV5pSZDAFnM6hBapinhKjYUmsf8ZMd8IP-U,107
43
+ freemyid/engine/_dev/site/devblocked.example/optout.html,sha256=2THCF1Dv7fZQ0vjQ6Cg3dUbaKxM9WbuYeYKJ1ct6dWw,194
44
+ freemyid/engine/_dev/site/devblocked.example/name/jane-testperson/il.html,sha256=2THCF1Dv7fZQ0vjQ6Cg3dUbaKxM9WbuYeYKJ1ct6dWw,194
45
+ freemyid/engine/_dev/site/devfill.example/ajax.html,sha256=4zCYdgRewpE065oX37vbrrlvc8FA0MEHlzL6lHjAMh8,918
46
+ freemyid/engine/_dev/site/devfill.example/aria.html,sha256=lc5GCT9wmhRpVG1TQKDwxtohGUlVwEkpMmWqhEFczYA,719
47
+ freemyid/engine/_dev/site/devfill.example/consent.html,sha256=Wf_dqT9w3JZX7VISHukWpZ0g2LJv-80mrJ3MV4iyT9w,1230
48
+ freemyid/engine/_dev/site/devfill.example/done.html,sha256=_MLjuuLa-T2EabsKjqqevr_uK8VPVfTmc-qUl2ZPPfI,180
49
+ freemyid/engine/_dev/site/devfill.example/empty.html,sha256=_J6m2AJX4uH1Cl1xF_QEcjwd6Wbo4JRyPKXMIoKC11U,94
50
+ freemyid/engine/_dev/site/devfill.example/fullname.html,sha256=ArRNbzgnp98lhU9aPxfMtjMVXXLvUhJkkmuNgyTGY3Y,626
51
+ freemyid/engine/_dev/site/devfill.example/labels.html,sha256=W0-bUdGAEBGGbcE03Mr4auYfFpIBdpyofW1RChpsGng,1798
52
+ freemyid/engine/_dev/site/devfill.example/late.html,sha256=vJzqs8Nam9AKF9r6IEj7dtsoVa3XlJfLUzlh_LaWSrg,1004
53
+ freemyid/engine/_dev/site/devfind.example/hostile.html,sha256=MgjELACJTv3uhiMnLvo5UYGbTudZxCbk_DzokOXG9gk,499
54
+ freemyid/engine/_dev/site/devfind.example/results.html,sha256=BAYELt2iIrj8kQbBfEvhtVdyvHOVKRjB2cCW4R65qWA,592
55
+ freemyid/engine/_dev/site/devfind.example/search.html,sha256=ervqWxLHDmmObkcvjlF5moaEKAYKFsDVyxDxYFuxyQw,713
56
+ freemyid/engine/_dev/site/devfind.example/p/jane-testperson-shelbyville-il.html,sha256=sX3UwzAGrXVAjfGRAw64pgNCWXGXmVvrCfaDaU46n-o,311
57
+ freemyid/engine/_dev/site/devfind.example/p/jane-testperson-springfield-il.html,sha256=udiWkkWG5WK2uIj_7JlApS3halkAppMRj43K5Z1RTtM,337
58
+ freemyid/engine/_dev/site/devweb.example/remove.html,sha256=FhsaeShj1BKLodVQmSKVwbW6XL50LPZiG5K9v8PmynI,97
59
+ freemyid/scheduler/__init__.py,sha256=KzqJlVLabQIt0KVy0BaDJCwRdlCn9NbTKHSc9ExbEJ4,8272
60
+ freemyid/scheduler/render.py,sha256=D7H33a3HazxWYb_KKjpiT7LdKxfZRD0hk3panZ0mjz8,4749
61
+ freemyid/storage/__init__.py,sha256=2RZhK4dUCDWh6_1BrRLkeXMC1PLebQQQZNMVJU4EfhE,43
62
+ freemyid/storage/db.py,sha256=Bjy3rqKKggc7SmRnw6KeJFneYXRE9Y7CXH1mM3Ldh1Y,17807
63
+ freemyid/submitters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
+ freemyid/submitters/assisted.py,sha256=vg8yOSADAJeBrkRAtdp5bVfKDFhg97n4s7eE9rkYqys,8217
65
+ freemyid/submitters/base.py,sha256=_iaZy4Wilx9YH7hiezkW5uTxgCgyntx51hdGt-jZF-0,5402
66
+ freemyid/submitters/email.py,sha256=pOrmoVh11A3ZC6I6RyabGqEvX-Tzzom3m3Yr9BlaaSQ,2167
67
+ freemyid/submitters/web_form.py,sha256=WyqLUJ0eL8fR_lZSm_6a-NLax_QOEO_tvUvT2BqNT6k,11993
68
+ freemyid/templates/_identity.txt.j2,sha256=riDwwfaODV_a2cvjQw0FfJtX3cSxhE6UhByOb_2lbIk,606
69
+ freemyid/templates/_signature.txt.j2,sha256=IcPA0R4o18eTZRUST4HQ3Nf4Buey3h9d3X2-tb6T9QU,212
70
+ freemyid/templates/ccpa.txt.j2,sha256=tpkLCBTDs9kDKc53Kilzz3hP3xIs4CJw5V2xvY-uFFk,802
71
+ freemyid/templates/follow_up.txt.j2,sha256=OVG9Sti1jMQM7xnoTnjolkMp9j22diNlNVdbwhrvc-A,748
72
+ freemyid/templates/generic_removal.txt.j2,sha256=0d9OPaPyCZHr0PHQFEvEE_w_FDRgflBcaMfUX_TkypE,415
73
+ freemyid/templates/legal_demand.txt.j2,sha256=BDgn-Wa7C48a3A0ErylkNzpbxQRYfgGnSb3V3eo9eoQ,932
74
+ freemyid/templates/phone_script.txt.j2,sha256=KNbGx5aYwtjcB7dOjwYXdMkfGcjm_5nNzVCaVEojxRA,1163
75
+ freemyid/templates/postal_letter.txt.j2,sha256=imyFu-vRV3ryA2z-tNmGC3IQcA1_x65sjBAWg2bCH9U,550
76
+ freemyid/templates/state_privacy.txt.j2,sha256=KVT4rPndrjo7ShdB5k_QgK0ugNRVcRPWK1FKoGPUsMU,749
77
+ freemyid/wizard/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
+ freemyid/wizard/dashboard.py,sha256=GrJv6iIHwU2ha59NZYKlnqvP043XFboTGh0X7mqUmf4,22532
79
+ freemyid/wizard/desktop.py,sha256=gHgXb-ninc2_JrEx3bfhz_nkQ5YfUaaoKn0KSdwaYso,6830
80
+ freemyid/wizard/draft.py,sha256=XRbTUBVrBQScE5jZZ6hcaxwpe7nNmTjKzi7OchE4ujE,27962
81
+ freemyid/wizard/firstrun.py,sha256=3dvsZl3jvAxUSLb_8TKfKBDldr1JYz7FNfClL4sAbi0,1526
82
+ freemyid/wizard/flows.py,sha256=c4UAUEIqwhuk23093RvrfHyG3ma5H1MnFmBFHTBlzkk,11213
83
+ freemyid/wizard/report.py,sha256=DA3PCO27gsn0N6iNBosyEPXD4MdmCNLFq1xFKrs857Y,4134
84
+ freemyid/wizard/server.py,sha256=vN7rtv8qi0LTy94Vix1qcx4opBBhBXWtkafv0kKLugQ,36482
85
+ freemyid/wizard/ui.py,sha256=15lYBdox7DOMiA3bsawYVCM_x59wg1uYc3_Xeh9d3RI,20060
86
+ freemyid/wizard/static/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
+ freemyid/wizard/static/app.js,sha256=4ReYuHbyhzkL-s764Er7u2HJyzkKFQI1i9LWcmvutAw,4087
88
+ freemyid/wizard/static/freemyid.png,sha256=0x73Q2Uw2uPsEvsXDE937REMypYh9vKrojXVPqBZbnk,11143
89
+ freemyid/wizard/static/style.css,sha256=SvmbNubWRHf7EIrwo_PjbMekNtzti_Sj-33KzcpqJQ0,5378
90
+ freemyid/_data/schema/registry.schema.json,sha256=h5F79tfoG9bKfKwpqG9Sj5kTDrvT1TONUxPZaAV_lgA,15689
91
+ freemyid/_data/schema/setup-config.schema.json,sha256=FnAb-dam2BdEXOajyL_pqoLdR0zKJDFcsSo2_X1f0xI,12669
92
+ freemyid/_data/schema/state_laws.schema.json,sha256=MXQGQcKtpVtVlZWjWF9alPunOM9ez5llH91GwPB020E,4765
93
+ freemyid/_data/brokers/LICENSE-DATA.md,sha256=rqrXSz15lGW-jvosfZdIWGHYh3xb4j05ITcpDt4pf9g,1404
94
+ freemyid/_data/brokers/registry.sample.yaml,sha256=60ret4HlbsX4lYSV46FAftfhj2bYDg6MOizM3gxpabM,40252
95
+ freemyid/_data/brokers/state_laws.yaml,sha256=mybu-szvnXNI9tp8HU8_OzIQLchyPisca7mDSOVSf6I,27862
96
+ freemyid-0.2.0.dev1.dist-info/METADATA,sha256=9nvRP77jTFp9Hfom9dzmmrRki2mn6oRSt2a2wqFqSGQ,24409
97
+ freemyid-0.2.0.dev1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
98
+ freemyid-0.2.0.dev1.dist-info/entry_points.txt,sha256=tSnFk2bCUOEK3LY5ZcYPjtwu5aHiP3yncdFdVflh6Jg,46
99
+ freemyid-0.2.0.dev1.dist-info/licenses/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
100
+ freemyid-0.2.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ freemyid = freemyid.cli:app