init-data-py 0.2.7__tar.gz → 1.0.0__tar.gz

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 (45) hide show
  1. init_data_py-1.0.0/.gitattributes +1 -0
  2. init_data_py-1.0.0/.github/workflows/ci.yml +97 -0
  3. init_data_py-1.0.0/.github/workflows/publish.yml +31 -0
  4. init_data_py-1.0.0/CHANGELOG.md +75 -0
  5. init_data_py-1.0.0/PKG-INFO +279 -0
  6. init_data_py-1.0.0/README.md +247 -0
  7. init_data_py-1.0.0/pyproject.toml +118 -0
  8. init_data_py-1.0.0/src/init_data_py/__init__.py +41 -0
  9. init_data_py-1.0.0/src/init_data_py/_crypto.py +87 -0
  10. init_data_py-1.0.0/src/init_data_py/_ed25519.py +63 -0
  11. init_data_py-1.0.0/src/init_data_py/errors.py +217 -0
  12. init_data_py-1.0.0/src/init_data_py/models.py +434 -0
  13. init_data_py-1.0.0/src/init_data_py/parsing.py +221 -0
  14. init_data_py-1.0.0/src/init_data_py/signing.py +85 -0
  15. init_data_py-1.0.0/src/init_data_py/validation.py +284 -0
  16. init_data_py-1.0.0/tests/__init__.py +0 -0
  17. init_data_py-1.0.0/tests/test_forward_compat.py +65 -0
  18. init_data_py-1.0.0/tests/test_parse.py +58 -0
  19. init_data_py-1.0.0/tests/test_sign.py +80 -0
  20. init_data_py-1.0.0/tests/test_signature.py +219 -0
  21. init_data_py-1.0.0/tests/test_validate.py +140 -0
  22. init_data_py-1.0.0/tests/vectors.py +86 -0
  23. init_data_py-1.0.0/uv.lock +502 -0
  24. init_data_py-0.2.7/.github/workflows/publish.yml +0 -41
  25. init_data_py-0.2.7/PKG-INFO +0 -86
  26. init_data_py-0.2.7/README.md +0 -76
  27. init_data_py-0.2.7/pyproject.toml +0 -77
  28. init_data_py-0.2.7/requirements-dev.lock +0 -42
  29. init_data_py-0.2.7/requirements.lock +0 -12
  30. init_data_py-0.2.7/src/init_data_py/__init__.py +0 -5
  31. init_data_py-0.2.7/src/init_data_py/errors/__init__.py +0 -15
  32. init_data_py-0.2.7/src/init_data_py/errors/errors.py +0 -27
  33. init_data_py-0.2.7/src/init_data_py/init_data.py +0 -304
  34. init_data_py-0.2.7/src/init_data_py/types/__init__.py +0 -4
  35. init_data_py-0.2.7/src/init_data_py/types/chat.py +0 -41
  36. init_data_py-0.2.7/src/init_data_py/types/object.py +0 -34
  37. init_data_py-0.2.7/src/init_data_py/types/user.py +0 -65
  38. init_data_py-0.2.7/tests/test_parse.py +0 -29
  39. init_data_py-0.2.7/tests/test_sign.py +0 -33
  40. init_data_py-0.2.7/tests/test_to_query_string.py +0 -11
  41. init_data_py-0.2.7/tests/test_validate.py +0 -60
  42. {init_data_py-0.2.7 → init_data_py-1.0.0}/.gitignore +0 -0
  43. {init_data_py-0.2.7 → init_data_py-1.0.0}/.python-version +0 -0
  44. {init_data_py-0.2.7 → init_data_py-1.0.0}/LICENCE +0 -0
  45. /init_data_py-0.2.7/tests/__init__.py → /init_data_py-1.0.0/src/init_data_py/py.typed +0 -0
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,97 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ name: Test on Python ${{ matrix.python-version }}
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - name: Install uv
21
+ uses: astral-sh/setup-uv@v9.0.0
22
+ with:
23
+ enable-cache: true
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install project
27
+ run: uv sync --locked
28
+
29
+ # Run twice: the core must work with no dependencies at all, and the
30
+ # signature tests must run when the extra is present.
31
+ - name: Test without the ed25519 extra
32
+ run: uv run python -m unittest discover --verbose
33
+
34
+ - name: Test with the ed25519 extra
35
+ run: uv run --extra ed25519 python -m unittest discover --verbose
36
+
37
+ lint:
38
+ name: Lint
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v4
42
+
43
+ - name: Install uv
44
+ uses: astral-sh/setup-uv@v9.0.0
45
+ with:
46
+ enable-cache: true
47
+
48
+ - name: Install project
49
+ run: uv sync --locked
50
+
51
+ - name: Ruff check
52
+ run: uv run ruff check .
53
+
54
+ - name: Ruff format check
55
+ run: uv run ruff format --check .
56
+
57
+ types:
58
+ name: Type check
59
+ runs-on: ubuntu-latest
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+
63
+ - name: Install uv
64
+ uses: astral-sh/setup-uv@v9.0.0
65
+ with:
66
+ enable-cache: true
67
+
68
+ - name: Install project with the ed25519 extra
69
+ run: uv sync --locked --extra ed25519
70
+
71
+ - name: Mypy
72
+ run: uv run --no-sync mypy
73
+
74
+ build:
75
+ name: Build
76
+ runs-on: ubuntu-latest
77
+ steps:
78
+ - uses: actions/checkout@v4
79
+
80
+ - name: Install uv
81
+ uses: astral-sh/setup-uv@v9.0.0
82
+ with:
83
+ enable-cache: true
84
+
85
+ - name: Build distributions
86
+ run: uv build
87
+
88
+ - name: Check the wheel ships py.typed
89
+ run: |
90
+ python - <<'EOF'
91
+ import glob, zipfile
92
+
93
+ wheel = glob.glob("dist/*.whl")[0]
94
+ names = zipfile.ZipFile(wheel).namelist()
95
+ assert "init_data_py/py.typed" in names, names
96
+ print("py.typed present in", wheel)
97
+ EOF
@@ -0,0 +1,31 @@
1
+ name: Publish Python Package to PyPI
2
+
3
+ on:
4
+ release:
5
+ types:
6
+ - published
7
+
8
+ jobs:
9
+ publish:
10
+ name: Publish to PyPI
11
+ runs-on: ubuntu-latest
12
+
13
+ environment:
14
+ name: pypi
15
+ url: https://pypi.org/p/init-data-py
16
+ permissions:
17
+ id-token: write
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@v9.0.0
24
+ with:
25
+ enable-cache: true
26
+
27
+ - name: Build
28
+ run: uv build --no-sources
29
+
30
+ - name: Publish to PyPI
31
+ run: uv publish
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. This project follows
4
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [1.0.0] - 2026-09-06
7
+
8
+ A rewrite. See the migration table in the README for the old to new mapping.
9
+
10
+ ### Fixed
11
+
12
+ - **Hashes are computed from the query string as received.** Earlier versions
13
+ rebuilt the signed string from parsed fields, then patched over the
14
+ differences by escaping every slash in it. That produced hashes Telegram
15
+ would reject whenever a value held a slash, such as `start_param=ref/abc`,
16
+ and made the result depend on the order of keys inside the `user` JSON.
17
+ - **Unknown fields no longer raise.** A field this library does not recognise
18
+ used to raise `UnexpectedFormatError`, and an unrecognised field inside
19
+ `user` raised a bare `TypeError`. Both are now kept and readable through
20
+ `extra`, so a field Telegram adds later cannot break parsing.
21
+ - Hashes are compared with `hmac.compare_digest` instead of `!=`.
22
+ - A field sent more than once is rejected instead of silently taking the last
23
+ value, which could hash one value and hand another to the caller.
24
+ - Timestamps are timezone aware UTC rather than naive local time.
25
+ - A malformed `auth_date` raises `MalformedFieldError` instead of letting a
26
+ `ValueError` escape.
27
+ - Objects are hashable again. Defining `__eq__` without `__hash__` had made
28
+ them unusable in sets and as dict keys.
29
+
30
+ ### Added
31
+
32
+ - `validate_by_signature` and `is_valid_by_signature`, which verify Telegram's
33
+ Ed25519 `signature` field. For third parties that need to trust init data for
34
+ a bot whose token they do not have. Needs the new `ed25519` extra:
35
+ `pip install "init-data-py[ed25519]"`.
36
+ - `hash_token` and `token_hashed`, so a service can validate without ever
37
+ holding the bot token itself.
38
+ - `strip_auth_header`, for the `Authorization: tma <init data>` convention.
39
+ - `InitData.extra`, `InitData.get`, `User.extra` and `Chat.extra` for reading
40
+ fields this version does not know about.
41
+ - `InitData.issued_at`, `auth_date` as a timezone aware datetime.
42
+ - `is_ed25519_available` and `TELEGRAM_PUBLIC_KEYS`.
43
+ - A `py.typed` marker. Type checkers previously treated the package as
44
+ untyped despite it being fully annotated.
45
+
46
+ ### Changed
47
+
48
+ - **Init data now expires after one day by default.** It previously never
49
+ expired unless a lifetime was passed, so replay protection was something you
50
+ had to remember to switch on. Pass `expires_in=0` for the old behaviour.
51
+ - `validate_by_hash` returns the parsed init data, so there is no step where
52
+ parsed but unchecked data sits in a variable.
53
+ - `validate(raise_error=False)` is replaced by `is_valid_by_hash`. The return
54
+ type no longer depends on an argument. It reports `False` only for
55
+ unacceptable init data, and still raises on a mistake in the calling code.
56
+ - `sign` returns a new query string instead of mutating and returning `self`.
57
+ - `chat_type` and `Chat.type` are plain strings rather than a fixed set, since
58
+ Telegram can add values.
59
+ - `User` and `Chat` are frozen dataclasses.
60
+ - Errors carry structured data. `ExpiredError` has `issued_at`, `expires_at`
61
+ and `expired_for`; `SignatureInvalidError` has `bot_id` and `environment`.
62
+ - `MissingDependencyError` sits outside `InvalidInitDataError`, so a missing
63
+ optional dependency raises rather than being reported as invalid data.
64
+ - Requires Python 3.10 or newer.
65
+
66
+ ### Removed
67
+
68
+ - The `InitData` class API: `parse`, `from_query_string`, `validate`, `sign`,
69
+ `calculate_hash`, `to_dict`, `to_json` as methods.
70
+ - The `init_data_py.types` package. `User` and `Chat` come from the top level.
71
+ - `init_data_py.errors.errors`. Import from `init_data_py.errors`.
72
+
73
+ The old error names `InitDataPyError`, `UnexpectedFormatError`,
74
+ `SignMissingError` and `SignInvalidError` are kept as aliases of their
75
+ replacements, so existing `except` clauses keep working.
@@ -0,0 +1,279 @@
1
+ Metadata-Version: 2.5
2
+ Name: init-data-py
3
+ Version: 1.0.0
4
+ Summary: A Python library that provides tools for using and validating Telegram Mini App init data.
5
+ Project-URL: Homepage, https://github.com/nimaxin/init-data-py
6
+ Project-URL: Repository, https://github.com/nimaxin/init-data-py
7
+ Project-URL: Issues, https://github.com/nimaxin/init-data-py/issues
8
+ Project-URL: Changelog, https://github.com/nimaxin/init-data-py/blob/master/CHANGELOG.md
9
+ Author-email: nimaxin <nimaxin@proton.me>
10
+ License-Expression: MIT
11
+ License-File: LICENCE
12
+ Keywords: authentication,init-data,telegram,telegram-mini-app,telegram-web-app,validation,webappinitdata
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Programming Language :: Python :: Implementation :: CPython
23
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
24
+ Classifier: Topic :: Internet
25
+ Classifier: Topic :: Security
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Provides-Extra: ed25519
30
+ Requires-Dist: cryptography>=41.0; extra == 'ed25519'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # init-data-py
34
+
35
+ ![Package version](https://img.shields.io/pypi/v/init-data-py?color=%2334D058&label=pypi%20package)
36
+ ![Downloads](https://img.shields.io/pepy/dt/init-data-py)
37
+ ![Supported Python versions](https://img.shields.io/pypi/pyversions/init-data-py)
38
+ ![License](https://img.shields.io/github/license/nimaxin/init-data-py)
39
+
40
+ Parse, validate and sign [Telegram Mini App init data](https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app).
41
+
42
+ Init data is the signed payload a Mini App receives in
43
+ `window.Telegram.WebApp.initData` and sends to your backend. Anyone can post a
44
+ made up query string to that backend, so the signature has to be checked before
45
+ anything inside it is trusted.
46
+
47
+ ## Installation
48
+
49
+ ```bash
50
+ pip install init-data-py
51
+ ```
52
+
53
+ The core has no dependencies. Validating with Telegram's public key instead of
54
+ your bot token needs one extra:
55
+
56
+ ```bash
57
+ pip install "init-data-py[ed25519]"
58
+ ```
59
+
60
+ Requires Python 3.10 or newer.
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ from init_data_py import validate_by_hash
66
+
67
+ init_data = validate_by_hash(query_string, bot_token)
68
+ print(init_data.user.id)
69
+ ```
70
+
71
+ `validate_by_hash` returns the parsed init data and raises if anything is
72
+ wrong, so there is never a moment where you are holding data you have not
73
+ checked yet.
74
+
75
+ ## Validating
76
+
77
+ ### With your bot token
78
+
79
+ The usual case, for a backend that owns the bot.
80
+
81
+ ```python
82
+ from init_data_py import errors, validate_by_hash
83
+
84
+ try:
85
+ init_data = validate_by_hash(query_string, bot_token)
86
+ except errors.ExpiredError as error:
87
+ ... # error.issued_at, error.expires_at, error.expired_for
88
+ except errors.InvalidInitDataError:
89
+ ... # anything else wrong with it
90
+ ```
91
+
92
+ Init data expires after one day by default. Pass `expires_in` to change it, or
93
+ `0` to turn the check off. Turning it off lets an old query string be replayed
94
+ forever, so prefer a short lifetime.
95
+
96
+ ```python
97
+ from datetime import timedelta
98
+
99
+ validate_by_hash(query_string, bot_token, expires_in=3600)
100
+ validate_by_hash(query_string, bot_token, expires_in=timedelta(hours=1))
101
+ validate_by_hash(query_string, bot_token, expires_in=0) # no expiry check
102
+ ```
103
+
104
+ If you only want a boolean, use `is_valid_by_hash`. It returns `False` when the
105
+ init data is unacceptable, but still raises if your own call was wrong, so a
106
+ mistake in your code never looks like a rejected user.
107
+
108
+ ```python
109
+ from init_data_py import is_valid_by_hash
110
+
111
+ if not is_valid_by_hash(query_string, bot_token):
112
+ raise PermissionError
113
+ ```
114
+
115
+ ### Keeping the bot token out of the service
116
+
117
+ `hash_token` turns a bot token into the digest that validation actually uses.
118
+ Store that instead of the token and pass `token_hashed=True`.
119
+
120
+ ```python
121
+ from init_data_py import hash_token, validate_by_hash
122
+
123
+ digest = hash_token(bot_token) # do this once, somewhere safe
124
+ validate_by_hash(query_string, digest, token_hashed=True)
125
+ ```
126
+
127
+ ### Without the bot token
128
+
129
+ A third party that needs to trust init data for a bot it does not own can
130
+ verify Telegram's Ed25519 signature instead. This needs the `ed25519` extra,
131
+ and only clients from Bot API 8.0 onwards send the `signature` field.
132
+
133
+ ```python
134
+ from init_data_py import validate_by_signature
135
+
136
+ init_data = validate_by_signature(query_string, bot_id)
137
+ init_data = validate_by_signature(query_string, bot_id, environment="test")
138
+ ```
139
+
140
+ `is_valid_by_signature` is the boolean form. If the extra is not installed,
141
+ both raise `MissingDependencyError` rather than reporting a bad signature, so a
142
+ packaging mistake can never be mistaken for a forged request. Use
143
+ `is_ed25519_available()` to check for the extra at startup instead of finding
144
+ out on the first request. The keys themselves are in `TELEGRAM_PUBLIC_KEYS`.
145
+
146
+ ## Parsing without validating
147
+
148
+ `parse` only reads the query string. It proves nothing about where the data
149
+ came from, so do not trust anything it returns until a validator has passed.
150
+
151
+ ```python
152
+ from init_data_py import parse
153
+
154
+ init_data = parse(query_string)
155
+ init_data.user # a User, or None
156
+ init_data.chat # a Chat, or None
157
+ init_data.issued_at # timezone aware UTC datetime
158
+ init_data.to_query_string() # exactly the string that was parsed
159
+ ```
160
+
161
+ Fields this version does not know about are kept rather than rejected, so a
162
+ field Telegram adds later cannot break parsing:
163
+
164
+ ```python
165
+ init_data.get("some_new_field") # raw value from the query string
166
+ init_data.extra # every field with no attribute of its own
167
+ init_data.user.extra # the same, inside user and chat
168
+ ```
169
+
170
+ ## Reading the Authorization header
171
+
172
+ Mini App backends usually receive init data as `Authorization: tma <init data>`.
173
+
174
+ ```python
175
+ from init_data_py import strip_auth_header, validate_by_hash
176
+
177
+ raw = strip_auth_header(request.headers["Authorization"])
178
+ init_data = validate_by_hash(raw, bot_token)
179
+ ```
180
+
181
+ ## Signing
182
+
183
+ Useful for tests and fixtures. Real init data comes from Telegram.
184
+
185
+ ```python
186
+ from init_data_py import UnsignedInitData, User, sign
187
+
188
+ query_string = sign(
189
+ UnsignedInitData(user=User(id=5167898484, first_name="xin")),
190
+ bot_token,
191
+ )
192
+ ```
193
+
194
+ `sign` returns a new query string and changes nothing in place. It produces
195
+ `auth_date` and `hash` itself, so passing either one in is an error. A mapping
196
+ or a list of pairs works too, when you want to control the exact strings.
197
+
198
+ ## Errors
199
+
200
+ Everything raised by this library inherits from `InitDataError`. Everything
201
+ caused by bad init data inherits from `InvalidInitDataError`, which is exactly
202
+ what the `is_valid_*` functions turn into `False`.
203
+
204
+ ```
205
+ InitDataError
206
+ ├── InvalidInitDataError
207
+ │ ├── ParseError
208
+ │ │ ├── MalformedQueryStringError
209
+ │ │ ├── MalformedAuthHeaderError
210
+ │ │ ├── DuplicateFieldError
211
+ │ │ ├── MissingFieldError
212
+ │ │ │ ├── AuthDateMissingError
213
+ │ │ │ └── HashMissingError
214
+ │ │ └── MalformedFieldError
215
+ │ ├── AuthenticityError
216
+ │ │ ├── HashInvalidError
217
+ │ │ ├── SignatureMissingError
218
+ │ │ └── SignatureInvalidError
219
+ │ └── ExpiredError
220
+ └── MissingDependencyError
221
+ ```
222
+
223
+ `MissingDependencyError` sits outside `InvalidInitDataError` on purpose. A
224
+ missing optional dependency raises instead of quietly reporting invalid data.
225
+
226
+ ## Migrating from 0.2.x
227
+
228
+ Version 1.0 replaces the `InitData` class with functions. The old class
229
+ rebuilt the signed string from parsed fields, which produced hashes Telegram
230
+ would reject whenever a value held a slash or the JSON key order differed.
231
+
232
+ | 0.2.x | 1.0 |
233
+ | --- | --- |
234
+ | `InitData.parse(qs)` | `parse(qs)` |
235
+ | `InitData.from_query_string(qs)` | `parse(qs)` |
236
+ | `init_data.validate(token)` | `validate_by_hash(qs, token)` |
237
+ | `init_data.validate(token, raise_error=False)` | `is_valid_by_hash(qs, token)` |
238
+ | `init_data.validate(token, lifetime=3600)` | `validate_by_hash(qs, token, expires_in=3600)` |
239
+ | `InitData(user=user).sign(token)` | `sign(UnsignedInitData(user=user), token)` |
240
+ | `init_data.calculate_hash(token)` | not public, use `validate_by_hash` |
241
+ | `from init_data_py.types import User` | `from init_data_py import User` |
242
+ | `errors.InitDataPyError` | `errors.InitDataError` |
243
+ | `errors.UnexpectedFormatError` | `errors.ParseError` |
244
+ | `errors.SignMissingError` | `errors.HashMissingError` |
245
+ | `errors.SignInvalidError` | `errors.HashInvalidError` |
246
+
247
+ The old error names are kept as aliases of the new classes, so existing
248
+ `except` clauses keep working.
249
+
250
+ Two behaviour changes worth knowing:
251
+
252
+ - **Init data now expires by default.** 0.2.x accepted init data of any age
253
+ unless you passed `lifetime`. Pass `expires_in=0` for the old behaviour.
254
+ - **Unknown fields no longer raise.** 0.2.x rejected any field it did not
255
+ recognise, which broke every time Telegram added one.
256
+
257
+ ## Development
258
+
259
+ This project uses [uv](https://docs.astral.sh/uv/).
260
+
261
+ ```bash
262
+ uv sync --extra ed25519 # set up the environment
263
+ uv run python -m unittest discover --verbose # run the tests
264
+ uv run ruff check . # lint
265
+ uv run ruff format . # format
266
+ uv run mypy # type check
267
+ uv build # build the distributions
268
+ ```
269
+
270
+ The test suite must also pass without the extra, since the core is meant to
271
+ have no dependencies:
272
+
273
+ ```bash
274
+ uv sync && uv run python -m unittest discover
275
+ ```
276
+
277
+ ## License
278
+
279
+ This library is licensed under the [MIT License](LICENCE).