hax-sdk 0.2.4rc6__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.
- hax_sdk-0.2.4rc6/.gitignore +68 -0
- hax_sdk-0.2.4rc6/PKG-INFO +311 -0
- hax_sdk-0.2.4rc6/README.md +281 -0
- hax_sdk-0.2.4rc6/examples/__init__.py +1 -0
- hax_sdk-0.2.4rc6/examples/basic_approval.py +103 -0
- hax_sdk-0.2.4rc6/examples/confirm_action.py +121 -0
- hax_sdk-0.2.4rc6/examples/webhook_handler.py +103 -0
- hax_sdk-0.2.4rc6/hax/__init__.py +79 -0
- hax_sdk-0.2.4rc6/hax/client.py +620 -0
- hax_sdk-0.2.4rc6/hax/crypto.py +378 -0
- hax_sdk-0.2.4rc6/hax/exceptions.py +82 -0
- hax_sdk-0.2.4rc6/hax/form_builder.py +223 -0
- hax_sdk-0.2.4rc6/hax/http.py +114 -0
- hax_sdk-0.2.4rc6/hax/models/__init__.py +13 -0
- hax_sdk-0.2.4rc6/hax/models/delivery.py +40 -0
- hax_sdk-0.2.4rc6/hax/models/form_request.py +40 -0
- hax_sdk-0.2.4rc6/hax/models/request.py +150 -0
- hax_sdk-0.2.4rc6/hax/py.typed +0 -0
- hax_sdk-0.2.4rc6/hax/webhooks.py +169 -0
- hax_sdk-0.2.4rc6/pyproject.toml +70 -0
- hax_sdk-0.2.4rc6/tests/__init__.py +1 -0
- hax_sdk-0.2.4rc6/tests/conftest.py +25 -0
- hax_sdk-0.2.4rc6/tests/test_crypto.py +355 -0
- hax_sdk-0.2.4rc6/tests/test_delivery.py +291 -0
- hax_sdk-0.2.4rc6/tests/test_form_builder.py +547 -0
- hax_sdk-0.2.4rc6/tests/test_webhooks.py +215 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
node_modules
|
|
5
|
+
/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# testing
|
|
14
|
+
/coverage
|
|
15
|
+
|
|
16
|
+
# next.js
|
|
17
|
+
/.next/
|
|
18
|
+
/out/
|
|
19
|
+
|
|
20
|
+
# production
|
|
21
|
+
/build
|
|
22
|
+
|
|
23
|
+
# misc
|
|
24
|
+
.DS_Store
|
|
25
|
+
*.pem
|
|
26
|
+
|
|
27
|
+
# debug
|
|
28
|
+
npm-debug.log*
|
|
29
|
+
yarn-debug.log*
|
|
30
|
+
yarn-error.log*
|
|
31
|
+
.pnpm-debug.log*
|
|
32
|
+
|
|
33
|
+
# env files (can opt-in for committing if needed)
|
|
34
|
+
.env
|
|
35
|
+
|
|
36
|
+
# vercel
|
|
37
|
+
.vercel
|
|
38
|
+
|
|
39
|
+
# typescript
|
|
40
|
+
*.tsbuildinfo
|
|
41
|
+
next-env.d.ts
|
|
42
|
+
|
|
43
|
+
# clerk configuration (can include secrets)
|
|
44
|
+
/.clerk/
|
|
45
|
+
|
|
46
|
+
# python
|
|
47
|
+
__pycache__/
|
|
48
|
+
|
|
49
|
+
# pipeline artifacts
|
|
50
|
+
.artifacts/
|
|
51
|
+
|
|
52
|
+
# build artifacts
|
|
53
|
+
server-bundle.cjs
|
|
54
|
+
|
|
55
|
+
# worktrees
|
|
56
|
+
.worktrees/
|
|
57
|
+
|
|
58
|
+
# python (additional)
|
|
59
|
+
*.pyc
|
|
60
|
+
*.pyo
|
|
61
|
+
*.pyd
|
|
62
|
+
.venv/
|
|
63
|
+
venv/
|
|
64
|
+
*.egg-info/
|
|
65
|
+
.pytest_cache/
|
|
66
|
+
.mypy_cache/
|
|
67
|
+
.ruff_cache/
|
|
68
|
+
.tox/
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hax-sdk
|
|
3
|
+
Version: 0.2.4rc6
|
|
4
|
+
Summary: Python SDK for the HAX (Human Approval eXchange) API
|
|
5
|
+
Project-URL: Homepage, https://github.com/Agent-Field/hax-sdk
|
|
6
|
+
Project-URL: Documentation, https://github.com/Agent-Field/hax-sdk
|
|
7
|
+
Project-URL: Repository, https://github.com/Agent-Field/hax-sdk
|
|
8
|
+
Author: HAX Team
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: api,approval,forms,hax,human-in-the-loop,sdk
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: cryptography>=41.0.0
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-httpx>=0.30.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# HAX Python SDK
|
|
32
|
+
|
|
33
|
+
Python client for the HAX (Human Approval eXchange) API. Enables agents and automated systems to programmatically collect human input.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
The Python SDK is not yet published to PyPI. Install from source:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e sdks/python # from the repo root
|
|
41
|
+
# or
|
|
42
|
+
pip install -e . # from sdks/python/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from hax import HaxClient
|
|
49
|
+
|
|
50
|
+
client = HaxClient(
|
|
51
|
+
api_key="hax_live_...",
|
|
52
|
+
base_url="http://localhost:3000/api/v1",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
request = client.create_request(
|
|
56
|
+
type="text-approval-v1",
|
|
57
|
+
payload={"text": "Deploy main to prod?", "approveLabel": "Ship it", "denyLabel": "Hold"},
|
|
58
|
+
webhook_url="https://myapp.com/webhook",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
print("Share with approver:", request.url)
|
|
62
|
+
|
|
63
|
+
# Poll until completed/expired/cancelled
|
|
64
|
+
request = client.wait_for_response(request.id, timeout=300)
|
|
65
|
+
if request.is_completed:
|
|
66
|
+
print("Decision:", request.response.get("decision"))
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Features
|
|
70
|
+
|
|
71
|
+
- **Pydantic models**: Typed request/response models with validation
|
|
72
|
+
- **FormBuilder**: Fluent API for building typed forms with runtime type inference
|
|
73
|
+
- **E2E encryption**: RSA-OAEP + AES-GCM hybrid encryption for sensitive responses
|
|
74
|
+
- **Webhook verification**: HMAC-SHA256 signature verification
|
|
75
|
+
- **Delivery**: Send requests via email or SMS
|
|
76
|
+
- **Polling**: Built-in `wait_for_response` with configurable timeout
|
|
77
|
+
- **Error handling**: Typed exception hierarchy
|
|
78
|
+
|
|
79
|
+
## Request Methods
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# Create a request
|
|
83
|
+
request = client.create_request(
|
|
84
|
+
type="text-approval-v1",
|
|
85
|
+
payload={"text": "Approve this action?"},
|
|
86
|
+
title="Optional title",
|
|
87
|
+
description="Optional description",
|
|
88
|
+
webhook_url="https://myapp.com/webhook",
|
|
89
|
+
expires_in_seconds=3600,
|
|
90
|
+
metadata={"pr_number": 123},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Send via email
|
|
94
|
+
request = client.request_via_email(
|
|
95
|
+
type="confirm-action-v1",
|
|
96
|
+
payload={"title": "Approve?", "confirmPhrase": "YES"},
|
|
97
|
+
to_email="approver@example.com",
|
|
98
|
+
subject="Approval Required",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Send via SMS
|
|
102
|
+
request = client.request_via_sms(
|
|
103
|
+
type="text-approval-v1",
|
|
104
|
+
payload={"text": "Approve?"},
|
|
105
|
+
to_phone="+15551234567",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Get a request by ID
|
|
109
|
+
request = client.get_request("req_123")
|
|
110
|
+
|
|
111
|
+
# List recent requests
|
|
112
|
+
requests = client.list_requests()
|
|
113
|
+
|
|
114
|
+
# Cancel a pending request
|
|
115
|
+
cancelled = client.cancel_request("req_123")
|
|
116
|
+
|
|
117
|
+
# Submit a response (for testing)
|
|
118
|
+
completed = client.submit_response("req_123", {"decision": "approve"})
|
|
119
|
+
|
|
120
|
+
# Wait for completion with timeout
|
|
121
|
+
result = client.wait_for_response("req_123", poll_interval=2.0, timeout=60)
|
|
122
|
+
|
|
123
|
+
# List available template types
|
|
124
|
+
types = client.list_types()
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Status Helpers
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
if request.is_pending:
|
|
131
|
+
print("Waiting for response...")
|
|
132
|
+
if request.is_completed:
|
|
133
|
+
print("Response:", request.response)
|
|
134
|
+
if request.is_expired:
|
|
135
|
+
print("Request expired")
|
|
136
|
+
if request.is_cancelled:
|
|
137
|
+
print("Request was cancelled")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## FormBuilder
|
|
141
|
+
|
|
142
|
+
Build typed forms with a fluent API:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from hax import HaxClient, FormBuilder
|
|
146
|
+
|
|
147
|
+
client = HaxClient(api_key="hax_live_...")
|
|
148
|
+
|
|
149
|
+
form = (FormBuilder()
|
|
150
|
+
.title("Event Registration")
|
|
151
|
+
.input("name", label="Full Name", required=True)
|
|
152
|
+
.input("email", label="Email", variant="email", required=True)
|
|
153
|
+
.number("age", label="Age", min=0, max=120)
|
|
154
|
+
.checkbox("newsletter", checkbox_label="Subscribe to newsletter"))
|
|
155
|
+
|
|
156
|
+
handle = client.create_form_request(form,
|
|
157
|
+
webhook_url="https://myapp.com/webhook")
|
|
158
|
+
|
|
159
|
+
print(f"Form URL: {handle.url}")
|
|
160
|
+
|
|
161
|
+
# Wait for typed response
|
|
162
|
+
response = handle.wait_for_response(timeout=300)
|
|
163
|
+
print(response.values.name) # str
|
|
164
|
+
print(response.values.email) # str
|
|
165
|
+
print(response.values.age) # float
|
|
166
|
+
print(response.values.newsletter) # bool
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Available Field Types
|
|
170
|
+
|
|
171
|
+
| Method | Output Type | Description |
|
|
172
|
+
|--------|------------|-------------|
|
|
173
|
+
| `.input(id)` | `str` | Text input (variants: text, email, url, tel) |
|
|
174
|
+
| `.textarea(id)` | `str` | Multi-line text input |
|
|
175
|
+
| `.select(id, options=...)` | `str` | Dropdown select |
|
|
176
|
+
| `.radio_group(id, options=...)` | `str` | Radio button group |
|
|
177
|
+
| `.date(id)` | `str` | Date picker (ISO format) |
|
|
178
|
+
| `.number(id)` | `float` | Numeric input |
|
|
179
|
+
| `.slider(id, min=, max=)` | `float` | Slider control |
|
|
180
|
+
| `.checkbox(id)` | `bool` | Single checkbox |
|
|
181
|
+
| `.switch(id)` | `bool` | Toggle switch |
|
|
182
|
+
| `.checkbox_group(id, options=...)` | `list[str]` | Multi-select checkboxes |
|
|
183
|
+
| `.hidden(id, value)` | `type(value)` | Hidden field |
|
|
184
|
+
|
|
185
|
+
## Webhooks
|
|
186
|
+
|
|
187
|
+
Verify and parse webhook events:
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from hax import verify_signature, parse_event
|
|
191
|
+
|
|
192
|
+
# In your webhook handler
|
|
193
|
+
def handle_webhook(request):
|
|
194
|
+
# Verify signature
|
|
195
|
+
is_valid = verify_signature(
|
|
196
|
+
payload=request.body,
|
|
197
|
+
signature=request.headers["X-Hax-Signature"],
|
|
198
|
+
secret="whsec_...",
|
|
199
|
+
)
|
|
200
|
+
if not is_valid:
|
|
201
|
+
return 400, "Invalid signature"
|
|
202
|
+
|
|
203
|
+
# Parse the event
|
|
204
|
+
event = parse_event(request.body)
|
|
205
|
+
|
|
206
|
+
if event.event_type == "completed":
|
|
207
|
+
print(f"Request {event.request_id} completed!")
|
|
208
|
+
print(f"Response: {event.response}")
|
|
209
|
+
elif event.event_type == "expired":
|
|
210
|
+
print(f"Request {event.request_id} expired")
|
|
211
|
+
|
|
212
|
+
return 200, "OK"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Event Types
|
|
216
|
+
|
|
217
|
+
- `request.sent` — Notification was delivered (email/SMS)
|
|
218
|
+
- `request.opened` — Human opened the request link
|
|
219
|
+
- `request.completed` — Human submitted a response
|
|
220
|
+
- `request.expired` — Request expired without action
|
|
221
|
+
|
|
222
|
+
## Encryption
|
|
223
|
+
|
|
224
|
+
For sensitive response data, use end-to-end encryption:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from hax import HaxClient
|
|
228
|
+
|
|
229
|
+
# Passphrase-based (automatic encrypt/decrypt)
|
|
230
|
+
client = HaxClient(
|
|
231
|
+
api_key="hax_live_...",
|
|
232
|
+
encryption_key="my-secret-passphrase",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Public key is automatically sent with requests
|
|
236
|
+
request = client.create_request(
|
|
237
|
+
type="text-approval-v1",
|
|
238
|
+
payload={"text": "Approve this sensitive action?"},
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Response is automatically decrypted when retrieved
|
|
242
|
+
completed = client.get_request(request.id)
|
|
243
|
+
print(completed.response) # Decrypted plaintext
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Manual Decryption
|
|
247
|
+
|
|
248
|
+
```python
|
|
249
|
+
from hax import generate_key_pair, decrypt_response, is_encrypted_response
|
|
250
|
+
|
|
251
|
+
public_key, private_key = generate_key_pair("my-secret")
|
|
252
|
+
|
|
253
|
+
# Use public_key when creating the client
|
|
254
|
+
client = HaxClient(api_key="...", public_key=public_key)
|
|
255
|
+
|
|
256
|
+
# Later, manually decrypt
|
|
257
|
+
request = client.get_request("req_123")
|
|
258
|
+
if is_encrypted_response(request.response):
|
|
259
|
+
decrypted = decrypt_response(request.response["_encrypted"], private_key)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Error Handling
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
from hax import (
|
|
266
|
+
HaxError, # Base error
|
|
267
|
+
AuthenticationError, # Invalid API key (401)
|
|
268
|
+
ValidationError, # Invalid request data (400/422)
|
|
269
|
+
NotFoundError, # Resource not found (404)
|
|
270
|
+
RateLimitError, # Too many requests (429)
|
|
271
|
+
ServerError, # Server error (500+)
|
|
272
|
+
DecryptionError, # Decryption failure
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
request = client.create_request(...)
|
|
277
|
+
except AuthenticationError:
|
|
278
|
+
print("Check your API key")
|
|
279
|
+
except ValidationError as e:
|
|
280
|
+
print(f"Invalid request: {e}")
|
|
281
|
+
except RateLimitError:
|
|
282
|
+
print("Rate limited, try again later")
|
|
283
|
+
except HaxError as e:
|
|
284
|
+
print(f"API error: {e}")
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Template Types
|
|
288
|
+
|
|
289
|
+
| Template | Description |
|
|
290
|
+
|----------|-------------|
|
|
291
|
+
| `text-approval-v1` | Show text and collect an approve/deny decision |
|
|
292
|
+
| `confirm-action-v1` | Require typing a specific phrase to confirm a destructive action |
|
|
293
|
+
| `collect-email-v1` | Prompt the user for an email address |
|
|
294
|
+
| `form-builder` | Advanced forms with field types, layouts, validation, and conditional logic |
|
|
295
|
+
| `multi-choice-selection-v1` | Single or multiple selection from customizable option cards |
|
|
296
|
+
| `code-changes-v1` | GitHub-style diff view with inline line comments |
|
|
297
|
+
| `rich-text-editor-v1` | Markdown-formatted text editing for documents and reports |
|
|
298
|
+
| `file-upload-v1` | Collect files (documents, images, CSVs) from users |
|
|
299
|
+
| `signature-capture-v1` | Capture e-signatures with optional signer name and legal text |
|
|
300
|
+
| `data-table-review-v1` | Review, select, or edit tabular data |
|
|
301
|
+
| `scheduling-picker-v1` | Date/time slot selection with optional recurring schedules |
|
|
302
|
+
| `multi-step-wizard-v1` | Sequential steps with navigation and progress indicator |
|
|
303
|
+
| `side-by-side-comparison-v1` | Compare two versions with diff highlighting |
|
|
304
|
+
| `terminal-output-v1` | Display command output/logs with approve-to-continue |
|
|
305
|
+
|
|
306
|
+
## Notes
|
|
307
|
+
|
|
308
|
+
- Auth is **API key only**. Provide the key via `HaxClient(api_key=...)`; Clerk/session auth is not required for API access.
|
|
309
|
+
- API responses wrap resources (e.g., `{"request": {...}}`); the SDK unwraps this automatically.
|
|
310
|
+
- Template payloads and responses are flexible; consult the template configs for the fields each template expects/returns.
|
|
311
|
+
- The client supports context manager usage: `with HaxClient(...) as client:`
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# HAX Python SDK
|
|
2
|
+
|
|
3
|
+
Python client for the HAX (Human Approval eXchange) API. Enables agents and automated systems to programmatically collect human input.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The Python SDK is not yet published to PyPI. Install from source:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e sdks/python # from the repo root
|
|
11
|
+
# or
|
|
12
|
+
pip install -e . # from sdks/python/
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from hax import HaxClient
|
|
19
|
+
|
|
20
|
+
client = HaxClient(
|
|
21
|
+
api_key="hax_live_...",
|
|
22
|
+
base_url="http://localhost:3000/api/v1",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
request = client.create_request(
|
|
26
|
+
type="text-approval-v1",
|
|
27
|
+
payload={"text": "Deploy main to prod?", "approveLabel": "Ship it", "denyLabel": "Hold"},
|
|
28
|
+
webhook_url="https://myapp.com/webhook",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
print("Share with approver:", request.url)
|
|
32
|
+
|
|
33
|
+
# Poll until completed/expired/cancelled
|
|
34
|
+
request = client.wait_for_response(request.id, timeout=300)
|
|
35
|
+
if request.is_completed:
|
|
36
|
+
print("Decision:", request.response.get("decision"))
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Pydantic models**: Typed request/response models with validation
|
|
42
|
+
- **FormBuilder**: Fluent API for building typed forms with runtime type inference
|
|
43
|
+
- **E2E encryption**: RSA-OAEP + AES-GCM hybrid encryption for sensitive responses
|
|
44
|
+
- **Webhook verification**: HMAC-SHA256 signature verification
|
|
45
|
+
- **Delivery**: Send requests via email or SMS
|
|
46
|
+
- **Polling**: Built-in `wait_for_response` with configurable timeout
|
|
47
|
+
- **Error handling**: Typed exception hierarchy
|
|
48
|
+
|
|
49
|
+
## Request Methods
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# Create a request
|
|
53
|
+
request = client.create_request(
|
|
54
|
+
type="text-approval-v1",
|
|
55
|
+
payload={"text": "Approve this action?"},
|
|
56
|
+
title="Optional title",
|
|
57
|
+
description="Optional description",
|
|
58
|
+
webhook_url="https://myapp.com/webhook",
|
|
59
|
+
expires_in_seconds=3600,
|
|
60
|
+
metadata={"pr_number": 123},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Send via email
|
|
64
|
+
request = client.request_via_email(
|
|
65
|
+
type="confirm-action-v1",
|
|
66
|
+
payload={"title": "Approve?", "confirmPhrase": "YES"},
|
|
67
|
+
to_email="approver@example.com",
|
|
68
|
+
subject="Approval Required",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Send via SMS
|
|
72
|
+
request = client.request_via_sms(
|
|
73
|
+
type="text-approval-v1",
|
|
74
|
+
payload={"text": "Approve?"},
|
|
75
|
+
to_phone="+15551234567",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Get a request by ID
|
|
79
|
+
request = client.get_request("req_123")
|
|
80
|
+
|
|
81
|
+
# List recent requests
|
|
82
|
+
requests = client.list_requests()
|
|
83
|
+
|
|
84
|
+
# Cancel a pending request
|
|
85
|
+
cancelled = client.cancel_request("req_123")
|
|
86
|
+
|
|
87
|
+
# Submit a response (for testing)
|
|
88
|
+
completed = client.submit_response("req_123", {"decision": "approve"})
|
|
89
|
+
|
|
90
|
+
# Wait for completion with timeout
|
|
91
|
+
result = client.wait_for_response("req_123", poll_interval=2.0, timeout=60)
|
|
92
|
+
|
|
93
|
+
# List available template types
|
|
94
|
+
types = client.list_types()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Status Helpers
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
if request.is_pending:
|
|
101
|
+
print("Waiting for response...")
|
|
102
|
+
if request.is_completed:
|
|
103
|
+
print("Response:", request.response)
|
|
104
|
+
if request.is_expired:
|
|
105
|
+
print("Request expired")
|
|
106
|
+
if request.is_cancelled:
|
|
107
|
+
print("Request was cancelled")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## FormBuilder
|
|
111
|
+
|
|
112
|
+
Build typed forms with a fluent API:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from hax import HaxClient, FormBuilder
|
|
116
|
+
|
|
117
|
+
client = HaxClient(api_key="hax_live_...")
|
|
118
|
+
|
|
119
|
+
form = (FormBuilder()
|
|
120
|
+
.title("Event Registration")
|
|
121
|
+
.input("name", label="Full Name", required=True)
|
|
122
|
+
.input("email", label="Email", variant="email", required=True)
|
|
123
|
+
.number("age", label="Age", min=0, max=120)
|
|
124
|
+
.checkbox("newsletter", checkbox_label="Subscribe to newsletter"))
|
|
125
|
+
|
|
126
|
+
handle = client.create_form_request(form,
|
|
127
|
+
webhook_url="https://myapp.com/webhook")
|
|
128
|
+
|
|
129
|
+
print(f"Form URL: {handle.url}")
|
|
130
|
+
|
|
131
|
+
# Wait for typed response
|
|
132
|
+
response = handle.wait_for_response(timeout=300)
|
|
133
|
+
print(response.values.name) # str
|
|
134
|
+
print(response.values.email) # str
|
|
135
|
+
print(response.values.age) # float
|
|
136
|
+
print(response.values.newsletter) # bool
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Available Field Types
|
|
140
|
+
|
|
141
|
+
| Method | Output Type | Description |
|
|
142
|
+
|--------|------------|-------------|
|
|
143
|
+
| `.input(id)` | `str` | Text input (variants: text, email, url, tel) |
|
|
144
|
+
| `.textarea(id)` | `str` | Multi-line text input |
|
|
145
|
+
| `.select(id, options=...)` | `str` | Dropdown select |
|
|
146
|
+
| `.radio_group(id, options=...)` | `str` | Radio button group |
|
|
147
|
+
| `.date(id)` | `str` | Date picker (ISO format) |
|
|
148
|
+
| `.number(id)` | `float` | Numeric input |
|
|
149
|
+
| `.slider(id, min=, max=)` | `float` | Slider control |
|
|
150
|
+
| `.checkbox(id)` | `bool` | Single checkbox |
|
|
151
|
+
| `.switch(id)` | `bool` | Toggle switch |
|
|
152
|
+
| `.checkbox_group(id, options=...)` | `list[str]` | Multi-select checkboxes |
|
|
153
|
+
| `.hidden(id, value)` | `type(value)` | Hidden field |
|
|
154
|
+
|
|
155
|
+
## Webhooks
|
|
156
|
+
|
|
157
|
+
Verify and parse webhook events:
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from hax import verify_signature, parse_event
|
|
161
|
+
|
|
162
|
+
# In your webhook handler
|
|
163
|
+
def handle_webhook(request):
|
|
164
|
+
# Verify signature
|
|
165
|
+
is_valid = verify_signature(
|
|
166
|
+
payload=request.body,
|
|
167
|
+
signature=request.headers["X-Hax-Signature"],
|
|
168
|
+
secret="whsec_...",
|
|
169
|
+
)
|
|
170
|
+
if not is_valid:
|
|
171
|
+
return 400, "Invalid signature"
|
|
172
|
+
|
|
173
|
+
# Parse the event
|
|
174
|
+
event = parse_event(request.body)
|
|
175
|
+
|
|
176
|
+
if event.event_type == "completed":
|
|
177
|
+
print(f"Request {event.request_id} completed!")
|
|
178
|
+
print(f"Response: {event.response}")
|
|
179
|
+
elif event.event_type == "expired":
|
|
180
|
+
print(f"Request {event.request_id} expired")
|
|
181
|
+
|
|
182
|
+
return 200, "OK"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Event Types
|
|
186
|
+
|
|
187
|
+
- `request.sent` — Notification was delivered (email/SMS)
|
|
188
|
+
- `request.opened` — Human opened the request link
|
|
189
|
+
- `request.completed` — Human submitted a response
|
|
190
|
+
- `request.expired` — Request expired without action
|
|
191
|
+
|
|
192
|
+
## Encryption
|
|
193
|
+
|
|
194
|
+
For sensitive response data, use end-to-end encryption:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from hax import HaxClient
|
|
198
|
+
|
|
199
|
+
# Passphrase-based (automatic encrypt/decrypt)
|
|
200
|
+
client = HaxClient(
|
|
201
|
+
api_key="hax_live_...",
|
|
202
|
+
encryption_key="my-secret-passphrase",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Public key is automatically sent with requests
|
|
206
|
+
request = client.create_request(
|
|
207
|
+
type="text-approval-v1",
|
|
208
|
+
payload={"text": "Approve this sensitive action?"},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Response is automatically decrypted when retrieved
|
|
212
|
+
completed = client.get_request(request.id)
|
|
213
|
+
print(completed.response) # Decrypted plaintext
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Manual Decryption
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from hax import generate_key_pair, decrypt_response, is_encrypted_response
|
|
220
|
+
|
|
221
|
+
public_key, private_key = generate_key_pair("my-secret")
|
|
222
|
+
|
|
223
|
+
# Use public_key when creating the client
|
|
224
|
+
client = HaxClient(api_key="...", public_key=public_key)
|
|
225
|
+
|
|
226
|
+
# Later, manually decrypt
|
|
227
|
+
request = client.get_request("req_123")
|
|
228
|
+
if is_encrypted_response(request.response):
|
|
229
|
+
decrypted = decrypt_response(request.response["_encrypted"], private_key)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from hax import (
|
|
236
|
+
HaxError, # Base error
|
|
237
|
+
AuthenticationError, # Invalid API key (401)
|
|
238
|
+
ValidationError, # Invalid request data (400/422)
|
|
239
|
+
NotFoundError, # Resource not found (404)
|
|
240
|
+
RateLimitError, # Too many requests (429)
|
|
241
|
+
ServerError, # Server error (500+)
|
|
242
|
+
DecryptionError, # Decryption failure
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
request = client.create_request(...)
|
|
247
|
+
except AuthenticationError:
|
|
248
|
+
print("Check your API key")
|
|
249
|
+
except ValidationError as e:
|
|
250
|
+
print(f"Invalid request: {e}")
|
|
251
|
+
except RateLimitError:
|
|
252
|
+
print("Rate limited, try again later")
|
|
253
|
+
except HaxError as e:
|
|
254
|
+
print(f"API error: {e}")
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Template Types
|
|
258
|
+
|
|
259
|
+
| Template | Description |
|
|
260
|
+
|----------|-------------|
|
|
261
|
+
| `text-approval-v1` | Show text and collect an approve/deny decision |
|
|
262
|
+
| `confirm-action-v1` | Require typing a specific phrase to confirm a destructive action |
|
|
263
|
+
| `collect-email-v1` | Prompt the user for an email address |
|
|
264
|
+
| `form-builder` | Advanced forms with field types, layouts, validation, and conditional logic |
|
|
265
|
+
| `multi-choice-selection-v1` | Single or multiple selection from customizable option cards |
|
|
266
|
+
| `code-changes-v1` | GitHub-style diff view with inline line comments |
|
|
267
|
+
| `rich-text-editor-v1` | Markdown-formatted text editing for documents and reports |
|
|
268
|
+
| `file-upload-v1` | Collect files (documents, images, CSVs) from users |
|
|
269
|
+
| `signature-capture-v1` | Capture e-signatures with optional signer name and legal text |
|
|
270
|
+
| `data-table-review-v1` | Review, select, or edit tabular data |
|
|
271
|
+
| `scheduling-picker-v1` | Date/time slot selection with optional recurring schedules |
|
|
272
|
+
| `multi-step-wizard-v1` | Sequential steps with navigation and progress indicator |
|
|
273
|
+
| `side-by-side-comparison-v1` | Compare two versions with diff highlighting |
|
|
274
|
+
| `terminal-output-v1` | Display command output/logs with approve-to-continue |
|
|
275
|
+
|
|
276
|
+
## Notes
|
|
277
|
+
|
|
278
|
+
- Auth is **API key only**. Provide the key via `HaxClient(api_key=...)`; Clerk/session auth is not required for API access.
|
|
279
|
+
- API responses wrap resources (e.g., `{"request": {...}}`); the SDK unwraps this automatically.
|
|
280
|
+
- Template payloads and responses are flexible; consult the template configs for the fields each template expects/returns.
|
|
281
|
+
- The client supports context manager usage: `with HaxClient(...) as client:`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# HAX SDK Examples
|