vercre-sdk 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.
- vercre_sdk-1.0.0/.gitignore +43 -0
- vercre_sdk-1.0.0/PKG-INFO +141 -0
- vercre_sdk-1.0.0/README.md +108 -0
- vercre_sdk-1.0.0/pyproject.toml +52 -0
- vercre_sdk-1.0.0/src/vercre_sdk/__init__.py +36 -0
- vercre_sdk-1.0.0/src/vercre_sdk/_transport.py +82 -0
- vercre_sdk-1.0.0/src/vercre_sdk/async_client.py +274 -0
- vercre_sdk-1.0.0/src/vercre_sdk/client.py +275 -0
- vercre_sdk-1.0.0/src/vercre_sdk/exceptions.py +46 -0
- vercre_sdk-1.0.0/src/vercre_sdk/models.py +121 -0
- vercre_sdk-1.0.0/src/vercre_sdk/py.typed +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
*.egg
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
.env
|
|
12
|
+
*.pem
|
|
13
|
+
*.key
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
htmlcov/
|
|
19
|
+
node_modules/
|
|
20
|
+
artifacts/
|
|
21
|
+
cache/
|
|
22
|
+
nohup.out
|
|
23
|
+
|
|
24
|
+
# sops / age secrets
|
|
25
|
+
keys.txt
|
|
26
|
+
secrets.json
|
|
27
|
+
|
|
28
|
+
# Generated key files (contain private keys — never commit)
|
|
29
|
+
issuer.json
|
|
30
|
+
subject.json
|
|
31
|
+
|
|
32
|
+
# Issued credential files (contain signed JWTs — never commit)
|
|
33
|
+
credential.json
|
|
34
|
+
kyc.json
|
|
35
|
+
aml.json
|
|
36
|
+
|
|
37
|
+
# Frontend
|
|
38
|
+
# frontend/
|
|
39
|
+
.vercel
|
|
40
|
+
|
|
41
|
+
# Database hardening
|
|
42
|
+
certs/
|
|
43
|
+
backups/
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vercre-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python SDK for the VERCRE Verifiable Credentials API
|
|
5
|
+
Project-URL: Homepage, https://vercre.vectorguardlabs.com
|
|
6
|
+
Project-URL: Documentation, https://vercre.vectorguardlabs.com/docs/sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/pavondunbar/Verified-Credentials-SDK
|
|
8
|
+
Project-URL: Issues, https://github.com/pavondunbar/Verified-Credentials-SDK/issues
|
|
9
|
+
Author: Pavon Dunbar
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: api-client,did,sdk,vercre,verifiable-credentials,w3c
|
|
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.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Security :: Cryptography
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx<1.0,>=0.25.0
|
|
25
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
26
|
+
Provides-Extra: async
|
|
27
|
+
Requires-Dist: anyio>=4.0; extra == 'async'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# vercre-sdk
|
|
35
|
+
|
|
36
|
+
Python SDK for the [VERCRE](https://vercre.vectorguardlabs.com) Verifiable Credentials API.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install vercre-sdk
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from vercre_sdk import VercreClient
|
|
48
|
+
|
|
49
|
+
client = VercreClient(
|
|
50
|
+
api_key="vercre_admin_your_key_here",
|
|
51
|
+
base_url="https://api.vercre.io",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Issue a credential
|
|
55
|
+
credential = client.issue_credential(
|
|
56
|
+
issuer_did="did:key:z6Mk...",
|
|
57
|
+
subject_did="did:key:z6Mp...",
|
|
58
|
+
credential_type="KYCBasic",
|
|
59
|
+
claims={"name": "Alice Johnson", "country": "US"},
|
|
60
|
+
)
|
|
61
|
+
print(f"Issued: {credential.credential_id}")
|
|
62
|
+
|
|
63
|
+
# Verify a credential
|
|
64
|
+
result = client.verify_credential(credential=credential.credential)
|
|
65
|
+
print(f"Valid: {result.valid}, Decision: {result.compliance_decision}")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Async Usage
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import asyncio
|
|
72
|
+
from vercre_sdk import AsyncVercreClient
|
|
73
|
+
|
|
74
|
+
async def main():
|
|
75
|
+
async with AsyncVercreClient(api_key="vercre_admin_xxx") as client:
|
|
76
|
+
result = await client.verify_credential(credential=my_credential)
|
|
77
|
+
print(result.compliance_decision)
|
|
78
|
+
|
|
79
|
+
asyncio.run(main())
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Error Handling
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from vercre_sdk import VercreClient, RateLimitError, AuthenticationError
|
|
86
|
+
|
|
87
|
+
client = VercreClient(api_key="...")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = client.verify_credential(credential=cred)
|
|
91
|
+
except RateLimitError as e:
|
|
92
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
93
|
+
except AuthenticationError:
|
|
94
|
+
print("Invalid API key")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Available Methods
|
|
98
|
+
|
|
99
|
+
### Credentials
|
|
100
|
+
- `issue_credential()` - Issue a new verifiable credential
|
|
101
|
+
- `verify_credential()` - Verify a credential with compliance scoring
|
|
102
|
+
- `revoke_credential()` - Revoke a credential
|
|
103
|
+
- `get_credential_status()` - Check on-chain attestation status
|
|
104
|
+
- `get_credential()` - Look up credential by ID
|
|
105
|
+
- `list_credentials()` - List credentials by subject DID
|
|
106
|
+
|
|
107
|
+
### Verification
|
|
108
|
+
- `verify_presentation()` - Verify a W3C Verifiable Presentation
|
|
109
|
+
|
|
110
|
+
### Issuers
|
|
111
|
+
- `get_issuer()` - Get issuer details
|
|
112
|
+
- `list_issuers()` - List registered issuers
|
|
113
|
+
- `check_issuer_can_issue()` - Check credential type authorization
|
|
114
|
+
|
|
115
|
+
### Disputes
|
|
116
|
+
- `file_dispute()` - File a dispute against a credential
|
|
117
|
+
- `get_dispute()` - Get dispute details
|
|
118
|
+
- `list_disputes()` - List disputes
|
|
119
|
+
|
|
120
|
+
### Reputation
|
|
121
|
+
- `get_reputation()` - Get issuer reputation score
|
|
122
|
+
- `submit_feedback()` - Submit verifier feedback
|
|
123
|
+
|
|
124
|
+
### Webhooks
|
|
125
|
+
- `register_webhook()` - Register a webhook endpoint
|
|
126
|
+
- `list_webhooks()` - List registered webhooks
|
|
127
|
+
- `delete_webhook()` - Remove a webhook
|
|
128
|
+
|
|
129
|
+
### Billing
|
|
130
|
+
- `get_subscription()` - Get current plan
|
|
131
|
+
- `get_usage()` - Get usage and remaining quota
|
|
132
|
+
- `subscribe()` - Change billing plan
|
|
133
|
+
- `get_billing_portal_url()` - Self-service billing management
|
|
134
|
+
|
|
135
|
+
### Keys
|
|
136
|
+
- `rotate_key()` - Rotate signing key for a DID
|
|
137
|
+
- `list_key_versions()` - List key version history
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# vercre-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for the [VERCRE](https://vercre.vectorguardlabs.com) Verifiable Credentials API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install vercre-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from vercre_sdk import VercreClient
|
|
15
|
+
|
|
16
|
+
client = VercreClient(
|
|
17
|
+
api_key="vercre_admin_your_key_here",
|
|
18
|
+
base_url="https://api.vercre.io",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Issue a credential
|
|
22
|
+
credential = client.issue_credential(
|
|
23
|
+
issuer_did="did:key:z6Mk...",
|
|
24
|
+
subject_did="did:key:z6Mp...",
|
|
25
|
+
credential_type="KYCBasic",
|
|
26
|
+
claims={"name": "Alice Johnson", "country": "US"},
|
|
27
|
+
)
|
|
28
|
+
print(f"Issued: {credential.credential_id}")
|
|
29
|
+
|
|
30
|
+
# Verify a credential
|
|
31
|
+
result = client.verify_credential(credential=credential.credential)
|
|
32
|
+
print(f"Valid: {result.valid}, Decision: {result.compliance_decision}")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Async Usage
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from vercre_sdk import AsyncVercreClient
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
async with AsyncVercreClient(api_key="vercre_admin_xxx") as client:
|
|
43
|
+
result = await client.verify_credential(credential=my_credential)
|
|
44
|
+
print(result.compliance_decision)
|
|
45
|
+
|
|
46
|
+
asyncio.run(main())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Error Handling
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from vercre_sdk import VercreClient, RateLimitError, AuthenticationError
|
|
53
|
+
|
|
54
|
+
client = VercreClient(api_key="...")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
result = client.verify_credential(credential=cred)
|
|
58
|
+
except RateLimitError as e:
|
|
59
|
+
print(f"Rate limited. Retry after {e.retry_after}s")
|
|
60
|
+
except AuthenticationError:
|
|
61
|
+
print("Invalid API key")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Available Methods
|
|
65
|
+
|
|
66
|
+
### Credentials
|
|
67
|
+
- `issue_credential()` - Issue a new verifiable credential
|
|
68
|
+
- `verify_credential()` - Verify a credential with compliance scoring
|
|
69
|
+
- `revoke_credential()` - Revoke a credential
|
|
70
|
+
- `get_credential_status()` - Check on-chain attestation status
|
|
71
|
+
- `get_credential()` - Look up credential by ID
|
|
72
|
+
- `list_credentials()` - List credentials by subject DID
|
|
73
|
+
|
|
74
|
+
### Verification
|
|
75
|
+
- `verify_presentation()` - Verify a W3C Verifiable Presentation
|
|
76
|
+
|
|
77
|
+
### Issuers
|
|
78
|
+
- `get_issuer()` - Get issuer details
|
|
79
|
+
- `list_issuers()` - List registered issuers
|
|
80
|
+
- `check_issuer_can_issue()` - Check credential type authorization
|
|
81
|
+
|
|
82
|
+
### Disputes
|
|
83
|
+
- `file_dispute()` - File a dispute against a credential
|
|
84
|
+
- `get_dispute()` - Get dispute details
|
|
85
|
+
- `list_disputes()` - List disputes
|
|
86
|
+
|
|
87
|
+
### Reputation
|
|
88
|
+
- `get_reputation()` - Get issuer reputation score
|
|
89
|
+
- `submit_feedback()` - Submit verifier feedback
|
|
90
|
+
|
|
91
|
+
### Webhooks
|
|
92
|
+
- `register_webhook()` - Register a webhook endpoint
|
|
93
|
+
- `list_webhooks()` - List registered webhooks
|
|
94
|
+
- `delete_webhook()` - Remove a webhook
|
|
95
|
+
|
|
96
|
+
### Billing
|
|
97
|
+
- `get_subscription()` - Get current plan
|
|
98
|
+
- `get_usage()` - Get usage and remaining quota
|
|
99
|
+
- `subscribe()` - Change billing plan
|
|
100
|
+
- `get_billing_portal_url()` - Self-service billing management
|
|
101
|
+
|
|
102
|
+
### Keys
|
|
103
|
+
- `rotate_key()` - Rotate signing key for a DID
|
|
104
|
+
- `list_key_versions()` - List key version history
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vercre-sdk"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Python SDK for the VERCRE Verifiable Credentials API"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Pavon Dunbar" },
|
|
10
|
+
]
|
|
11
|
+
keywords = [
|
|
12
|
+
"verifiable-credentials",
|
|
13
|
+
"w3c",
|
|
14
|
+
"did",
|
|
15
|
+
"sdk",
|
|
16
|
+
"api-client",
|
|
17
|
+
"vercre",
|
|
18
|
+
]
|
|
19
|
+
classifiers = [
|
|
20
|
+
"Development Status :: 4 - Beta",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: Security :: Cryptography",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Typing :: Typed",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"httpx>=0.25.0,<1.0",
|
|
34
|
+
"pydantic>=2.0,<3.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
async = ["anyio>=4.0"]
|
|
39
|
+
dev = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://vercre.vectorguardlabs.com"
|
|
43
|
+
Documentation = "https://vercre.vectorguardlabs.com/docs/sdk"
|
|
44
|
+
Repository = "https://github.com/pavondunbar/Verified-Credentials-SDK"
|
|
45
|
+
Issues = "https://github.com/pavondunbar/Verified-Credentials-SDK/issues"
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["hatchling"]
|
|
49
|
+
build-backend = "hatchling.build"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/vercre_sdk"]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""VERCRE SDK — Python client for the VERCRE Verifiable Credentials API."""
|
|
2
|
+
|
|
3
|
+
from vercre_sdk.async_client import AsyncVercreClient
|
|
4
|
+
from vercre_sdk.client import VercreClient
|
|
5
|
+
from vercre_sdk.exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
NotFoundError,
|
|
8
|
+
RateLimitError,
|
|
9
|
+
ServerError,
|
|
10
|
+
ValidationError,
|
|
11
|
+
VercreError,
|
|
12
|
+
)
|
|
13
|
+
from vercre_sdk.models import (
|
|
14
|
+
Credential,
|
|
15
|
+
Issuer,
|
|
16
|
+
Subscription,
|
|
17
|
+
UsageInfo,
|
|
18
|
+
VerificationResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__version__ = "1.0.0"
|
|
22
|
+
__all__ = [
|
|
23
|
+
"VercreClient",
|
|
24
|
+
"AsyncVercreClient",
|
|
25
|
+
"VercreError",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"RateLimitError",
|
|
28
|
+
"NotFoundError",
|
|
29
|
+
"ValidationError",
|
|
30
|
+
"ServerError",
|
|
31
|
+
"Credential",
|
|
32
|
+
"VerificationResult",
|
|
33
|
+
"Issuer",
|
|
34
|
+
"Subscription",
|
|
35
|
+
"UsageInfo",
|
|
36
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""HTTP transport layer with error handling and retry logic."""
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from vercre_sdk.exceptions import (
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ConflictError,
|
|
8
|
+
ForbiddenError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ServerError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
VercreError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
17
|
+
_MAX_RETRIES = 3
|
|
18
|
+
_RETRY_BACKOFF = 1.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
22
|
+
"""Raise appropriate SDK exception for error HTTP responses."""
|
|
23
|
+
if response.status_code < 400:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
body = response.json()
|
|
28
|
+
detail = body.get("detail", response.text)
|
|
29
|
+
except Exception:
|
|
30
|
+
detail = response.text
|
|
31
|
+
|
|
32
|
+
status = response.status_code
|
|
33
|
+
msg = f"VERCRE API error ({status}): {detail}"
|
|
34
|
+
|
|
35
|
+
if status == 401:
|
|
36
|
+
raise AuthenticationError(msg, status_code=status, detail=detail)
|
|
37
|
+
if status == 403:
|
|
38
|
+
raise ForbiddenError(msg, status_code=status, detail=detail)
|
|
39
|
+
if status == 404:
|
|
40
|
+
raise NotFoundError(msg, status_code=status, detail=detail)
|
|
41
|
+
if status == 409:
|
|
42
|
+
raise ConflictError(msg, status_code=status, detail=detail)
|
|
43
|
+
if status in (400, 422):
|
|
44
|
+
raise ValidationError(msg, status_code=status, detail=detail)
|
|
45
|
+
if status == 429:
|
|
46
|
+
retry_after = response.headers.get("Retry-After")
|
|
47
|
+
raise RateLimitError(
|
|
48
|
+
msg,
|
|
49
|
+
status_code=status,
|
|
50
|
+
detail=detail,
|
|
51
|
+
retry_after=int(retry_after) if retry_after else None,
|
|
52
|
+
)
|
|
53
|
+
if status >= 500:
|
|
54
|
+
raise ServerError(msg, status_code=status, detail=detail)
|
|
55
|
+
raise VercreError(msg, status_code=status, detail=detail)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_headers(api_key: str) -> dict[str, str]:
|
|
60
|
+
"""Build default request headers."""
|
|
61
|
+
return {
|
|
62
|
+
"X-API-Key": api_key,
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
"User-Agent": "vercre-sdk-python/1.0.0",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def should_retry(status_code: int, attempt: int) -> bool:
|
|
69
|
+
"""Determine if a request should be retried."""
|
|
70
|
+
if attempt >= _MAX_RETRIES:
|
|
71
|
+
return False
|
|
72
|
+
return status_code in (429, 502, 503, 504)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def retry_delay(attempt: int, retry_after: str | None = None) -> float:
|
|
76
|
+
"""Calculate delay before next retry (exponential backoff)."""
|
|
77
|
+
if retry_after:
|
|
78
|
+
try:
|
|
79
|
+
return float(retry_after)
|
|
80
|
+
except ValueError:
|
|
81
|
+
pass
|
|
82
|
+
return _RETRY_BACKOFF * (2 ** attempt)
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Asynchronous VERCRE API client."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from vercre_sdk._transport import (
|
|
9
|
+
_DEFAULT_TIMEOUT,
|
|
10
|
+
_raise_for_status,
|
|
11
|
+
build_headers,
|
|
12
|
+
retry_delay,
|
|
13
|
+
should_retry,
|
|
14
|
+
)
|
|
15
|
+
from vercre_sdk.models import (
|
|
16
|
+
Credential,
|
|
17
|
+
CredentialStatus,
|
|
18
|
+
DisputeResult,
|
|
19
|
+
Issuer,
|
|
20
|
+
Subscription,
|
|
21
|
+
UsageInfo,
|
|
22
|
+
VerificationResult,
|
|
23
|
+
WebhookResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsyncVercreClient:
|
|
28
|
+
"""Async client for the VERCRE Verifiable Credentials API.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
async with AsyncVercreClient(api_key="vercre_admin_xxx") as client:
|
|
32
|
+
cred = await client.issue_credential(
|
|
33
|
+
issuer_did="did:key:z6Mk...",
|
|
34
|
+
subject_did="did:key:z6Mp...",
|
|
35
|
+
credential_type="KYCBasic",
|
|
36
|
+
claims={"name": "Alice"},
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str,
|
|
43
|
+
base_url: str = "https://api.vercre.io",
|
|
44
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._api_key = api_key
|
|
47
|
+
self._base_url = base_url.rstrip("/")
|
|
48
|
+
self._timeout = timeout
|
|
49
|
+
self._client = httpx.AsyncClient(
|
|
50
|
+
base_url=self._base_url,
|
|
51
|
+
headers=build_headers(api_key),
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
async def close(self) -> None:
|
|
57
|
+
"""Close the underlying HTTP connection."""
|
|
58
|
+
await self._client.aclose()
|
|
59
|
+
|
|
60
|
+
async def __aenter__(self):
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
async def __aexit__(self, *args):
|
|
64
|
+
await self.close()
|
|
65
|
+
|
|
66
|
+
async def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]:
|
|
67
|
+
"""Make an HTTP request with retry logic."""
|
|
68
|
+
url = f"/api/v1{path}"
|
|
69
|
+
response = None
|
|
70
|
+
for attempt in range(4):
|
|
71
|
+
response = await self._client.request(method, url, **kwargs)
|
|
72
|
+
if should_retry(response.status_code, attempt):
|
|
73
|
+
delay = retry_delay(attempt, response.headers.get("Retry-After"))
|
|
74
|
+
await asyncio.sleep(delay)
|
|
75
|
+
continue
|
|
76
|
+
_raise_for_status(response)
|
|
77
|
+
if response.status_code == 204:
|
|
78
|
+
return {}
|
|
79
|
+
return response.json()
|
|
80
|
+
_raise_for_status(response)
|
|
81
|
+
return response.json()
|
|
82
|
+
|
|
83
|
+
# --- Health ---
|
|
84
|
+
|
|
85
|
+
async def health(self) -> dict:
|
|
86
|
+
"""Check API health."""
|
|
87
|
+
return await self._request("GET", "/health")
|
|
88
|
+
|
|
89
|
+
# --- Credentials ---
|
|
90
|
+
|
|
91
|
+
async def issue_credential(
|
|
92
|
+
self,
|
|
93
|
+
issuer_did: str,
|
|
94
|
+
subject_did: str,
|
|
95
|
+
credential_type: str,
|
|
96
|
+
claims: dict,
|
|
97
|
+
expiration: str | None = None,
|
|
98
|
+
) -> Credential:
|
|
99
|
+
"""Issue a new verifiable credential."""
|
|
100
|
+
payload: dict[str, Any] = {
|
|
101
|
+
"issuerDid": issuer_did,
|
|
102
|
+
"subjectDid": subject_did,
|
|
103
|
+
"credentialType": credential_type,
|
|
104
|
+
"claims": claims,
|
|
105
|
+
}
|
|
106
|
+
if expiration:
|
|
107
|
+
payload["expiration"] = expiration
|
|
108
|
+
data = await self._request("POST", "/issue", json=payload)
|
|
109
|
+
return Credential.model_validate(data)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def verify_credential(
|
|
113
|
+
self,
|
|
114
|
+
credential: dict | None = None,
|
|
115
|
+
credential_id: str | None = None,
|
|
116
|
+
policy: dict | None = None,
|
|
117
|
+
) -> VerificationResult:
|
|
118
|
+
"""Verify a credential and get a compliance decision."""
|
|
119
|
+
payload: dict[str, Any] = {}
|
|
120
|
+
if credential:
|
|
121
|
+
payload["credential"] = credential
|
|
122
|
+
if credential_id:
|
|
123
|
+
payload["credentialId"] = credential_id
|
|
124
|
+
if policy:
|
|
125
|
+
payload["policy"] = policy
|
|
126
|
+
data = await self._request("POST", "/verify", json=payload)
|
|
127
|
+
return VerificationResult.model_validate(data)
|
|
128
|
+
|
|
129
|
+
async def revoke_credential(self, credential: dict, issuer_did: str) -> dict:
|
|
130
|
+
"""Revoke a previously issued credential."""
|
|
131
|
+
return await self._request(
|
|
132
|
+
"POST", "/credentials/revoke",
|
|
133
|
+
json={"credential": credential, "issuerDid": issuer_did},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def get_credential_status(self, credential_hash: str) -> CredentialStatus:
|
|
137
|
+
"""Get on-chain attestation status."""
|
|
138
|
+
data = await self._request("GET", f"/credentials/{credential_hash}/status")
|
|
139
|
+
return CredentialStatus.model_validate(data)
|
|
140
|
+
|
|
141
|
+
async def get_credential(self, credential_id: str) -> dict:
|
|
142
|
+
"""Look up a credential by ID."""
|
|
143
|
+
return await self._request("GET", f"/credentials/{credential_id}/detail")
|
|
144
|
+
|
|
145
|
+
async def list_credentials(self, subject_did: str) -> dict:
|
|
146
|
+
"""List all credentials for a subject DID."""
|
|
147
|
+
return await self._request("GET", f"/subjects/{subject_did}/credentials")
|
|
148
|
+
|
|
149
|
+
# --- Verification ---
|
|
150
|
+
|
|
151
|
+
async def verify_presentation(
|
|
152
|
+
self, presentation: dict, check_chain: bool = True
|
|
153
|
+
) -> dict:
|
|
154
|
+
"""Verify a W3C Verifiable Presentation."""
|
|
155
|
+
return await self._request(
|
|
156
|
+
"POST", "/verify-presentation",
|
|
157
|
+
json={"presentation": presentation, "checkChain": check_chain},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --- Issuers ---
|
|
162
|
+
|
|
163
|
+
async def get_issuer(self, did: str) -> Issuer:
|
|
164
|
+
"""Get issuer details."""
|
|
165
|
+
data = await self._request("GET", f"/issuers/{did}")
|
|
166
|
+
return Issuer.model_validate(data)
|
|
167
|
+
|
|
168
|
+
async def list_issuers(self, tier: str | None = None) -> list[dict]:
|
|
169
|
+
"""List registered issuers."""
|
|
170
|
+
params = {"tier": tier} if tier else {}
|
|
171
|
+
data = await self._request("GET", "/issuers", params=params)
|
|
172
|
+
return data if isinstance(data, list) else data.get("issuers", [])
|
|
173
|
+
|
|
174
|
+
async def check_issuer_can_issue(self, did: str, credential_type: str) -> dict:
|
|
175
|
+
"""Check if an issuer can issue a credential type."""
|
|
176
|
+
return await self._request(
|
|
177
|
+
"GET", f"/issuers/{did}/can-issue", params={"type": credential_type}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# --- Disputes ---
|
|
181
|
+
|
|
182
|
+
async def file_dispute(
|
|
183
|
+
self,
|
|
184
|
+
credential_id: str,
|
|
185
|
+
reporter_did: str,
|
|
186
|
+
issuer_did: str,
|
|
187
|
+
reason: str,
|
|
188
|
+
) -> DisputeResult:
|
|
189
|
+
"""File a dispute against a credential."""
|
|
190
|
+
data = await self._request(
|
|
191
|
+
"POST", "/disputes",
|
|
192
|
+
json={
|
|
193
|
+
"credentialId": credential_id,
|
|
194
|
+
"reporterDid": reporter_did,
|
|
195
|
+
"issuerDid": issuer_did,
|
|
196
|
+
"reason": reason,
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
return DisputeResult.model_validate(data)
|
|
200
|
+
|
|
201
|
+
async def get_dispute(self, dispute_id: str) -> dict:
|
|
202
|
+
"""Get dispute details."""
|
|
203
|
+
return await self._request("GET", f"/disputes/{dispute_id}")
|
|
204
|
+
|
|
205
|
+
async def list_disputes(self, issuer_did: str | None = None) -> list[dict]:
|
|
206
|
+
"""List disputes."""
|
|
207
|
+
params = {"issuer": issuer_did} if issuer_did else {}
|
|
208
|
+
data = await self._request("GET", "/disputes", params=params)
|
|
209
|
+
return data if isinstance(data, list) else data.get("disputes", [])
|
|
210
|
+
|
|
211
|
+
# --- Reputation ---
|
|
212
|
+
|
|
213
|
+
async def get_reputation(self, did: str) -> dict:
|
|
214
|
+
"""Get an issuer's reputation score."""
|
|
215
|
+
return await self._request("GET", f"/reputation/{did}")
|
|
216
|
+
|
|
217
|
+
async def submit_feedback(self, did: str, positive: bool, detail: str = "") -> dict:
|
|
218
|
+
"""Submit verifier feedback on an issuer."""
|
|
219
|
+
return await self._request(
|
|
220
|
+
"POST", f"/reputation/{did}/feedback",
|
|
221
|
+
json={"positive": positive, "detail": detail},
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# --- Webhooks ---
|
|
226
|
+
|
|
227
|
+
async def register_webhook(self, url: str, events: list[str]) -> WebhookResult:
|
|
228
|
+
"""Register a webhook endpoint."""
|
|
229
|
+
data = await self._request(
|
|
230
|
+
"POST", "/webhooks", json={"url": url, "events": events}
|
|
231
|
+
)
|
|
232
|
+
return WebhookResult.model_validate(data)
|
|
233
|
+
|
|
234
|
+
async def list_webhooks(self) -> list[dict]:
|
|
235
|
+
"""List registered webhooks."""
|
|
236
|
+
data = await self._request("GET", "/webhooks")
|
|
237
|
+
return data if isinstance(data, list) else data.get("webhooks", [])
|
|
238
|
+
|
|
239
|
+
async def delete_webhook(self, webhook_id: str) -> dict:
|
|
240
|
+
"""Delete a webhook subscription."""
|
|
241
|
+
return await self._request("DELETE", f"/webhooks/{webhook_id}")
|
|
242
|
+
|
|
243
|
+
# --- Billing ---
|
|
244
|
+
|
|
245
|
+
async def get_subscription(self) -> Subscription:
|
|
246
|
+
"""Get current billing subscription."""
|
|
247
|
+
data = await self._request("GET", "/billing/subscription")
|
|
248
|
+
return Subscription.model_validate(data)
|
|
249
|
+
|
|
250
|
+
async def get_usage(self) -> UsageInfo:
|
|
251
|
+
"""Get current period usage and quotas."""
|
|
252
|
+
data = await self._request("GET", "/billing/usage")
|
|
253
|
+
return UsageInfo.model_validate(data)
|
|
254
|
+
|
|
255
|
+
async def subscribe(self, plan: str) -> dict:
|
|
256
|
+
"""Create or change a billing subscription."""
|
|
257
|
+
return await self._request("POST", "/billing/subscribe", json={"plan": plan})
|
|
258
|
+
|
|
259
|
+
async def get_billing_portal_url(self, return_url: str = "") -> str:
|
|
260
|
+
"""Get a Stripe billing portal URL."""
|
|
261
|
+
payload = {"returnUrl": return_url} if return_url else {}
|
|
262
|
+
data = await self._request("POST", "/billing/portal", json=payload)
|
|
263
|
+
return data.get("url", "")
|
|
264
|
+
|
|
265
|
+
# --- Keys ---
|
|
266
|
+
|
|
267
|
+
async def rotate_key(self, did: str) -> dict:
|
|
268
|
+
"""Rotate the signing key for a DID."""
|
|
269
|
+
return await self._request("POST", f"/keys/{did}/rotate")
|
|
270
|
+
|
|
271
|
+
async def list_key_versions(self, did: str) -> list[dict]:
|
|
272
|
+
"""List all key versions for a DID."""
|
|
273
|
+
data = await self._request("GET", f"/keys/{did}/versions")
|
|
274
|
+
return data if isinstance(data, list) else []
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Synchronous VERCRE API client."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from vercre_sdk._transport import (
|
|
9
|
+
_DEFAULT_TIMEOUT,
|
|
10
|
+
_raise_for_status,
|
|
11
|
+
build_headers,
|
|
12
|
+
retry_delay,
|
|
13
|
+
should_retry,
|
|
14
|
+
)
|
|
15
|
+
from vercre_sdk.models import (
|
|
16
|
+
Credential,
|
|
17
|
+
CredentialStatus,
|
|
18
|
+
DisputeResult,
|
|
19
|
+
Issuer,
|
|
20
|
+
Subscription,
|
|
21
|
+
UsageInfo,
|
|
22
|
+
VerificationResult,
|
|
23
|
+
WebhookResult,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VercreClient:
|
|
28
|
+
"""Synchronous client for the VERCRE Verifiable Credentials API.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
client = VercreClient(api_key="vercre_admin_xxx", base_url="https://api.vercre.io")
|
|
32
|
+
cred = client.issue_credential(
|
|
33
|
+
issuer_did="did:key:z6Mk...",
|
|
34
|
+
subject_did="did:key:z6Mp...",
|
|
35
|
+
credential_type="KYCBasic",
|
|
36
|
+
claims={"name": "Alice", "country": "US"},
|
|
37
|
+
)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
api_key: str,
|
|
43
|
+
base_url: str = "https://api.vercre.io",
|
|
44
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._api_key = api_key
|
|
47
|
+
self._base_url = base_url.rstrip("/")
|
|
48
|
+
self._timeout = timeout
|
|
49
|
+
self._client = httpx.Client(
|
|
50
|
+
base_url=self._base_url,
|
|
51
|
+
headers=build_headers(api_key),
|
|
52
|
+
timeout=timeout,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def close(self) -> None:
|
|
57
|
+
"""Close the underlying HTTP connection."""
|
|
58
|
+
self._client.close()
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, *args):
|
|
64
|
+
self.close()
|
|
65
|
+
|
|
66
|
+
def _request(self, method: str, path: str, **kwargs) -> dict[str, Any]:
|
|
67
|
+
"""Make an HTTP request with retry logic."""
|
|
68
|
+
url = f"/api/v1{path}"
|
|
69
|
+
for attempt in range(4):
|
|
70
|
+
response = self._client.request(method, url, **kwargs)
|
|
71
|
+
if should_retry(response.status_code, attempt):
|
|
72
|
+
delay = retry_delay(attempt, response.headers.get("Retry-After"))
|
|
73
|
+
time.sleep(delay)
|
|
74
|
+
continue
|
|
75
|
+
_raise_for_status(response)
|
|
76
|
+
if response.status_code == 204:
|
|
77
|
+
return {}
|
|
78
|
+
return response.json()
|
|
79
|
+
_raise_for_status(response)
|
|
80
|
+
return response.json()
|
|
81
|
+
|
|
82
|
+
# --- Health ---
|
|
83
|
+
|
|
84
|
+
def health(self) -> dict:
|
|
85
|
+
"""Check API health."""
|
|
86
|
+
return self._request("GET", "/health")
|
|
87
|
+
|
|
88
|
+
# --- Credentials ---
|
|
89
|
+
|
|
90
|
+
def issue_credential(
|
|
91
|
+
self,
|
|
92
|
+
issuer_did: str,
|
|
93
|
+
subject_did: str,
|
|
94
|
+
credential_type: str,
|
|
95
|
+
claims: dict,
|
|
96
|
+
expiration: str | None = None,
|
|
97
|
+
) -> Credential:
|
|
98
|
+
"""Issue a new verifiable credential."""
|
|
99
|
+
payload: dict[str, Any] = {
|
|
100
|
+
"issuerDid": issuer_did,
|
|
101
|
+
"subjectDid": subject_did,
|
|
102
|
+
"credentialType": credential_type,
|
|
103
|
+
"claims": claims,
|
|
104
|
+
}
|
|
105
|
+
if expiration:
|
|
106
|
+
payload["expiration"] = expiration
|
|
107
|
+
data = self._request("POST", "/issue", json=payload)
|
|
108
|
+
return Credential.model_validate(data)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def verify_credential(
|
|
112
|
+
self,
|
|
113
|
+
credential: dict | None = None,
|
|
114
|
+
credential_id: str | None = None,
|
|
115
|
+
policy: dict | None = None,
|
|
116
|
+
) -> VerificationResult:
|
|
117
|
+
"""Verify a credential and get a compliance decision."""
|
|
118
|
+
payload: dict[str, Any] = {}
|
|
119
|
+
if credential:
|
|
120
|
+
payload["credential"] = credential
|
|
121
|
+
if credential_id:
|
|
122
|
+
payload["credentialId"] = credential_id
|
|
123
|
+
if policy:
|
|
124
|
+
payload["policy"] = policy
|
|
125
|
+
data = self._request("POST", "/verify", json=payload)
|
|
126
|
+
return VerificationResult.model_validate(data)
|
|
127
|
+
|
|
128
|
+
def revoke_credential(self, credential: dict, issuer_did: str) -> dict:
|
|
129
|
+
"""Revoke a previously issued credential."""
|
|
130
|
+
return self._request(
|
|
131
|
+
"POST", "/credentials/revoke",
|
|
132
|
+
json={"credential": credential, "issuerDid": issuer_did},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def get_credential_status(self, credential_hash: str) -> CredentialStatus:
|
|
136
|
+
"""Get on-chain attestation status for a credential."""
|
|
137
|
+
data = self._request("GET", f"/credentials/{credential_hash}/status")
|
|
138
|
+
return CredentialStatus.model_validate(data)
|
|
139
|
+
|
|
140
|
+
def get_credential(self, credential_id: str) -> dict:
|
|
141
|
+
"""Look up a credential by ID."""
|
|
142
|
+
return self._request("GET", f"/credentials/{credential_id}/detail")
|
|
143
|
+
|
|
144
|
+
def list_credentials(self, subject_did: str) -> dict:
|
|
145
|
+
"""List all credentials for a subject DID."""
|
|
146
|
+
return self._request("GET", f"/subjects/{subject_did}/credentials")
|
|
147
|
+
|
|
148
|
+
# --- Verification ---
|
|
149
|
+
|
|
150
|
+
def verify_presentation(
|
|
151
|
+
self, presentation: dict, check_chain: bool = True
|
|
152
|
+
) -> dict:
|
|
153
|
+
"""Verify a W3C Verifiable Presentation."""
|
|
154
|
+
return self._request(
|
|
155
|
+
"POST", "/verify-presentation",
|
|
156
|
+
json={"presentation": presentation, "checkChain": check_chain},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# --- Issuers ---
|
|
161
|
+
|
|
162
|
+
def get_issuer(self, did: str) -> Issuer:
|
|
163
|
+
"""Get issuer details."""
|
|
164
|
+
data = self._request("GET", f"/issuers/{did}")
|
|
165
|
+
return Issuer.model_validate(data)
|
|
166
|
+
|
|
167
|
+
def list_issuers(self, tier: str | None = None) -> list[dict]:
|
|
168
|
+
"""List registered issuers."""
|
|
169
|
+
params = {"tier": tier} if tier else {}
|
|
170
|
+
data = self._request("GET", "/issuers", params=params)
|
|
171
|
+
return data if isinstance(data, list) else data.get("issuers", [])
|
|
172
|
+
|
|
173
|
+
def check_issuer_can_issue(self, did: str, credential_type: str) -> dict:
|
|
174
|
+
"""Check if an issuer is authorized to issue a credential type."""
|
|
175
|
+
return self._request(
|
|
176
|
+
"GET", f"/issuers/{did}/can-issue",
|
|
177
|
+
params={"type": credential_type},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# --- Disputes ---
|
|
181
|
+
|
|
182
|
+
def file_dispute(
|
|
183
|
+
self,
|
|
184
|
+
credential_id: str,
|
|
185
|
+
reporter_did: str,
|
|
186
|
+
issuer_did: str,
|
|
187
|
+
reason: str,
|
|
188
|
+
) -> DisputeResult:
|
|
189
|
+
"""File a dispute against a credential."""
|
|
190
|
+
data = self._request(
|
|
191
|
+
"POST", "/disputes",
|
|
192
|
+
json={
|
|
193
|
+
"credentialId": credential_id,
|
|
194
|
+
"reporterDid": reporter_did,
|
|
195
|
+
"issuerDid": issuer_did,
|
|
196
|
+
"reason": reason,
|
|
197
|
+
},
|
|
198
|
+
)
|
|
199
|
+
return DisputeResult.model_validate(data)
|
|
200
|
+
|
|
201
|
+
def get_dispute(self, dispute_id: str) -> dict:
|
|
202
|
+
"""Get dispute details."""
|
|
203
|
+
return self._request("GET", f"/disputes/{dispute_id}")
|
|
204
|
+
|
|
205
|
+
def list_disputes(self, issuer_did: str | None = None) -> list[dict]:
|
|
206
|
+
"""List disputes, optionally filtered by issuer."""
|
|
207
|
+
params = {"issuer": issuer_did} if issuer_did else {}
|
|
208
|
+
data = self._request("GET", "/disputes", params=params)
|
|
209
|
+
return data if isinstance(data, list) else data.get("disputes", [])
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# --- Reputation ---
|
|
213
|
+
|
|
214
|
+
def get_reputation(self, did: str) -> dict:
|
|
215
|
+
"""Get an issuer's reputation score."""
|
|
216
|
+
return self._request("GET", f"/reputation/{did}")
|
|
217
|
+
|
|
218
|
+
def submit_feedback(self, did: str, positive: bool, detail: str = "") -> dict:
|
|
219
|
+
"""Submit verifier feedback on an issuer."""
|
|
220
|
+
return self._request(
|
|
221
|
+
"POST", f"/reputation/{did}/feedback",
|
|
222
|
+
json={"positive": positive, "detail": detail},
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# --- Webhooks ---
|
|
226
|
+
|
|
227
|
+
def register_webhook(self, url: str, events: list[str]) -> WebhookResult:
|
|
228
|
+
"""Register a webhook endpoint."""
|
|
229
|
+
data = self._request(
|
|
230
|
+
"POST", "/webhooks",
|
|
231
|
+
json={"url": url, "events": events},
|
|
232
|
+
)
|
|
233
|
+
return WebhookResult.model_validate(data)
|
|
234
|
+
|
|
235
|
+
def list_webhooks(self) -> list[dict]:
|
|
236
|
+
"""List registered webhooks."""
|
|
237
|
+
data = self._request("GET", "/webhooks")
|
|
238
|
+
return data if isinstance(data, list) else data.get("webhooks", [])
|
|
239
|
+
|
|
240
|
+
def delete_webhook(self, webhook_id: str) -> dict:
|
|
241
|
+
"""Delete a webhook subscription."""
|
|
242
|
+
return self._request("DELETE", f"/webhooks/{webhook_id}")
|
|
243
|
+
|
|
244
|
+
# --- Billing ---
|
|
245
|
+
|
|
246
|
+
def get_subscription(self) -> Subscription:
|
|
247
|
+
"""Get current billing subscription."""
|
|
248
|
+
data = self._request("GET", "/billing/subscription")
|
|
249
|
+
return Subscription.model_validate(data)
|
|
250
|
+
|
|
251
|
+
def get_usage(self) -> UsageInfo:
|
|
252
|
+
"""Get current period usage and quotas."""
|
|
253
|
+
data = self._request("GET", "/billing/usage")
|
|
254
|
+
return UsageInfo.model_validate(data)
|
|
255
|
+
|
|
256
|
+
def subscribe(self, plan: str) -> dict:
|
|
257
|
+
"""Create or change a billing subscription."""
|
|
258
|
+
return self._request("POST", "/billing/subscribe", json={"plan": plan})
|
|
259
|
+
|
|
260
|
+
def get_billing_portal_url(self, return_url: str = "") -> str:
|
|
261
|
+
"""Get a Stripe billing portal URL for self-service management."""
|
|
262
|
+
payload = {"returnUrl": return_url} if return_url else {}
|
|
263
|
+
data = self._request("POST", "/billing/portal", json=payload)
|
|
264
|
+
return data.get("url", "")
|
|
265
|
+
|
|
266
|
+
# --- Keys ---
|
|
267
|
+
|
|
268
|
+
def rotate_key(self, did: str) -> dict:
|
|
269
|
+
"""Rotate the signing key for a DID."""
|
|
270
|
+
return self._request("POST", f"/keys/{did}/rotate")
|
|
271
|
+
|
|
272
|
+
def list_key_versions(self, did: str) -> list[dict]:
|
|
273
|
+
"""List all key versions for a DID."""
|
|
274
|
+
data = self._request("GET", f"/keys/{did}/versions")
|
|
275
|
+
return data if isinstance(data, list) else []
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""SDK exceptions for VERCRE API errors."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class VercreError(Exception):
|
|
5
|
+
"""Base exception for all VERCRE SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, status_code: int | None = None, detail: str | None = None):
|
|
8
|
+
self.status_code = status_code
|
|
9
|
+
self.detail = detail or message
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(VercreError):
|
|
14
|
+
"""Raised when API key is missing or invalid (401)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ForbiddenError(VercreError):
|
|
18
|
+
"""Raised when the API key lacks permission for the operation (403)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NotFoundError(VercreError):
|
|
22
|
+
"""Raised when a requested resource does not exist (404)."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ValidationError(VercreError):
|
|
26
|
+
"""Raised when request data fails validation (400/422)."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class RateLimitError(VercreError):
|
|
30
|
+
"""Raised when rate limit is exceeded (429).
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
retry_after: Seconds to wait before retrying.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str, retry_after: int | None = None, **kwargs):
|
|
37
|
+
self.retry_after = retry_after
|
|
38
|
+
super().__init__(message, **kwargs)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ServerError(VercreError):
|
|
42
|
+
"""Raised when the VERCRE API returns a 5xx error."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ConflictError(VercreError):
|
|
46
|
+
"""Raised when the request conflicts with existing state (409)."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Response models for the VERCRE SDK."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Credential(BaseModel):
|
|
7
|
+
"""An issued verifiable credential."""
|
|
8
|
+
|
|
9
|
+
credential: dict = Field(description="Full signed VC object.")
|
|
10
|
+
credential_hash: str = Field(alias="credentialHash", description="SHA-256 hash.")
|
|
11
|
+
credential_id: str = Field(alias="credentialId", description="Unique ID.")
|
|
12
|
+
transaction_id: str = Field(alias="transactionId", description="On-chain tx ID.")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IssuerInfo(BaseModel):
|
|
16
|
+
"""Issuer details from verification."""
|
|
17
|
+
|
|
18
|
+
did: str
|
|
19
|
+
tier: str
|
|
20
|
+
reputation: int
|
|
21
|
+
status: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CredentialInfo(BaseModel):
|
|
25
|
+
"""Credential metadata from verification."""
|
|
26
|
+
|
|
27
|
+
type: str
|
|
28
|
+
expired: bool
|
|
29
|
+
revoked: bool
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class VerificationResult(BaseModel):
|
|
33
|
+
"""Result of a credential verification."""
|
|
34
|
+
|
|
35
|
+
valid: bool
|
|
36
|
+
issuer: IssuerInfo | None = None
|
|
37
|
+
credential: CredentialInfo | None = None
|
|
38
|
+
confidence_score: float | None = Field(None, alias="confidenceScore")
|
|
39
|
+
compliance_decision: str = Field(alias="complianceDecision")
|
|
40
|
+
reasons: list[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Issuer(BaseModel):
|
|
45
|
+
"""Registered issuer information."""
|
|
46
|
+
|
|
47
|
+
did: str
|
|
48
|
+
tier: str
|
|
49
|
+
reputation: int
|
|
50
|
+
status: str
|
|
51
|
+
stake: float
|
|
52
|
+
name: str | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Subscription(BaseModel):
|
|
56
|
+
"""Billing subscription state."""
|
|
57
|
+
|
|
58
|
+
plan: str
|
|
59
|
+
active: bool = True
|
|
60
|
+
stripe_customer_id: str | None = Field(None, alias="stripeCustomerId")
|
|
61
|
+
stripe_subscription_id: str | None = Field(None, alias="stripeSubscriptionId")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class UsageInfo(BaseModel):
|
|
65
|
+
"""Current period usage information."""
|
|
66
|
+
|
|
67
|
+
plan: str
|
|
68
|
+
# Issuer fields
|
|
69
|
+
issuances: int | None = None
|
|
70
|
+
included_issuances: int | None = Field(None, alias="includedIssuances")
|
|
71
|
+
remaining_issuances: int | None = Field(None, alias="remainingIssuances")
|
|
72
|
+
anchors: int | None = None
|
|
73
|
+
included_anchors: int | None = Field(None, alias="includedAnchors")
|
|
74
|
+
remaining_anchors: int | None = Field(None, alias="remainingAnchors")
|
|
75
|
+
# Verifier fields
|
|
76
|
+
verifications: int | None = None
|
|
77
|
+
included: int | None = None
|
|
78
|
+
remaining: int | None = None
|
|
79
|
+
rate_limit_rpm: int | None = Field(None, alias="rateLimitRpm")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OnboardResult(BaseModel):
|
|
83
|
+
"""Result of onboarding (signup)."""
|
|
84
|
+
|
|
85
|
+
did: str
|
|
86
|
+
name: str
|
|
87
|
+
api_key: str = Field(alias="apiKey")
|
|
88
|
+
api_key_id: str = Field(alias="apiKeyId")
|
|
89
|
+
plan: str | None = None
|
|
90
|
+
tier: str | None = None
|
|
91
|
+
status: str | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class CredentialStatus(BaseModel):
|
|
95
|
+
"""On-chain credential status."""
|
|
96
|
+
|
|
97
|
+
status: str
|
|
98
|
+
revoked: bool
|
|
99
|
+
expired: bool
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class DisputeResult(BaseModel):
|
|
103
|
+
"""Filed dispute result."""
|
|
104
|
+
|
|
105
|
+
id: str
|
|
106
|
+
credential_id: str = Field(alias="credentialId")
|
|
107
|
+
reporter_did: str = Field(alias="reporterDid")
|
|
108
|
+
issuer_did: str = Field(alias="issuerDid")
|
|
109
|
+
reason: str
|
|
110
|
+
status: str
|
|
111
|
+
created_at: str = Field(alias="createdAt")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class WebhookResult(BaseModel):
|
|
115
|
+
"""Registered webhook."""
|
|
116
|
+
|
|
117
|
+
id: str
|
|
118
|
+
url: str
|
|
119
|
+
events: list[str]
|
|
120
|
+
created_at: str = Field(alias="createdAt")
|
|
121
|
+
active: bool
|
|
File without changes
|