asap-protocol 0.3.0__py3-none-any.whl → 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.
- asap/__init__.py +1 -1
- asap/cli.py +137 -2
- asap/errors.py +167 -0
- asap/examples/README.md +81 -10
- asap/examples/auth_patterns.py +212 -0
- asap/examples/error_recovery.py +248 -0
- asap/examples/long_running.py +287 -0
- asap/examples/mcp_integration.py +240 -0
- asap/examples/multi_step_workflow.py +134 -0
- asap/examples/orchestration.py +293 -0
- asap/examples/rate_limiting.py +137 -0
- asap/examples/run_demo.py +9 -4
- asap/examples/secure_handler.py +84 -0
- asap/examples/state_migration.py +240 -0
- asap/examples/streaming_response.py +108 -0
- asap/examples/websocket_concept.py +129 -0
- asap/mcp/__init__.py +43 -0
- asap/mcp/client.py +224 -0
- asap/mcp/protocol.py +179 -0
- asap/mcp/server.py +333 -0
- asap/mcp/server_runner.py +40 -0
- asap/models/__init__.py +4 -0
- asap/models/base.py +0 -3
- asap/models/constants.py +76 -1
- asap/models/entities.py +58 -7
- asap/models/envelope.py +14 -1
- asap/models/ids.py +8 -4
- asap/models/parts.py +33 -3
- asap/models/validators.py +16 -0
- asap/observability/__init__.py +6 -0
- asap/observability/dashboards/README.md +24 -0
- asap/observability/dashboards/asap-detailed.json +131 -0
- asap/observability/dashboards/asap-red.json +129 -0
- asap/observability/logging.py +81 -1
- asap/observability/metrics.py +15 -1
- asap/observability/trace_parser.py +238 -0
- asap/observability/trace_ui.py +218 -0
- asap/observability/tracing.py +293 -0
- asap/state/machine.py +15 -2
- asap/state/snapshot.py +0 -9
- asap/testing/__init__.py +31 -0
- asap/testing/assertions.py +108 -0
- asap/testing/fixtures.py +113 -0
- asap/testing/mocks.py +152 -0
- asap/transport/__init__.py +31 -0
- asap/transport/cache.py +180 -0
- asap/transport/circuit_breaker.py +194 -0
- asap/transport/client.py +989 -72
- asap/transport/compression.py +389 -0
- asap/transport/handlers.py +106 -53
- asap/transport/middleware.py +64 -39
- asap/transport/server.py +461 -94
- asap/transport/validators.py +320 -0
- asap/utils/__init__.py +7 -0
- asap/utils/sanitization.py +134 -0
- asap_protocol-1.0.0.dist-info/METADATA +264 -0
- asap_protocol-1.0.0.dist-info/RECORD +70 -0
- asap_protocol-0.3.0.dist-info/METADATA +0 -227
- asap_protocol-0.3.0.dist-info/RECORD +0 -37
- {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
- {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
- {asap_protocol-0.3.0.dist-info → asap_protocol-1.0.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Validation utilities for ASAP protocol envelopes.
|
|
2
|
+
|
|
3
|
+
This module provides validation functions for envelope security checks,
|
|
4
|
+
including timestamp validation for replay attack prevention and optional
|
|
5
|
+
nonce validation for duplicate detection.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from typing import Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from asap.errors import InvalidNonceError, InvalidTimestampError
|
|
14
|
+
from asap.models.constants import (
|
|
15
|
+
MAX_ENVELOPE_AGE_SECONDS,
|
|
16
|
+
MAX_FUTURE_TOLERANCE_SECONDS,
|
|
17
|
+
NONCE_TTL_SECONDS,
|
|
18
|
+
)
|
|
19
|
+
from asap.models.envelope import Envelope
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def validate_envelope_timestamp(envelope: Envelope) -> None:
|
|
23
|
+
"""Validate envelope timestamp to prevent replay attacks.
|
|
24
|
+
|
|
25
|
+
Checks that the envelope timestamp is:
|
|
26
|
+
- Not older than MAX_ENVELOPE_AGE_SECONDS (default: 5 minutes)
|
|
27
|
+
- Not more than MAX_FUTURE_TOLERANCE_SECONDS in the future (default: 30 seconds)
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
envelope: The envelope to validate
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
InvalidTimestampError: If the timestamp is too old or too far in the future
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> from asap.models.envelope import Envelope
|
|
37
|
+
>>> from asap.transport.validators import validate_envelope_timestamp
|
|
38
|
+
>>>
|
|
39
|
+
>>> # Recent envelope passes
|
|
40
|
+
>>> recent = Envelope(
|
|
41
|
+
... asap_version="0.1",
|
|
42
|
+
... sender="urn:asap:agent:test",
|
|
43
|
+
... recipient="urn:asap:agent:test",
|
|
44
|
+
... payload_type="TaskRequest",
|
|
45
|
+
... payload={}
|
|
46
|
+
... )
|
|
47
|
+
>>> validate_envelope_timestamp(recent) # No exception
|
|
48
|
+
>>>
|
|
49
|
+
>>> # Old envelope raises error
|
|
50
|
+
>>> from datetime import datetime, timezone, timedelta
|
|
51
|
+
>>> old = Envelope(
|
|
52
|
+
... asap_version="0.1",
|
|
53
|
+
... sender="urn:asap:agent:test",
|
|
54
|
+
... recipient="urn:asap:agent:test",
|
|
55
|
+
... payload_type="TaskRequest",
|
|
56
|
+
... payload={},
|
|
57
|
+
... timestamp=datetime.now(timezone.utc) - timedelta(minutes=10)
|
|
58
|
+
... )
|
|
59
|
+
>>> validate_envelope_timestamp(old) # Raises InvalidTimestampError
|
|
60
|
+
"""
|
|
61
|
+
if envelope.timestamp is None:
|
|
62
|
+
raise InvalidTimestampError(
|
|
63
|
+
timestamp="",
|
|
64
|
+
message="Envelope timestamp is required for validation",
|
|
65
|
+
details={"envelope_id": envelope.id},
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
now = datetime.now(timezone.utc)
|
|
69
|
+
timestamp = envelope.timestamp
|
|
70
|
+
|
|
71
|
+
if timestamp.tzinfo is None:
|
|
72
|
+
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
|
73
|
+
else:
|
|
74
|
+
# Convert to UTC if needed
|
|
75
|
+
timestamp = timestamp.astimezone(timezone.utc)
|
|
76
|
+
|
|
77
|
+
# Calculate age in seconds
|
|
78
|
+
age_seconds = (now - timestamp).total_seconds()
|
|
79
|
+
|
|
80
|
+
if age_seconds > MAX_ENVELOPE_AGE_SECONDS:
|
|
81
|
+
raise InvalidTimestampError(
|
|
82
|
+
timestamp=timestamp.isoformat(),
|
|
83
|
+
message=(
|
|
84
|
+
f"Envelope timestamp is too old: {age_seconds:.1f} seconds "
|
|
85
|
+
f"(max: {MAX_ENVELOPE_AGE_SECONDS} seconds)"
|
|
86
|
+
),
|
|
87
|
+
age_seconds=age_seconds,
|
|
88
|
+
details={
|
|
89
|
+
"envelope_id": envelope.id,
|
|
90
|
+
"max_age_seconds": MAX_ENVELOPE_AGE_SECONDS,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
future_offset_seconds = (timestamp - now).total_seconds()
|
|
95
|
+
if future_offset_seconds > MAX_FUTURE_TOLERANCE_SECONDS:
|
|
96
|
+
raise InvalidTimestampError(
|
|
97
|
+
timestamp=timestamp.isoformat(),
|
|
98
|
+
message=(
|
|
99
|
+
f"Envelope timestamp is too far in the future: "
|
|
100
|
+
f"{future_offset_seconds:.1f} seconds "
|
|
101
|
+
f"(max tolerance: {MAX_FUTURE_TOLERANCE_SECONDS} seconds)"
|
|
102
|
+
),
|
|
103
|
+
future_offset_seconds=future_offset_seconds,
|
|
104
|
+
details={
|
|
105
|
+
"envelope_id": envelope.id,
|
|
106
|
+
"max_future_tolerance_seconds": MAX_FUTURE_TOLERANCE_SECONDS,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@runtime_checkable
|
|
112
|
+
class NonceStore(Protocol):
|
|
113
|
+
"""Protocol for nonce storage and validation.
|
|
114
|
+
|
|
115
|
+
Implementations must provide thread-safe storage and TTL-based expiration
|
|
116
|
+
for nonce values to prevent replay attacks.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def is_used(self, nonce: str) -> bool:
|
|
120
|
+
"""Check if a nonce has been used.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
nonce: The nonce value to check
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if the nonce has been used, False otherwise
|
|
127
|
+
"""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
def mark_used(self, nonce: str, ttl_seconds: int) -> None:
|
|
131
|
+
"""Mark a nonce as used with a TTL.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
nonce: The nonce value to mark as used
|
|
135
|
+
ttl_seconds: Time-to-live in seconds for the nonce
|
|
136
|
+
"""
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
def check_and_mark(self, nonce: str, ttl_seconds: int) -> bool:
|
|
140
|
+
"""Atomically check if nonce is used and mark it if not.
|
|
141
|
+
|
|
142
|
+
This method performs both check and mark operations atomically to
|
|
143
|
+
prevent race conditions between concurrent requests.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
nonce: The nonce value to check and mark
|
|
147
|
+
ttl_seconds: Time-to-live in seconds for the nonce
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
True if nonce was already used, False if it was newly marked
|
|
151
|
+
"""
|
|
152
|
+
...
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class InMemoryNonceStore:
|
|
156
|
+
"""In-memory nonce store with TTL-based expiration.
|
|
157
|
+
|
|
158
|
+
This implementation uses a dictionary to store nonces with their
|
|
159
|
+
expiration times. Expired nonces are lazily cleaned up on access.
|
|
160
|
+
|
|
161
|
+
Attributes:
|
|
162
|
+
_store: Dictionary mapping nonce to expiration timestamp
|
|
163
|
+
_lock: Thread lock for thread-safe operations
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(self) -> None:
|
|
167
|
+
"""Initialize in-memory nonce store."""
|
|
168
|
+
self._store: dict[str, float] = {}
|
|
169
|
+
self._lock = threading.RLock()
|
|
170
|
+
|
|
171
|
+
def _cleanup_expired(self) -> None:
|
|
172
|
+
"""Remove expired nonces from the store.
|
|
173
|
+
|
|
174
|
+
This is called lazily during access operations to keep the store
|
|
175
|
+
from growing unbounded.
|
|
176
|
+
"""
|
|
177
|
+
now = time.time()
|
|
178
|
+
expired = [nonce for nonce, expiry in self._store.items() if expiry < now]
|
|
179
|
+
for nonce in expired:
|
|
180
|
+
self._store.pop(nonce, None)
|
|
181
|
+
|
|
182
|
+
def is_used(self, nonce: str) -> bool:
|
|
183
|
+
"""Check if a nonce has been used.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
nonce: The nonce value to check
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
True if the nonce has been used and not expired, False otherwise
|
|
190
|
+
"""
|
|
191
|
+
with self._lock:
|
|
192
|
+
self._cleanup_expired()
|
|
193
|
+
if nonce not in self._store:
|
|
194
|
+
return False
|
|
195
|
+
expiry = self._store[nonce]
|
|
196
|
+
return expiry >= time.time()
|
|
197
|
+
|
|
198
|
+
def mark_used(self, nonce: str, ttl_seconds: int) -> None:
|
|
199
|
+
"""Mark a nonce as used with a TTL.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
nonce: The nonce value to mark as used
|
|
203
|
+
ttl_seconds: Time-to-live in seconds for the nonce
|
|
204
|
+
"""
|
|
205
|
+
with self._lock:
|
|
206
|
+
self._cleanup_expired()
|
|
207
|
+
expiry = time.time() + ttl_seconds
|
|
208
|
+
self._store[nonce] = expiry
|
|
209
|
+
|
|
210
|
+
def check_and_mark(self, nonce: str, ttl_seconds: int) -> bool:
|
|
211
|
+
"""Atomically check if nonce is used and mark it if not.
|
|
212
|
+
|
|
213
|
+
This method performs both check and mark operations atomically to
|
|
214
|
+
prevent race conditions between concurrent requests with the same nonce.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
nonce: The nonce value to check and mark
|
|
218
|
+
ttl_seconds: Time-to-live in seconds for the nonce
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
True if nonce was already used, False if it was newly marked
|
|
222
|
+
"""
|
|
223
|
+
with self._lock:
|
|
224
|
+
self._cleanup_expired()
|
|
225
|
+
if nonce in self._store and self._store[nonce] >= time.time():
|
|
226
|
+
return True # Already used
|
|
227
|
+
self._store[nonce] = time.time() + ttl_seconds
|
|
228
|
+
return False # Newly marked
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def validate_envelope_nonce(envelope: Envelope, nonce_store: NonceStore | None) -> None:
|
|
232
|
+
"""Validate envelope nonce to prevent duplicate message replay.
|
|
233
|
+
|
|
234
|
+
If the envelope has a nonce in its extensions, checks that it hasn't
|
|
235
|
+
been used before. If no nonce is present, validation passes (nonce
|
|
236
|
+
is optional). If a nonce_store is not provided, validation is skipped.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
envelope: The envelope to validate
|
|
240
|
+
nonce_store: Optional nonce store for duplicate detection
|
|
241
|
+
|
|
242
|
+
Raises:
|
|
243
|
+
InvalidNonceError: If the nonce has been used before
|
|
244
|
+
|
|
245
|
+
Example:
|
|
246
|
+
>>> from asap.models.envelope import Envelope
|
|
247
|
+
>>> from asap.transport.validators import (
|
|
248
|
+
... validate_envelope_nonce,
|
|
249
|
+
... InMemoryNonceStore
|
|
250
|
+
... )
|
|
251
|
+
>>>
|
|
252
|
+
>>> store = InMemoryNonceStore()
|
|
253
|
+
>>>
|
|
254
|
+
>>> # Envelope without nonce passes
|
|
255
|
+
>>> envelope1 = Envelope(
|
|
256
|
+
... asap_version="0.1",
|
|
257
|
+
... sender="urn:asap:agent:test",
|
|
258
|
+
... recipient="urn:asap:agent:test",
|
|
259
|
+
... payload_type="TaskRequest",
|
|
260
|
+
... payload={}
|
|
261
|
+
... )
|
|
262
|
+
>>> validate_envelope_nonce(envelope1, store) # No exception
|
|
263
|
+
>>>
|
|
264
|
+
>>> # First use of nonce passes
|
|
265
|
+
>>> envelope2 = Envelope(
|
|
266
|
+
... asap_version="0.1",
|
|
267
|
+
... sender="urn:asap:agent:test",
|
|
268
|
+
... recipient="urn:asap:agent:test",
|
|
269
|
+
... payload_type="TaskRequest",
|
|
270
|
+
... payload={},
|
|
271
|
+
... extensions={"nonce": "unique-nonce-123"}
|
|
272
|
+
... )
|
|
273
|
+
>>> validate_envelope_nonce(envelope2, store) # No exception
|
|
274
|
+
>>>
|
|
275
|
+
>>> # Duplicate nonce raises error
|
|
276
|
+
>>> envelope3 = Envelope(
|
|
277
|
+
... asap_version="0.1",
|
|
278
|
+
... sender="urn:asap:agent:test",
|
|
279
|
+
... recipient="urn:asap:agent:test",
|
|
280
|
+
... payload_type="TaskRequest",
|
|
281
|
+
... payload={},
|
|
282
|
+
... extensions={"nonce": "unique-nonce-123"}
|
|
283
|
+
... )
|
|
284
|
+
>>> validate_envelope_nonce(envelope3, store) # Raises InvalidNonceError
|
|
285
|
+
"""
|
|
286
|
+
# Skip validation if no nonce store provided
|
|
287
|
+
if nonce_store is None:
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
# Skip validation if no nonce in extensions
|
|
291
|
+
if not envelope.extensions or "nonce" not in envelope.extensions:
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
nonce = envelope.extensions["nonce"]
|
|
295
|
+
|
|
296
|
+
if not isinstance(nonce, str) or not nonce:
|
|
297
|
+
raise InvalidNonceError(
|
|
298
|
+
nonce=str(nonce),
|
|
299
|
+
message=(
|
|
300
|
+
f"Nonce must be a non-empty string, got "
|
|
301
|
+
f"{type(nonce).__name__ if not isinstance(nonce, str) else 'empty string'}"
|
|
302
|
+
),
|
|
303
|
+
details={"envelope_id": envelope.id},
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Atomically check and mark nonce as used to prevent race conditions
|
|
307
|
+
if nonce_store.check_and_mark(nonce, ttl_seconds=NONCE_TTL_SECONDS):
|
|
308
|
+
raise InvalidNonceError(
|
|
309
|
+
nonce=nonce,
|
|
310
|
+
message=f"Duplicate nonce detected: {nonce}",
|
|
311
|
+
details={"envelope_id": envelope.id},
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
__all__ = [
|
|
316
|
+
"NonceStore",
|
|
317
|
+
"InMemoryNonceStore",
|
|
318
|
+
"validate_envelope_timestamp",
|
|
319
|
+
"validate_envelope_nonce",
|
|
320
|
+
]
|
asap/utils/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Log sanitization utilities for sensitive data protection.
|
|
2
|
+
|
|
3
|
+
This module provides functions to sanitize sensitive data before logging
|
|
4
|
+
to prevent credential leaks, token exposure, and other security issues.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from asap.utils.sanitization import sanitize_token, sanitize_nonce, sanitize_url
|
|
8
|
+
>>>
|
|
9
|
+
>>> # Sanitize a token (shows only first 8 characters)
|
|
10
|
+
>>> token = "sk_live_1234567890abcdef"
|
|
11
|
+
>>> sanitize_token(token) # Returns: "sk_live_..."
|
|
12
|
+
>>>
|
|
13
|
+
>>> # Sanitize a nonce (shows first 8 characters + "...")
|
|
14
|
+
>>> nonce = "a1b2c3d4e5f6g7h8i9j0"
|
|
15
|
+
>>> sanitize_nonce(nonce) # Returns: "a1b2c3d4..."
|
|
16
|
+
>>>
|
|
17
|
+
>>> # Sanitize a URL with credentials
|
|
18
|
+
>>> url = "https://user:password@example.com/api"
|
|
19
|
+
>>> sanitize_url(url) # Returns: "https://user:***@example.com/api"
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from urllib.parse import urlparse, urlunparse
|
|
24
|
+
|
|
25
|
+
# Sanitization configuration
|
|
26
|
+
SANITIZE_PREFIX_LENGTH = 8
|
|
27
|
+
"""Number of characters to show when truncating sensitive values.
|
|
28
|
+
|
|
29
|
+
This constant defines how many characters of a sensitive value (token, nonce)
|
|
30
|
+
are preserved when sanitizing for logs. The value balances security (preventing
|
|
31
|
+
full exposure) with debuggability (allowing identification of value types).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def sanitize_token(token: str) -> str:
|
|
36
|
+
"""Sanitize a token for safe logging.
|
|
37
|
+
|
|
38
|
+
Returns only the first SANITIZE_PREFIX_LENGTH characters followed by "..."
|
|
39
|
+
to prevent full token exposure in logs while still allowing identification
|
|
40
|
+
of token type (e.g., "sk_live_", "pk_test_").
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
token: The token to sanitize
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Sanitized token string showing only prefix (e.g., "sk_live_...")
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> sanitize_token("sk_live_1234567890abcdef")
|
|
50
|
+
'sk_live_...'
|
|
51
|
+
>>> sanitize_token("short")
|
|
52
|
+
'short'
|
|
53
|
+
"""
|
|
54
|
+
if not token:
|
|
55
|
+
return ""
|
|
56
|
+
if len(token) <= SANITIZE_PREFIX_LENGTH:
|
|
57
|
+
return token
|
|
58
|
+
return f"{token[:SANITIZE_PREFIX_LENGTH]}..."
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def sanitize_nonce(nonce: str) -> str:
|
|
62
|
+
"""Sanitize a nonce for safe logging.
|
|
63
|
+
|
|
64
|
+
Returns the first SANITIZE_PREFIX_LENGTH characters followed by "..."
|
|
65
|
+
to prevent full nonce exposure in logs while still allowing identification
|
|
66
|
+
for debugging purposes.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
nonce: The nonce to sanitize
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Sanitized nonce string showing first characters + "..." (e.g., "a1b2c3d4...")
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> sanitize_nonce("a1b2c3d4e5f6g7h8i9j0")
|
|
76
|
+
'a1b2c3d4...'
|
|
77
|
+
>>> sanitize_nonce("short")
|
|
78
|
+
'short'
|
|
79
|
+
"""
|
|
80
|
+
if not nonce:
|
|
81
|
+
return ""
|
|
82
|
+
if len(nonce) <= SANITIZE_PREFIX_LENGTH:
|
|
83
|
+
return nonce
|
|
84
|
+
return f"{nonce[:SANITIZE_PREFIX_LENGTH]}..."
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def sanitize_url(url: str) -> str:
|
|
88
|
+
"""Sanitize a URL by masking credentials in the userinfo component.
|
|
89
|
+
|
|
90
|
+
Masks passwords in URLs of the form:
|
|
91
|
+
- `https://user:password@example.com` → `https://user:***@example.com`
|
|
92
|
+
- `https://user@example.com` → `https://user@example.com` (no change)
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
url: The URL to sanitize
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Sanitized URL with masked credentials
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> sanitize_url("https://user:secret@example.com/api")
|
|
102
|
+
'https://user:***@example.com/api'
|
|
103
|
+
>>> sanitize_url("https://example.com/api")
|
|
104
|
+
'https://example.com/api'
|
|
105
|
+
"""
|
|
106
|
+
if not url:
|
|
107
|
+
return ""
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
parsed = urlparse(url)
|
|
111
|
+
if (parsed.username or parsed.password) and parsed.password:
|
|
112
|
+
netloc = f"{parsed.username}:***@{parsed.hostname}"
|
|
113
|
+
if parsed.port:
|
|
114
|
+
netloc = f"{netloc}:{parsed.port}"
|
|
115
|
+
return urlunparse(
|
|
116
|
+
(
|
|
117
|
+
parsed.scheme,
|
|
118
|
+
netloc,
|
|
119
|
+
parsed.path,
|
|
120
|
+
parsed.params,
|
|
121
|
+
parsed.query,
|
|
122
|
+
parsed.fragment,
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
return url
|
|
126
|
+
except Exception:
|
|
127
|
+
return re.sub(r"://[^:]+:[^@]+@", r"://***:***@", url)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__all__ = [
|
|
131
|
+
"sanitize_token",
|
|
132
|
+
"sanitize_nonce",
|
|
133
|
+
"sanitize_url",
|
|
134
|
+
]
|