assistant-runtime-sdk 1.0.0__py3-none-any.whl

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,161 @@
1
+ # Assistant Runtime SDK - Authentication
2
+ # Copyright (C) 2025 Paul Clinton
3
+ # AGPL-3.0 License
4
+
5
+ """
6
+ HMAC-SHA256 authentication for Assistant Runtime API requests.
7
+
8
+ Assistant Runtime uses HMAC-SHA256 signatures to authenticate API requests.
9
+ The signature format is: {timestamp}:{signature}
10
+ Where signature = HMAC-SHA256(tenant_secret, "{tenant_id}:{timestamp}:{params_json}")
11
+ """
12
+
13
+ import hmac
14
+ import hashlib
15
+ import json
16
+ import time
17
+ from typing import Dict, Any, Optional
18
+
19
+
20
+ def generate_signature(
21
+ tenant_id: str,
22
+ tenant_secret: str,
23
+ params: Dict[str, Any],
24
+ for_query_string: bool = False,
25
+ timestamp: Optional[int] = None,
26
+ ) -> str:
27
+ """
28
+ Generate HMAC-SHA256 signature for Assistant Runtime API request.
29
+
30
+ Args:
31
+ tenant_id: Unique tenant identifier
32
+ tenant_secret: HMAC secret for request signing
33
+ params: Request parameters (will be sorted by key)
34
+ for_query_string: If True, convert values to strings (for GET requests).
35
+ If False, keep original types (for POST JSON body).
36
+ timestamp: Optional timestamp override (for testing). Uses current time if None.
37
+
38
+ Returns:
39
+ Signature header value in format "timestamp:signature"
40
+
41
+ Example:
42
+ >>> sig = generate_signature("tenant-123", "secret", {"message": "Hello"})
43
+ >>> print(sig) # "1704067200:a1b2c3d4..."
44
+ """
45
+ if timestamp is None:
46
+ timestamp = int(time.time())
47
+ timestamp_str = str(timestamp)
48
+
49
+ if for_query_string:
50
+ # For GET requests: convert all values to strings to match how
51
+ # frappe.request.args returns all values as strings
52
+ params_to_sign = {k: str(v) if not isinstance(v, str) else v for k, v in params.items()}
53
+ else:
54
+ # For POST requests with JSON body: keep original types
55
+ # Assistant Runtime receives the parsed JSON with original types
56
+ params_to_sign = params
57
+
58
+ # JSON format must match Python's json.dumps with sort_keys and separators
59
+ # Assistant Runtime uses: json.dumps(params, sort_keys=True, separators=(', ', ': '))
60
+ params_json = json.dumps(params_to_sign, sort_keys=True, separators=(", ", ": "))
61
+
62
+ # Build message: tenant_id:timestamp:params_json
63
+ message = f"{tenant_id}:{timestamp_str}:{params_json}"
64
+
65
+ # Generate HMAC-SHA256 signature
66
+ signature = hmac.new(
67
+ tenant_secret.encode("utf-8"),
68
+ message.encode("utf-8"),
69
+ hashlib.sha256,
70
+ ).hexdigest()
71
+
72
+ return f"{timestamp_str}:{signature}"
73
+
74
+
75
+ def verify_signature(
76
+ signature_header: str,
77
+ tenant_id: str,
78
+ tenant_secret: str,
79
+ params: Dict[str, Any],
80
+ for_query_string: bool = False,
81
+ max_age_seconds: int = 300,
82
+ ) -> bool:
83
+ """
84
+ Verify HMAC-SHA256 signature from Assistant Runtime request.
85
+
86
+ Useful for servers receiving callbacks or webhooks from Assistant Runtime.
87
+
88
+ Args:
89
+ signature_header: The X-AR-Signature header value ("timestamp:signature")
90
+ tenant_id: Unique tenant identifier
91
+ tenant_secret: HMAC secret for request signing
92
+ params: Request parameters to verify
93
+ for_query_string: If True, treat params as query string (values as strings)
94
+ max_age_seconds: Maximum age of signature in seconds (default 5 minutes)
95
+
96
+ Returns:
97
+ True if signature is valid and not expired, False otherwise
98
+
99
+ Example:
100
+ >>> is_valid = verify_signature(
101
+ ... "1704067200:a1b2c3d4...",
102
+ ... "tenant-123",
103
+ ... "secret",
104
+ ... {"callback": "data"}
105
+ ... )
106
+ """
107
+ try:
108
+ parts = signature_header.split(":", 1)
109
+ if len(parts) != 2:
110
+ return False
111
+
112
+ timestamp_str, received_signature = parts
113
+ timestamp = int(timestamp_str)
114
+
115
+ # Check if signature is too old
116
+ current_time = int(time.time())
117
+ if abs(current_time - timestamp) > max_age_seconds:
118
+ return False
119
+
120
+ # Generate expected signature with same timestamp
121
+ expected = generate_signature(
122
+ tenant_id,
123
+ tenant_secret,
124
+ params,
125
+ for_query_string=for_query_string,
126
+ timestamp=timestamp,
127
+ )
128
+
129
+ # Compare signatures (timing-safe comparison)
130
+ return hmac.compare_digest(expected, signature_header)
131
+
132
+ except (ValueError, TypeError):
133
+ return False
134
+
135
+
136
+ def get_signature_header(
137
+ tenant_id: str,
138
+ tenant_secret: str,
139
+ params: Dict[str, Any],
140
+ for_query_string: bool = False,
141
+ ) -> Dict[str, str]:
142
+ """
143
+ Generate headers dict with Assistant Runtime signature for requests.
144
+
145
+ Convenience function that returns a dict ready to merge with other headers.
146
+
147
+ Args:
148
+ tenant_id: Unique tenant identifier
149
+ tenant_secret: HMAC secret
150
+ params: Request parameters
151
+ for_query_string: True for GET requests, False for POST JSON
152
+
153
+ Returns:
154
+ Dict with X-AR-Signature header
155
+
156
+ Example:
157
+ >>> headers = get_signature_header("tenant", "secret", {"msg": "hi"})
158
+ >>> requests.get(url, params=params, headers={**headers, **other_headers})
159
+ """
160
+ signature = generate_signature(tenant_id, tenant_secret, params, for_query_string)
161
+ return {"X-AR-Signature": signature}