t2em 0.6.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.
t2em-0.6.0/.gitignore ADDED
@@ -0,0 +1,67 @@
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ lerna-debug.log*
8
+
9
+ # Diagnostic reports (https://nodejs.org/api/report.html)
10
+ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11
+
12
+ # Runtime data
13
+ pids
14
+ *.pid
15
+ *.seed
16
+ *.pid.lock
17
+
18
+ # Coverage directory
19
+ coverage
20
+ *.lcov
21
+ .nyc_output
22
+
23
+ # Dependency directories
24
+ node_modules/
25
+
26
+ # TypeScript cache
27
+ *.tsbuildinfo
28
+
29
+ # Optional npm cache directory
30
+ .npm
31
+
32
+ # Optional eslint cache
33
+ .eslintcache
34
+
35
+ # REPL history
36
+ .node_repl_history
37
+
38
+ # Output of 'npm pack'
39
+ *.tgz
40
+
41
+ # dotenv environment variable files — NEVER commit secrets
42
+ # (Upstash REST tokens, Wrangler secrets live here in development)
43
+ .env
44
+ .env.*
45
+ !.env.example
46
+
47
+ # Build output
48
+ dist
49
+ .output
50
+
51
+ # Wrangler / Cloudflare local state
52
+ .wrangler/
53
+
54
+ # pnpm
55
+ .pnpm-store
56
+
57
+ # Vite files
58
+ vite.config.js.timestamp-*
59
+ vite.config.ts.timestamp-*
60
+ .vite/
61
+
62
+ # NOTE: unlike me2em-protocol, docs/ is NOT ignored here:
63
+ # docs/ holds the source TZ documents (TZ_t2em.md, TZ_we2em.md).
64
+
65
+ # Python bytecode
66
+ __pycache__/
67
+ *.pyc
t2em-0.6.0/PKG-INFO ADDED
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.5
2
+ Name: t2em
3
+ Version: 0.6.0
4
+ Summary: Python SDK for t2em.com — zero-knowledge secret drops (Blind Relay) for LangGraph / CrewAI / AutoGen pipelines
5
+ Project-URL: Homepage, https://t2em.com
6
+ Project-URL: Documentation, https://t2em.com/docs/python-sdk.md
7
+ Project-URL: Source, https://github.com/me2em-org/t2em
8
+ Author: Me2eM ecosystem
9
+ License: Apache-2.0
10
+ Keywords: aes-gcm,agents,e2ee,secrets,x25519,zero-knowledge
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Security :: Cryptography
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: cryptography>=41.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == 'dev'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # t2em — Python SDK (PyPI)
28
+
29
+ Python client for [t2em.com](https://t2em.com) for LangGraph / CrewAI /
30
+ AutoGen pipelines (ТЗ `docs/TZ_t2em.md` §8.3).
31
+
32
+ **Status: implemented (Этап 4 done).** Zero-knowledge secret drops —
33
+ the relay stores only an AES-256-GCM envelope and can never derive the
34
+ key.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install t2em # from PyPI (when published)
40
+ pip install -e . # from this repo (editable, dev)
41
+ ```
42
+
43
+ ## Usage (the ТЗ §8.3 contract)
44
+
45
+ ```python
46
+ from t2em import T2emClient, generate_x25519_keypair
47
+
48
+ client = T2emClient("https://t2em.com")
49
+
50
+ # Mode B (symmetric — key rides in the URL #fragment, never sent to the server)
51
+ drop = client.push(secret="sk-live-...", ttl=300)
52
+ print(drop.url) # https://t2em.com/s/abc123#xyz789
53
+ print(drop.delete_token)
54
+ secret = client.pull_text(drop.url)
55
+
56
+ # Mode A (asymmetric — sealed to the recipient's X25519 pubkey)
57
+ keys = generate_x25519_keypair()
58
+ drop = client.push(secret="sk-live-...", recipient_pubkey=keys.public_key, ttl=300)
59
+ secret = client.pull_text(drop.url, recipient_privkey=keys.private_key)
60
+
61
+ # Delete before TTL expiry
62
+ client.delete(drop.id, drop.delete_token)
63
+
64
+ # Keyfile — same JSON format as the TS CLI (`t2em-cli keygen`)
65
+ from t2em import write_keyfile, load_keyfile
66
+ pubkey = write_keyfile("./agent.key") # 0600, returns b64url pubkey
67
+ key = load_keyfile("./agent.key") # LoadedKey(private_key, public_key)
68
+ ```
69
+
70
+ ## Guarantees
71
+
72
+ - Python ≥ 3.10, full type hints (`py.typed`).
73
+ - Networking: stdlib `urllib` only (no `requests`/`httpx`) — §8.3.
74
+ - Crypto: PyCA `cryptography` as the single crypto dependency
75
+ (hand-rolled crypto is forbidden, §11.2); HKDF strictly per RFC 5869.
76
+ - Bit-compatible with the TypeScript protocol `@me2em/protocol`:
77
+ verified against `vectors/t2em-drop.v1.json` (unpack AND deterministic
78
+ re-encryption, both modes) plus TS↔Python cross-pack/unpack checks.
79
+ - Transport is injectable (`T2emClient(base_url, transport=...)`) for
80
+ testing without sockets.
81
+ - Error taxonomy mirrors §4.5: branch on `ProtocolError.code`
82
+ (`MALFORMED` / `UNSUPPORTED_VERSION` / `DECRYPT_FAILED`), `UsageError`,
83
+ `HttpError` (status / code / retry_after).
84
+
85
+ ## Layout
86
+
87
+ ```
88
+ python-sdk/
89
+ ├── src/t2em/ # package (py.typed)
90
+ │ ├── protocol.py # constants: version, TTL bounds, HKDF info (freeze §4.5)
91
+ │ ├── base64url.py # strict RFC 4648 §5 unpadded codec
92
+ │ ├── envelope.py # v1 envelope validation (mirror of @me2em/protocol)
93
+ │ ├── mode_b.py # pack/unpack sym (AES-256-GCM, 12B IV)
94
+ │ ├── mode_a.py # pack/unpack X25519 ECDH + HKDF-SHA256
95
+ │ ├── fingerprint.py # 8-char pubkey fingerprint (§4.2.1)
96
+ │ ├── keyfile.py # t2em-x25519-key-v1 JSON keyfile (0600)
97
+ │ └── client.py # T2emClient: push / pull_text / pull_bytes / delete
98
+ └── tests/ # pytest: vectors bit-compat, client journeys, keyfile
99
+ ```
t2em-0.6.0/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # t2em — Python SDK (PyPI)
2
+
3
+ Python client for [t2em.com](https://t2em.com) for LangGraph / CrewAI /
4
+ AutoGen pipelines (ТЗ `docs/TZ_t2em.md` §8.3).
5
+
6
+ **Status: implemented (Этап 4 done).** Zero-knowledge secret drops —
7
+ the relay stores only an AES-256-GCM envelope and can never derive the
8
+ key.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install t2em # from PyPI (when published)
14
+ pip install -e . # from this repo (editable, dev)
15
+ ```
16
+
17
+ ## Usage (the ТЗ §8.3 contract)
18
+
19
+ ```python
20
+ from t2em import T2emClient, generate_x25519_keypair
21
+
22
+ client = T2emClient("https://t2em.com")
23
+
24
+ # Mode B (symmetric — key rides in the URL #fragment, never sent to the server)
25
+ drop = client.push(secret="sk-live-...", ttl=300)
26
+ print(drop.url) # https://t2em.com/s/abc123#xyz789
27
+ print(drop.delete_token)
28
+ secret = client.pull_text(drop.url)
29
+
30
+ # Mode A (asymmetric — sealed to the recipient's X25519 pubkey)
31
+ keys = generate_x25519_keypair()
32
+ drop = client.push(secret="sk-live-...", recipient_pubkey=keys.public_key, ttl=300)
33
+ secret = client.pull_text(drop.url, recipient_privkey=keys.private_key)
34
+
35
+ # Delete before TTL expiry
36
+ client.delete(drop.id, drop.delete_token)
37
+
38
+ # Keyfile — same JSON format as the TS CLI (`t2em-cli keygen`)
39
+ from t2em import write_keyfile, load_keyfile
40
+ pubkey = write_keyfile("./agent.key") # 0600, returns b64url pubkey
41
+ key = load_keyfile("./agent.key") # LoadedKey(private_key, public_key)
42
+ ```
43
+
44
+ ## Guarantees
45
+
46
+ - Python ≥ 3.10, full type hints (`py.typed`).
47
+ - Networking: stdlib `urllib` only (no `requests`/`httpx`) — §8.3.
48
+ - Crypto: PyCA `cryptography` as the single crypto dependency
49
+ (hand-rolled crypto is forbidden, §11.2); HKDF strictly per RFC 5869.
50
+ - Bit-compatible with the TypeScript protocol `@me2em/protocol`:
51
+ verified against `vectors/t2em-drop.v1.json` (unpack AND deterministic
52
+ re-encryption, both modes) plus TS↔Python cross-pack/unpack checks.
53
+ - Transport is injectable (`T2emClient(base_url, transport=...)`) for
54
+ testing without sockets.
55
+ - Error taxonomy mirrors §4.5: branch on `ProtocolError.code`
56
+ (`MALFORMED` / `UNSUPPORTED_VERSION` / `DECRYPT_FAILED`), `UsageError`,
57
+ `HttpError` (status / code / retry_after).
58
+
59
+ ## Layout
60
+
61
+ ```
62
+ python-sdk/
63
+ ├── src/t2em/ # package (py.typed)
64
+ │ ├── protocol.py # constants: version, TTL bounds, HKDF info (freeze §4.5)
65
+ │ ├── base64url.py # strict RFC 4648 §5 unpadded codec
66
+ │ ├── envelope.py # v1 envelope validation (mirror of @me2em/protocol)
67
+ │ ├── mode_b.py # pack/unpack sym (AES-256-GCM, 12B IV)
68
+ │ ├── mode_a.py # pack/unpack X25519 ECDH + HKDF-SHA256
69
+ │ ├── fingerprint.py # 8-char pubkey fingerprint (§4.2.1)
70
+ │ ├── keyfile.py # t2em-x25519-key-v1 JSON keyfile (0600)
71
+ │ └── client.py # T2emClient: push / pull_text / pull_bytes / delete
72
+ └── tests/ # pytest: vectors bit-compat, client journeys, keyfile
73
+ ```
@@ -0,0 +1,45 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "t2em"
7
+ version = "0.6.0"
8
+ description = "Python SDK for t2em.com — zero-knowledge secret drops (Blind Relay) for LangGraph / CrewAI / AutoGen pipelines"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ authors = [{ name = "Me2eM ecosystem" }]
13
+ keywords = ["secrets", "zero-knowledge", "e2ee", "agents", "x25519", "aes-gcm"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Security :: Cryptography",
24
+ "Typing :: Typed",
25
+ ]
26
+ # ТЗ §8.3: zero external NETWORK deps — stdlib urllib only.
27
+ # Crypto: PyCA cryptography as the single crypto dependency (§11.2:
28
+ # hand-rolled crypto is forbidden).
29
+ dependencies = ["cryptography>=41.0"]
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["pytest>=8"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://t2em.com"
36
+ Documentation = "https://t2em.com/docs/python-sdk.md"
37
+ Source = "https://github.com/me2em-org/t2em"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/t2em"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ pythonpath = ["src"]
45
+ addopts = "-q"
@@ -0,0 +1,74 @@
1
+ """t2em — Python SDK for t2em.com (ТЗ docs/TZ_t2em.md §8.3).
2
+
3
+ Zero-knowledge secret drops over a Blind Relay: the server stores only
4
+ an AES-256-GCM envelope and can never derive the key. Bit-compatible
5
+ with the TypeScript protocol `@t2em/protocol` — verified against
6
+ `vectors/t2em-drop.v1.json`.
7
+
8
+ Typical use::
9
+
10
+ from t2em import T2emClient, generate_x25519_keypair
11
+
12
+ client = T2emClient("https://t2em.com")
13
+
14
+ # Mode B (symmetric, key lives in the URL #fragment)
15
+ drop = client.push(secret="sk-live-...", ttl=300)
16
+ secret = client.pull_text(drop.url)
17
+
18
+ # Mode A (asymmetric, sealed to the recipient's X25519 pubkey)
19
+ keys = generate_x25519_keypair()
20
+ drop = client.push(secret="sk-live-...", recipient_pubkey=keys.public_key, ttl=300)
21
+ secret = client.pull_text(drop.url, recipient_privkey=keys.private_key)
22
+ """
23
+
24
+ from .base64url import b64u_decode, b64u_encode, is_b64u_of_length
25
+ from ._meta import SDK_VERSION, SDK_USER_AGENT
26
+ from .client import CreatedDrop, ShareUrl, T2emClient
27
+ from .envelope import Envelope, ModeAEnvelope, ModeBEnvelope, validate_envelope
28
+ from .exceptions import HttpError, ProtocolError, T2emError, UsageError
29
+ from .facade import pack_drop, unpack_drop
30
+ from .fingerprint import fingerprint_pubkey, generate_fingerprint
31
+ from .keyfile import KEYFILE_KIND, KEYFILE_TYPE, KEYFILE_VERSION, load_keyfile, write_keyfile
32
+ from .keys import X25519KeyPair, generate_x25519_keypair
33
+ from .protocol import (
34
+ HKDF_INFO_DROP_V1,
35
+ MAX_TTL_SECONDS,
36
+ MIN_TTL_SECONDS,
37
+ T2EM_PROTOCOL_VERSION,
38
+ )
39
+
40
+ __version__ = SDK_VERSION
41
+
42
+ __all__ = [
43
+ "CreatedDrop",
44
+ "Envelope",
45
+ "HKDF_INFO_DROP_V1",
46
+ "KEYFILE_KIND",
47
+ "KEYFILE_TYPE",
48
+ "KEYFILE_VERSION",
49
+ "MAX_TTL_SECONDS",
50
+ "MIN_TTL_SECONDS",
51
+ "ModeAEnvelope",
52
+ "ModeBEnvelope",
53
+ "ProtocolError",
54
+ "HttpError",
55
+ "SDK_USER_AGENT",
56
+ "SDK_VERSION",
57
+ "ShareUrl",
58
+ "T2EM_PROTOCOL_VERSION",
59
+ "T2emClient",
60
+ "T2emError",
61
+ "UsageError",
62
+ "X25519KeyPair",
63
+ "b64u_decode",
64
+ "b64u_encode",
65
+ "fingerprint_pubkey",
66
+ "generate_fingerprint",
67
+ "generate_x25519_keypair",
68
+ "is_b64u_of_length",
69
+ "load_keyfile",
70
+ "pack_drop",
71
+ "unpack_drop",
72
+ "validate_envelope",
73
+ "write_keyfile",
74
+ ]
@@ -0,0 +1,8 @@
1
+ """SDK identity metadata — single source of the version string."""
2
+
3
+ SDK_VERSION = "0.6.0"
4
+
5
+ #: Identifying User-Agent (Cloudflare browser-integrity checks reject
6
+ #: the bare `Python-urllib` default; the ecosystem is agent-friendly,
7
+ #: so clients identify themselves — ТЗ §5.5, §8.6).
8
+ SDK_USER_AGENT = f"t2em-python-sdk/{SDK_VERSION}"
@@ -0,0 +1,45 @@
1
+ """Base64URL helpers (RFC 4648 §5, no padding) — ТЗ §4.1.
2
+
3
+ Strict mirror of `@t2em/protocol` `base64url.ts`: the same alphabet,
4
+ the same rejection rules, so every wire string produced here is
5
+ byte-identical to the TypeScript implementation.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import re
12
+
13
+ from .exceptions import ProtocolError
14
+
15
+ _B64URL_ALPHABET = re.compile(r"^[A-Za-z0-9\-_]+$")
16
+
17
+
18
+ def b64u_encode(data: bytes) -> str:
19
+ """Encode bytes to unpadded base64url."""
20
+ return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
21
+
22
+
23
+ def b64u_decode(text: str) -> bytes:
24
+ """Throwing strict decoder: rejects any non-base64url alphabet or padding."""
25
+ if not isinstance(text, str) or len(text) == 0:
26
+ raise ProtocolError("MALFORMED", "base64url string must be non-empty")
27
+ if not _B64URL_ALPHABET.match(text):
28
+ raise ProtocolError("MALFORMED", "not base64url (RFC 4648 §5, unpadded)")
29
+ if len(text) % 4 == 1:
30
+ raise ProtocolError("MALFORMED", "invalid base64url length")
31
+ pad = "=" * (-len(text) % 4)
32
+ try:
33
+ return base64.urlsafe_b64decode(text + pad)
34
+ except Exception as exc: # binascii.Error and friends
35
+ raise ProtocolError("MALFORMED", f"invalid base64url: {exc}") from exc
36
+
37
+
38
+ def is_b64u_of_length(text: object, byte_length: int) -> bool:
39
+ """True when `text` is valid base64url decoding to exactly `byte_length` bytes."""
40
+ if not isinstance(text, str):
41
+ return False
42
+ try:
43
+ return len(b64u_decode(text)) == byte_length
44
+ except ProtocolError:
45
+ return False