orangecheck 0.1.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.
@@ -0,0 +1,38 @@
1
+ # Dependencies
2
+ node_modules
3
+ **/node_modules
4
+
5
+ # Build outputs
6
+ dist
7
+ **/dist
8
+ *.tsbuildinfo
9
+
10
+ # Python
11
+ __pycache__
12
+ *.pyc
13
+ .pytest_cache
14
+ .venv
15
+ **/*.egg-info
16
+
17
+ # Test + coverage
18
+ coverage
19
+ .nyc_output
20
+
21
+ # Tool caches
22
+ .turbo
23
+ .cache
24
+
25
+ # OS
26
+ .DS_Store
27
+ *.swp
28
+
29
+ # Secrets
30
+ .env
31
+ .env.local
32
+ .env.*.local
33
+
34
+ # Logs
35
+ *.log
36
+ npm-debug.log*
37
+ yarn-debug.log*
38
+ yarn-error.log*
@@ -0,0 +1,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: orangecheck
3
+ Version: 0.1.0
4
+ Summary: Python SDK for OrangeCheck — proof of Bitcoin stake for the open web.
5
+ Project-URL: Homepage, https://ochk.io
6
+ Project-URL: Documentation, https://ochk.io/docs
7
+ Project-URL: Repository, https://github.com/orangecheck/oc-web
8
+ Project-URL: Issues, https://github.com/orangecheck/oc-web/issues
9
+ Author: OrangeCheck
10
+ License: MIT
11
+ Keywords: attestation,bip322,bitcoin,nostr,orangecheck,sybil-resistance
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
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: Topic :: Internet :: WWW/HTTP
22
+ Classifier: Topic :: Security
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: httpx>=0.25
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.0; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: respx>=0.20; extra == 'dev'
31
+ Requires-Dist: ruff>=0.1; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # `orangecheck` — Python SDK
35
+
36
+ **Proof of Bitcoin stake for the open web.** Python SDK for OrangeCheck.
37
+
38
+ A sybil-resistance primitive any Python app can consume in one call. Works with Django, Flask, FastAPI, Starlette, or a plain script.
39
+
40
+ ```bash
41
+ pip install orangecheck
42
+ ```
43
+
44
+ ---
45
+
46
+ ## The 30-second integration
47
+
48
+ ```python
49
+ from orangecheck import check
50
+
51
+ result = check(addr="bc1q...", min_sats=100_000, min_days=30)
52
+
53
+ if result.ok:
54
+ # let them through
55
+ ...
56
+ else:
57
+ print("rejected:", result.reasons)
58
+ ```
59
+
60
+ That's it. `check()` queries the hosted `ochk.io` API, which discovers the most recent attestation for the address on Nostr, verifies the Bitcoin signature, recomputes live chain state, and compares against your thresholds.
61
+
62
+ ---
63
+
64
+ ## Load-bearing functions
65
+
66
+ ```python
67
+ from orangecheck import check, verify, discover, challenge_issue, challenge_verify
68
+
69
+ # Gate
70
+ check(addr="bc1q...", min_sats=100_000, min_days=30)
71
+ check(id="a3f5b8c2...", min_sats=100_000)
72
+ check(identity="github:alice", min_sats=50_000)
73
+
74
+ # Verify a raw attestation
75
+ verify(addr="bc1q...", msg=canonical_message, sig=signature)
76
+
77
+ # List attestations for a subject
78
+ discover(addr="bc1q...", limit=10)
79
+
80
+ # Signed-challenge auth (prove address control)
81
+ ch = challenge_issue(addr="bc1q...", audience="https://example.com")
82
+ verified = challenge_verify(message=ch.message, signature=user_sig, expected_nonce=ch.nonce)
83
+ ```
84
+
85
+ All return typed dataclasses. All raise `OrangeCheckError` (or subclasses) on transport / server errors.
86
+
87
+ ---
88
+
89
+ ## Django integration
90
+
91
+ ```python
92
+ # views.py
93
+ from django.http import HttpResponse, JsonResponse
94
+ from orangecheck import check
95
+
96
+ def gated_post(request):
97
+ addr = request.session.get("btc_address")
98
+ if not addr:
99
+ return HttpResponse(status=401)
100
+ result = check(addr=addr, min_sats=100_000, min_days=30)
101
+ if not result.ok:
102
+ return JsonResponse({"error": "orangecheck", "reasons": result.reasons}, status=403)
103
+ # ... proceed
104
+ return JsonResponse({"ok": True})
105
+ ```
106
+
107
+ ### As middleware
108
+
109
+ ```python
110
+ # orangecheck_middleware.py
111
+ from django.http import JsonResponse
112
+ from orangecheck import check
113
+
114
+ class OrangeCheckMiddleware:
115
+ def __init__(self, get_response):
116
+ self.get_response = get_response
117
+
118
+ def __call__(self, request):
119
+ if request.path.startswith("/protected/"):
120
+ addr = request.session.get("btc_address")
121
+ r = check(addr=addr, min_sats=100_000, min_days=30)
122
+ if not r.ok:
123
+ return JsonResponse({"error": r.reasons}, status=403)
124
+ return self.get_response(request)
125
+ ```
126
+
127
+ ---
128
+
129
+ ## FastAPI integration
130
+
131
+ ```python
132
+ from fastapi import FastAPI, Depends, HTTPException
133
+ from orangecheck import AsyncClient
134
+
135
+ app = FastAPI()
136
+ oc = AsyncClient()
137
+
138
+ async def require_stake(addr: str, min_sats: int = 100_000, min_days: int = 30):
139
+ r = await oc.check(addr=addr, min_sats=min_sats, min_days=min_days)
140
+ if not r.ok:
141
+ raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
142
+ return r
143
+
144
+ @app.post("/post")
145
+ async def post_comment(gate = Depends(require_stake)):
146
+ return {"ok": True, "sats": gate.sats}
147
+ ```
148
+
149
+ ---
150
+
151
+ ## Flask integration
152
+
153
+ ```python
154
+ from functools import wraps
155
+ from flask import Flask, request, jsonify
156
+ from orangecheck import check
157
+
158
+ app = Flask(__name__)
159
+
160
+ def require_orangecheck(min_sats=100_000, min_days=30):
161
+ def decorator(fn):
162
+ @wraps(fn)
163
+ def wrapper(*args, **kwargs):
164
+ addr = request.headers.get("X-OC-Address")
165
+ r = check(addr=addr, min_sats=min_sats, min_days=min_days)
166
+ if not r.ok:
167
+ return jsonify(error="orangecheck", reasons=r.reasons), 403
168
+ return fn(*args, **kwargs)
169
+ return wrapper
170
+ return decorator
171
+
172
+ @app.post("/post")
173
+ @require_orangecheck(min_sats=100_000, min_days=30)
174
+ def post_comment():
175
+ return {"ok": True}
176
+ ```
177
+
178
+ ---
179
+
180
+ ## Async
181
+
182
+ Every function has a sync counterpart on `AsyncClient`:
183
+
184
+ ```python
185
+ import asyncio
186
+ from orangecheck import AsyncClient
187
+
188
+ async def main():
189
+ async with AsyncClient() as oc:
190
+ r = await oc.check(addr="bc1q...", min_sats=100_000)
191
+ print(r.ok, r.sats, r.days)
192
+
193
+ asyncio.run(main())
194
+ ```
195
+
196
+ ---
197
+
198
+ ## Configuration
199
+
200
+ ```python
201
+ from orangecheck import Client
202
+
203
+ # Default — hits https://ochk.io
204
+ c = Client()
205
+
206
+ # Self-hosted verifier, custom timeout
207
+ c = Client(base_url="https://verifier.mycompany.com", timeout=5.0)
208
+
209
+ # Reuse an existing httpx Client (connection pooling, auth, etc.)
210
+ import httpx
211
+ my_session = httpx.Client(proxies="http://proxy.example.com")
212
+ c = Client(session=my_session)
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Types
218
+
219
+ Every response is a frozen `dataclass` with predictable fields:
220
+
221
+ ```python
222
+ @dataclass(frozen=True)
223
+ class CheckResult:
224
+ ok: bool
225
+ sats: int
226
+ days: int
227
+ score: float
228
+ attestation_id: str | None
229
+ address: str | None
230
+ identities: tuple[IdentityBinding, ...]
231
+ network: Literal["mainnet", "testnet", "signet"] | None
232
+ reasons: tuple[str, ...]
233
+ ```
234
+
235
+ Full type information ships with the package (`py.typed` marker, tested with mypy strict).
236
+
237
+ ---
238
+
239
+ ## Errors
240
+
241
+ - **`OrangeCheckError`** — base class for everything else.
242
+ - **`RateLimitError`** — the hosted API returned 429.
243
+ - **`VerificationError`** — `challenge_verify` failed (bad signature, expired, nonce mismatch, …).
244
+
245
+ `check()` treats 404 (no attestation found) as `CheckResult(ok=False, reasons=("not_found",))` rather than raising — callers should gate on `.ok`, not on try/except.
246
+
247
+ ---
248
+
249
+ ## Shell smoke-test
250
+
251
+ ```bash
252
+ python -m orangecheck check --addr bc1q... --min-sats 100000
253
+ ```
254
+
255
+ Exits `0` on pass, `2` on fail. Prefer the TypeScript `oc` CLI for richer output; this one is here mostly to verify the install.
256
+
257
+ ---
258
+
259
+ ## License
260
+
261
+ MIT. The OrangeCheck protocol is CC-BY-4.0.
@@ -0,0 +1,228 @@
1
+ # `orangecheck` — Python SDK
2
+
3
+ **Proof of Bitcoin stake for the open web.** Python SDK for OrangeCheck.
4
+
5
+ A sybil-resistance primitive any Python app can consume in one call. Works with Django, Flask, FastAPI, Starlette, or a plain script.
6
+
7
+ ```bash
8
+ pip install orangecheck
9
+ ```
10
+
11
+ ---
12
+
13
+ ## The 30-second integration
14
+
15
+ ```python
16
+ from orangecheck import check
17
+
18
+ result = check(addr="bc1q...", min_sats=100_000, min_days=30)
19
+
20
+ if result.ok:
21
+ # let them through
22
+ ...
23
+ else:
24
+ print("rejected:", result.reasons)
25
+ ```
26
+
27
+ That's it. `check()` queries the hosted `ochk.io` API, which discovers the most recent attestation for the address on Nostr, verifies the Bitcoin signature, recomputes live chain state, and compares against your thresholds.
28
+
29
+ ---
30
+
31
+ ## Load-bearing functions
32
+
33
+ ```python
34
+ from orangecheck import check, verify, discover, challenge_issue, challenge_verify
35
+
36
+ # Gate
37
+ check(addr="bc1q...", min_sats=100_000, min_days=30)
38
+ check(id="a3f5b8c2...", min_sats=100_000)
39
+ check(identity="github:alice", min_sats=50_000)
40
+
41
+ # Verify a raw attestation
42
+ verify(addr="bc1q...", msg=canonical_message, sig=signature)
43
+
44
+ # List attestations for a subject
45
+ discover(addr="bc1q...", limit=10)
46
+
47
+ # Signed-challenge auth (prove address control)
48
+ ch = challenge_issue(addr="bc1q...", audience="https://example.com")
49
+ verified = challenge_verify(message=ch.message, signature=user_sig, expected_nonce=ch.nonce)
50
+ ```
51
+
52
+ All return typed dataclasses. All raise `OrangeCheckError` (or subclasses) on transport / server errors.
53
+
54
+ ---
55
+
56
+ ## Django integration
57
+
58
+ ```python
59
+ # views.py
60
+ from django.http import HttpResponse, JsonResponse
61
+ from orangecheck import check
62
+
63
+ def gated_post(request):
64
+ addr = request.session.get("btc_address")
65
+ if not addr:
66
+ return HttpResponse(status=401)
67
+ result = check(addr=addr, min_sats=100_000, min_days=30)
68
+ if not result.ok:
69
+ return JsonResponse({"error": "orangecheck", "reasons": result.reasons}, status=403)
70
+ # ... proceed
71
+ return JsonResponse({"ok": True})
72
+ ```
73
+
74
+ ### As middleware
75
+
76
+ ```python
77
+ # orangecheck_middleware.py
78
+ from django.http import JsonResponse
79
+ from orangecheck import check
80
+
81
+ class OrangeCheckMiddleware:
82
+ def __init__(self, get_response):
83
+ self.get_response = get_response
84
+
85
+ def __call__(self, request):
86
+ if request.path.startswith("/protected/"):
87
+ addr = request.session.get("btc_address")
88
+ r = check(addr=addr, min_sats=100_000, min_days=30)
89
+ if not r.ok:
90
+ return JsonResponse({"error": r.reasons}, status=403)
91
+ return self.get_response(request)
92
+ ```
93
+
94
+ ---
95
+
96
+ ## FastAPI integration
97
+
98
+ ```python
99
+ from fastapi import FastAPI, Depends, HTTPException
100
+ from orangecheck import AsyncClient
101
+
102
+ app = FastAPI()
103
+ oc = AsyncClient()
104
+
105
+ async def require_stake(addr: str, min_sats: int = 100_000, min_days: int = 30):
106
+ r = await oc.check(addr=addr, min_sats=min_sats, min_days=min_days)
107
+ if not r.ok:
108
+ raise HTTPException(status_code=403, detail={"reasons": list(r.reasons)})
109
+ return r
110
+
111
+ @app.post("/post")
112
+ async def post_comment(gate = Depends(require_stake)):
113
+ return {"ok": True, "sats": gate.sats}
114
+ ```
115
+
116
+ ---
117
+
118
+ ## Flask integration
119
+
120
+ ```python
121
+ from functools import wraps
122
+ from flask import Flask, request, jsonify
123
+ from orangecheck import check
124
+
125
+ app = Flask(__name__)
126
+
127
+ def require_orangecheck(min_sats=100_000, min_days=30):
128
+ def decorator(fn):
129
+ @wraps(fn)
130
+ def wrapper(*args, **kwargs):
131
+ addr = request.headers.get("X-OC-Address")
132
+ r = check(addr=addr, min_sats=min_sats, min_days=min_days)
133
+ if not r.ok:
134
+ return jsonify(error="orangecheck", reasons=r.reasons), 403
135
+ return fn(*args, **kwargs)
136
+ return wrapper
137
+ return decorator
138
+
139
+ @app.post("/post")
140
+ @require_orangecheck(min_sats=100_000, min_days=30)
141
+ def post_comment():
142
+ return {"ok": True}
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Async
148
+
149
+ Every function has a sync counterpart on `AsyncClient`:
150
+
151
+ ```python
152
+ import asyncio
153
+ from orangecheck import AsyncClient
154
+
155
+ async def main():
156
+ async with AsyncClient() as oc:
157
+ r = await oc.check(addr="bc1q...", min_sats=100_000)
158
+ print(r.ok, r.sats, r.days)
159
+
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Configuration
166
+
167
+ ```python
168
+ from orangecheck import Client
169
+
170
+ # Default — hits https://ochk.io
171
+ c = Client()
172
+
173
+ # Self-hosted verifier, custom timeout
174
+ c = Client(base_url="https://verifier.mycompany.com", timeout=5.0)
175
+
176
+ # Reuse an existing httpx Client (connection pooling, auth, etc.)
177
+ import httpx
178
+ my_session = httpx.Client(proxies="http://proxy.example.com")
179
+ c = Client(session=my_session)
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Types
185
+
186
+ Every response is a frozen `dataclass` with predictable fields:
187
+
188
+ ```python
189
+ @dataclass(frozen=True)
190
+ class CheckResult:
191
+ ok: bool
192
+ sats: int
193
+ days: int
194
+ score: float
195
+ attestation_id: str | None
196
+ address: str | None
197
+ identities: tuple[IdentityBinding, ...]
198
+ network: Literal["mainnet", "testnet", "signet"] | None
199
+ reasons: tuple[str, ...]
200
+ ```
201
+
202
+ Full type information ships with the package (`py.typed` marker, tested with mypy strict).
203
+
204
+ ---
205
+
206
+ ## Errors
207
+
208
+ - **`OrangeCheckError`** — base class for everything else.
209
+ - **`RateLimitError`** — the hosted API returned 429.
210
+ - **`VerificationError`** — `challenge_verify` failed (bad signature, expired, nonce mismatch, …).
211
+
212
+ `check()` treats 404 (no attestation found) as `CheckResult(ok=False, reasons=("not_found",))` rather than raising — callers should gate on `.ok`, not on try/except.
213
+
214
+ ---
215
+
216
+ ## Shell smoke-test
217
+
218
+ ```bash
219
+ python -m orangecheck check --addr bc1q... --min-sats 100000
220
+ ```
221
+
222
+ Exits `0` on pass, `2` on fail. Prefer the TypeScript `oc` CLI for richer output; this one is here mostly to verify the install.
223
+
224
+ ---
225
+
226
+ ## License
227
+
228
+ MIT. The OrangeCheck protocol is CC-BY-4.0.
@@ -0,0 +1,60 @@
1
+ """
2
+ orangecheck — Python SDK for OrangeCheck.
3
+
4
+ Proof of Bitcoin stake for the open web. A sybil-resistance primitive for
5
+ Django, Flask, FastAPI, or anything on the Python side of the web.
6
+
7
+ Three load-bearing functions, mirroring the JS SDK:
8
+
9
+ from orangecheck import check, verify, discover
10
+
11
+ result = check(addr="bc1q...", min_sats=100_000, min_days=30)
12
+ if result.ok:
13
+ ... # let them through
14
+
15
+ All functions are also available on the Client class for configuring a
16
+ custom API base URL, timeouts, or a shared httpx session:
17
+
18
+ from orangecheck import Client
19
+ oc = Client(base_url="https://my-orangecheck.example.com")
20
+ await oc.check_async(addr="bc1q...", min_sats=100_000)
21
+ """
22
+
23
+ from .client import AsyncClient, Client
24
+ from .errors import OrangeCheckError, RateLimitError, VerificationError
25
+ from .top_level import challenge_issue, challenge_verify, check, discover, verify
26
+ from .types import (
27
+ AttestationSummary,
28
+ Challenge,
29
+ ChallengeVerified,
30
+ CheckResult,
31
+ DiscoverResult,
32
+ IdentityBinding,
33
+ VerifyOutcome,
34
+ )
35
+
36
+ __all__ = [
37
+ # Top-level functions (sync)
38
+ "check",
39
+ "verify",
40
+ "discover",
41
+ "challenge_issue",
42
+ "challenge_verify",
43
+ # Clients
44
+ "Client",
45
+ "AsyncClient",
46
+ # Types
47
+ "CheckResult",
48
+ "VerifyOutcome",
49
+ "DiscoverResult",
50
+ "Challenge",
51
+ "ChallengeVerified",
52
+ "IdentityBinding",
53
+ "AttestationSummary",
54
+ # Errors
55
+ "OrangeCheckError",
56
+ "RateLimitError",
57
+ "VerificationError",
58
+ ]
59
+
60
+ __version__ = "0.1.0"
@@ -0,0 +1,68 @@
1
+ """``python -m orangecheck`` — a tiny CLI, mostly for quick sanity checks.
2
+
3
+ Prefer the TypeScript ``oc`` CLI (``npm install -g @orangecheck/cli``) for
4
+ day-to-day use — it has more features. This one is here so Python users
5
+ can smoke-test the package after ``pip install``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+
14
+ from . import __version__, check
15
+
16
+
17
+ def main() -> int:
18
+ parser = argparse.ArgumentParser(prog="orangecheck", description="OrangeCheck Python SDK")
19
+ parser.add_argument("--version", action="version", version=f"orangecheck {__version__}")
20
+
21
+ sub = parser.add_subparsers(dest="cmd")
22
+
23
+ c = sub.add_parser("check", help="Gate on an OrangeCheck proof")
24
+ c.add_argument("--addr")
25
+ c.add_argument("--id")
26
+ c.add_argument("--identity")
27
+ c.add_argument("--min-sats", type=int, default=0)
28
+ c.add_argument("--min-days", type=int, default=0)
29
+ c.add_argument("--base-url", default=None)
30
+
31
+ args = parser.parse_args()
32
+
33
+ if args.cmd == "check":
34
+ result = check(
35
+ addr=args.addr,
36
+ id=args.id,
37
+ identity=args.identity,
38
+ min_sats=args.min_sats,
39
+ min_days=args.min_days,
40
+ base_url=args.base_url,
41
+ )
42
+ json.dump(
43
+ {
44
+ "ok": result.ok,
45
+ "sats": result.sats,
46
+ "days": result.days,
47
+ "score": result.score,
48
+ "attestation_id": result.attestation_id,
49
+ "address": result.address,
50
+ "identities": [
51
+ {"protocol": i.protocol, "identifier": i.identifier}
52
+ for i in result.identities
53
+ ],
54
+ "network": result.network,
55
+ "reasons": list(result.reasons),
56
+ },
57
+ sys.stdout,
58
+ indent=2,
59
+ )
60
+ sys.stdout.write("\n")
61
+ return 0 if result.ok else 2
62
+
63
+ parser.print_help()
64
+ return 1
65
+
66
+
67
+ if __name__ == "__main__":
68
+ raise SystemExit(main())