@agentsdock/server 1.0.7-beta.5
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.
- package/LICENSE +202 -0
- package/NOTICE +19 -0
- package/README.md +60 -0
- package/npm/cli.cjs +289 -0
- package/package.json +32 -0
- package/server/LICENSE +202 -0
- package/server/NOTICE +19 -0
- package/server/VERSION +1 -0
- package/server/activation_transaction.py +2458 -0
- package/server/agent_server.py +93226 -0
- package/server/agentsdock_chats.py +1237 -0
- package/server/agentsdock_emergency.py +338 -0
- package/server/agentsdock_jobs.py +640 -0
- package/server/agentsdock_mail.py +325 -0
- package/server/agentsdock_publish.py +404 -0
- package/server/agentsdock_team.py +793 -0
- package/server/agentsdock_team_hub/__init__.py +33 -0
- package/server/agentsdock_team_hub/auth.py +710 -0
- package/server/agentsdock_team_hub/cli.py +647 -0
- package/server/agentsdock_team_hub/database.py +310 -0
- package/server/agentsdock_team_hub/mail_hint_streams.py +176 -0
- package/server/agentsdock_team_hub/mail_hints.py +201 -0
- package/server/agentsdock_team_hub/migrations/0001_identity_auth.sql +690 -0
- package/server/agentsdock_team_hub/migrations/0002_teamspace_ledger.sql +1336 -0
- package/server/agentsdock_team_hub/migrations/0003_service_runtime.sql +361 -0
- package/server/agentsdock_team_hub/migrations/0004_managed_host_binding.sql +22 -0
- package/server/agentsdock_team_hub/migrations/0005_tailnet_bootstrap_delegations.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0006_team_network_mailbox.sql +423 -0
- package/server/agentsdock_team_hub/migrations/0007_local_agent_mail.sql +63 -0
- package/server/agentsdock_team_hub/migrations/0008_managed_server_session.sql +73 -0
- package/server/agentsdock_team_hub/migrations/0009_team_messages.sql +324 -0
- package/server/agentsdock_team_hub/migrations/0010_team_attachment_orphan_reclamation.sql +52 -0
- package/server/agentsdock_team_hub/migrations/0011_human_admin_paging.sql +65 -0
- package/server/agentsdock_team_hub/migrations/0012_network_content_deletions.sql +69 -0
- package/server/agentsdock_team_hub/migrations/0013_team_message_revisions.sql +48 -0
- package/server/agentsdock_team_hub/migrations/0014_managed_network_owner.sql +50 -0
- package/server/agentsdock_team_hub/migrations/0015_team_message_inbox_dismissals.sql +3 -0
- package/server/agentsdock_team_hub/migrations/0016_team_message_all_servers.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0017_skill_announcement_deletions.sql +16 -0
- package/server/agentsdock_team_hub/migrations/0018_team_mail_subjects.sql +8 -0
- package/server/agentsdock_team_hub/migrations/0019_team_mailbox_state.sql +5 -0
- package/server/agentsdock_team_hub/migrations/0020_team_mail_arrivals.sql +44 -0
- package/server/agentsdock_team_hub/migrations/0021_team_mail_threads.sql +4 -0
- package/server/agentsdock_team_hub/migrations/0022_team_bulletin_changes.sql +49 -0
- package/server/agentsdock_team_hub/migrations/0023_team_message_search.sql +93 -0
- package/server/agentsdock_team_hub/migrations/__init__.py +1 -0
- package/server/agentsdock_team_hub/notification_hints.py +306 -0
- package/server/agentsdock_team_hub/secure_peer.py +11215 -0
- package/server/agentsdock_team_hub/secure_peer_hub.py +1372 -0
- package/server/agentsdock_team_hub/security.py +318 -0
- package/server/agentsdock_team_hub/service.py +1737 -0
- package/server/agentsdock_team_hub/store.py +16276 -0
- package/server/chat_mailbox.py +448 -0
- package/server/claude_background_reconciliation.py +92 -0
- package/server/claude_goals.py +245 -0
- package/server/claude_history_provenance.py +137 -0
- package/server/claude_history_repair.py +1601 -0
- package/server/claude_sdk_client.py +3731 -0
- package/server/claude_side_question.py +212 -0
- package/server/codex_app_server.py +3030 -0
- package/server/codex_auth.py +93 -0
- package/server/codex_history_repair.py +1247 -0
- package/server/codex_provider.py +1120 -0
- package/server/codex_side_question.py +301 -0
- package/server/cursor_agent_client.py +654 -0
- package/server/cursor_process_guard.py +347 -0
- package/server/execution_activation.py +610 -0
- package/server/execution_control.py +233 -0
- package/server/execution_durability.py +184 -0
- package/server/execution_http.py +256 -0
- package/server/execution_install.py +792 -0
- package/server/execution_legacy_runner.py +406 -0
- package/server/execution_maintenance.py +137 -0
- package/server/execution_manage.py +615 -0
- package/server/execution_ownership.py +83 -0
- package/server/execution_preparation.py +362 -0
- package/server/execution_recovery.py +845 -0
- package/server/execution_recovery_status.py +195 -0
- package/server/execution_service.py +387 -0
- package/server/execution_transport.py +622 -0
- package/server/execution_uninstall.py +406 -0
- package/server/execution_update_status.py +49 -0
- package/server/install.sh +7074 -0
- package/server/interactive_chat_controls.py +153 -0
- package/server/interactive_chat_native.py +66 -0
- package/server/interactive_chat_projection.py +152 -0
- package/server/interactive_chat_runtime.py +132 -0
- package/server/interactive_chat_share_routes.py +534 -0
- package/server/interactive_chat_share_web.py +12 -0
- package/server/interactive_chat_shares.py +238 -0
- package/server/provider_commands.py +425 -0
- package/server/public_chat_share_routes.py +426 -0
- package/server/public_chat_shares.py +759 -0
- package/server/public_chat_transcript.py +357 -0
- package/server/pyproject.toml +29 -0
- package/server/release-public-key.pem +3 -0
- package/server/secure_peer_delivery.py +1236 -0
- package/server/secure_peer_runtime.py +7694 -0
- package/server/shared_chat_video_stream.py +262 -0
- package/server/shared_chat_videos.py +155 -0
- package/server/side_questions.py +594 -0
- package/server/team_hub_host.py +1354 -0
- package/server/team_mail_grants.py +136 -0
- package/server/team_mail_runtime.py +302 -0
- package/server/team_mail_websocket.py +211 -0
- package/server/title_generation.py +333 -0
- package/server/uninstall.sh +359 -0
- package/server/update_handoff.py +237 -0
- package/server/update_preparation.py +264 -0
- package/server/update_recovery.py +140 -0
- package/server/update_runner.py +1819 -0
- package/server/uv.lock +1101 -0
- package/server/workspace_git.py +545 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Local key lifecycle and compact signed Team Hub access credentials."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
import secrets
|
|
13
|
+
import stat
|
|
14
|
+
import time
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ACCESS_TOKEN_TTL_SECONDS = 10 * 60
|
|
19
|
+
SESSION_TTL_SECONDS = 30 * 24 * 60 * 60
|
|
20
|
+
BOOTSTRAP_PROOF_TTL_SECONDS = 15 * 60
|
|
21
|
+
TOKEN_ISSUER = "agentsdock-team-hub"
|
|
22
|
+
TOKEN_AUDIENCE = "agentsdock-team-hub-api"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TokenError(RuntimeError):
|
|
26
|
+
"""A signed or opaque credential failed closed."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class MintedAccessToken:
|
|
31
|
+
token: str
|
|
32
|
+
jti: str
|
|
33
|
+
expires_at: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def now_seconds() -> int:
|
|
37
|
+
return int(time.time())
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def token_hash(value: str) -> bytes:
|
|
41
|
+
if not isinstance(value, str) or not 16 <= len(value) <= 4096:
|
|
42
|
+
hashlib.sha256(b"invalid-team-hub-credential").digest()
|
|
43
|
+
raise TokenError("credential is invalid or unavailable")
|
|
44
|
+
try:
|
|
45
|
+
encoded = value.encode("utf-8")
|
|
46
|
+
except UnicodeEncodeError as exc:
|
|
47
|
+
hashlib.sha256(b"invalid-team-hub-credential").digest()
|
|
48
|
+
raise TokenError("credential is invalid or unavailable") from exc
|
|
49
|
+
return hashlib.sha256(encoded).digest()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def opaque_secret(prefix: str) -> tuple[str, bytes]:
|
|
53
|
+
token = f"{prefix}.{secrets.token_urlsafe(32)}"
|
|
54
|
+
return token, hashlib.sha256(token.encode("ascii")).digest()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _b64url_encode(value: bytes) -> str:
|
|
58
|
+
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _b64url_decode(value: str) -> bytes:
|
|
62
|
+
if not value or len(value) > 8192:
|
|
63
|
+
raise TokenError("credential is invalid or unavailable")
|
|
64
|
+
try:
|
|
65
|
+
return base64.b64decode(
|
|
66
|
+
value.encode("ascii") + b"=" * (-len(value) % 4),
|
|
67
|
+
altchars=b"-_",
|
|
68
|
+
validate=True,
|
|
69
|
+
)
|
|
70
|
+
except (UnicodeEncodeError, ValueError) as exc:
|
|
71
|
+
raise TokenError("credential is invalid or unavailable") from exc
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def canonical_json(value: Any) -> bytes:
|
|
75
|
+
return json.dumps(
|
|
76
|
+
value, sort_keys=True, separators=(",", ":"), ensure_ascii=False
|
|
77
|
+
).encode("utf-8")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def canonical_fingerprint(value: Any) -> bytes:
|
|
81
|
+
return hashlib.sha256(canonical_json(value)).digest()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _validate_secret_file(path: Path) -> bytes:
|
|
85
|
+
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
86
|
+
descriptor = os.open(path, flags)
|
|
87
|
+
try:
|
|
88
|
+
info = os.fstat(descriptor)
|
|
89
|
+
if (
|
|
90
|
+
not stat.S_ISREG(info.st_mode)
|
|
91
|
+
or info.st_nlink != 1
|
|
92
|
+
or info.st_uid != os.getuid()
|
|
93
|
+
):
|
|
94
|
+
raise PermissionError(f"secret path is not an owner-controlled regular file: {path}")
|
|
95
|
+
if stat.S_IMODE(info.st_mode) != 0o600:
|
|
96
|
+
raise PermissionError(f"secret file must have mode 0600: {path}")
|
|
97
|
+
chunks: list[bytes] = []
|
|
98
|
+
remaining = 4097
|
|
99
|
+
while remaining:
|
|
100
|
+
chunk = os.read(descriptor, min(remaining, 4097))
|
|
101
|
+
if not chunk:
|
|
102
|
+
break
|
|
103
|
+
chunks.append(chunk)
|
|
104
|
+
remaining -= len(chunk)
|
|
105
|
+
data = b"".join(chunks)
|
|
106
|
+
if not 32 <= len(data) <= 4096:
|
|
107
|
+
raise PermissionError(f"secret file has an invalid size: {path}")
|
|
108
|
+
return data
|
|
109
|
+
finally:
|
|
110
|
+
os.close(descriptor)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ensure_private_directory(path: Path) -> None:
|
|
114
|
+
path.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
115
|
+
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
116
|
+
flags |= getattr(os, "O_DIRECTORY", 0)
|
|
117
|
+
descriptor = os.open(path, flags)
|
|
118
|
+
try:
|
|
119
|
+
info = os.fstat(descriptor)
|
|
120
|
+
if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid():
|
|
121
|
+
raise PermissionError(f"Team Hub data path must be an owner-controlled directory: {path}")
|
|
122
|
+
os.fchmod(descriptor, 0o700)
|
|
123
|
+
hardened = os.fstat(descriptor)
|
|
124
|
+
if stat.S_IMODE(hardened.st_mode) != 0o700 or hardened.st_uid != os.getuid():
|
|
125
|
+
raise PermissionError(f"Team Hub data directory must have mode 0700: {path}")
|
|
126
|
+
finally:
|
|
127
|
+
os.close(descriptor)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def create_secret_file(path: Path, value: bytes) -> None:
|
|
131
|
+
"""Create an owner-only secret without following links or overwriting data."""
|
|
132
|
+
|
|
133
|
+
ensure_private_directory(path.parent)
|
|
134
|
+
directory_flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0)
|
|
135
|
+
directory_flags |= getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_DIRECTORY", 0)
|
|
136
|
+
directory_descriptor = os.open(path.parent, directory_flags)
|
|
137
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
|
|
138
|
+
flags |= getattr(os, "O_CLOEXEC", 0)
|
|
139
|
+
flags |= getattr(os, "O_NOFOLLOW", 0)
|
|
140
|
+
try:
|
|
141
|
+
descriptor = os.open(path.name, flags, 0o600, dir_fd=directory_descriptor)
|
|
142
|
+
try:
|
|
143
|
+
os.fchmod(descriptor, 0o600)
|
|
144
|
+
written = 0
|
|
145
|
+
while written < len(value):
|
|
146
|
+
written += os.write(descriptor, value[written:])
|
|
147
|
+
os.fsync(descriptor)
|
|
148
|
+
finally:
|
|
149
|
+
os.close(descriptor)
|
|
150
|
+
os.fsync(directory_descriptor)
|
|
151
|
+
finally:
|
|
152
|
+
os.close(directory_descriptor)
|
|
153
|
+
_validate_secret_file(path)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_or_create_signing_key(path: Path) -> bytes:
|
|
157
|
+
try:
|
|
158
|
+
return _validate_secret_file(path)
|
|
159
|
+
except FileNotFoundError:
|
|
160
|
+
key = secrets.token_bytes(32)
|
|
161
|
+
try:
|
|
162
|
+
create_secret_file(path, key)
|
|
163
|
+
except FileExistsError:
|
|
164
|
+
return _validate_secret_file(path)
|
|
165
|
+
return key
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def read_secret_file(path: Path) -> bytes:
|
|
169
|
+
return _validate_secret_file(path)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def validate_tls_files(certificate_path: Path, private_key_path: Path) -> None:
|
|
173
|
+
"""Fail closed on unsafe listener TLS material before starting Uvicorn.
|
|
174
|
+
|
|
175
|
+
Certificates are public and may use ordinary read permissions. The
|
|
176
|
+
private key must be a single-link, current-user-owned regular file with
|
|
177
|
+
exact mode 0600. Both files are opened without following symlinks and are
|
|
178
|
+
bounded so devices/FIFOs and implausibly large inputs never reach the TLS
|
|
179
|
+
loader.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def inspect(path: Path, *, private: bool) -> None:
|
|
183
|
+
flags = os.O_RDONLY | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0)
|
|
184
|
+
descriptor = os.open(path, flags)
|
|
185
|
+
try:
|
|
186
|
+
info = os.fstat(descriptor)
|
|
187
|
+
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
|
188
|
+
raise PermissionError(f"TLS path must be a single-link regular file: {path}")
|
|
189
|
+
if private and (
|
|
190
|
+
info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) != 0o600
|
|
191
|
+
):
|
|
192
|
+
raise PermissionError(
|
|
193
|
+
f"TLS private key must be owned by the current user with mode 0600: {path}"
|
|
194
|
+
)
|
|
195
|
+
if not 1 <= info.st_size <= 1024 * 1024:
|
|
196
|
+
raise PermissionError(f"TLS file has an invalid size: {path}")
|
|
197
|
+
# Force a read now so an unreadable or unexpectedly truncated file
|
|
198
|
+
# is rejected before the listener is created. Uvicorn performs the
|
|
199
|
+
# cryptographic PEM validation when loading its SSL context.
|
|
200
|
+
if not os.read(descriptor, 1):
|
|
201
|
+
raise PermissionError(f"TLS file is empty: {path}")
|
|
202
|
+
finally:
|
|
203
|
+
os.close(descriptor)
|
|
204
|
+
|
|
205
|
+
inspect(Path(certificate_path), private=False)
|
|
206
|
+
inspect(Path(private_key_path), private=True)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AccessTokenSigner:
|
|
210
|
+
"""Mint and verify audience-bound HMAC-SHA256 access tokens.
|
|
211
|
+
|
|
212
|
+
The local signing key never enters SQLite, HTTP bodies, logs, or renderer
|
|
213
|
+
state. Authorization still re-reads session, principal, membership and ACL
|
|
214
|
+
state from SQLite for every request.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, key: bytes) -> None:
|
|
218
|
+
if len(key) < 32:
|
|
219
|
+
raise ValueError("access-token signing key must contain at least 32 bytes")
|
|
220
|
+
self._key = bytes(key)
|
|
221
|
+
|
|
222
|
+
def mint(
|
|
223
|
+
self,
|
|
224
|
+
human_principal_id: str,
|
|
225
|
+
device_session_id: str,
|
|
226
|
+
*,
|
|
227
|
+
now: int | None = None,
|
|
228
|
+
ttl_seconds: int = ACCESS_TOKEN_TTL_SECONDS,
|
|
229
|
+
) -> MintedAccessToken:
|
|
230
|
+
issued_at = now_seconds() if now is None else int(now)
|
|
231
|
+
if not 1 <= int(ttl_seconds) <= ACCESS_TOKEN_TTL_SECONDS:
|
|
232
|
+
raise ValueError("access-token ttl is outside the supported bound")
|
|
233
|
+
if (
|
|
234
|
+
not 1 <= len(human_principal_id) <= 240
|
|
235
|
+
or not 1 <= len(device_session_id) <= 240
|
|
236
|
+
or not all(33 <= ord(char) <= 126 for char in human_principal_id)
|
|
237
|
+
or not all(33 <= ord(char) <= 126 for char in device_session_id)
|
|
238
|
+
):
|
|
239
|
+
raise ValueError("access-token subject or session is invalid")
|
|
240
|
+
expires_at = issued_at + int(ttl_seconds)
|
|
241
|
+
jti = secrets.token_urlsafe(18)
|
|
242
|
+
header = {"alg": "HS256", "typ": "ADTH-AT1"}
|
|
243
|
+
payload = {
|
|
244
|
+
"iss": TOKEN_ISSUER,
|
|
245
|
+
"aud": TOKEN_AUDIENCE,
|
|
246
|
+
"sub": human_principal_id,
|
|
247
|
+
"sid": device_session_id,
|
|
248
|
+
"jti": jti,
|
|
249
|
+
"iat": issued_at,
|
|
250
|
+
"nbf": issued_at,
|
|
251
|
+
"exp": expires_at,
|
|
252
|
+
}
|
|
253
|
+
encoded_header = _b64url_encode(canonical_json(header))
|
|
254
|
+
encoded_payload = _b64url_encode(canonical_json(payload))
|
|
255
|
+
signing_input = f"{encoded_header}.{encoded_payload}".encode("ascii")
|
|
256
|
+
signature = hmac.new(self._key, signing_input, hashlib.sha256).digest()
|
|
257
|
+
return MintedAccessToken(
|
|
258
|
+
f"{encoded_header}.{encoded_payload}.{_b64url_encode(signature)}",
|
|
259
|
+
jti,
|
|
260
|
+
expires_at,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
def verify(self, token: str, *, now: int | None = None) -> dict[str, Any]:
|
|
264
|
+
if not isinstance(token, str) or not 40 <= len(token) <= 8192:
|
|
265
|
+
raise TokenError("credential is invalid or unavailable")
|
|
266
|
+
parts = token.split(".")
|
|
267
|
+
if len(parts) != 3:
|
|
268
|
+
raise TokenError("credential is invalid or unavailable")
|
|
269
|
+
try:
|
|
270
|
+
signing_input = f"{parts[0]}.{parts[1]}".encode("ascii", "strict")
|
|
271
|
+
except UnicodeEncodeError as exc:
|
|
272
|
+
raise TokenError("credential is invalid or unavailable") from exc
|
|
273
|
+
supplied_signature = _b64url_decode(parts[2])
|
|
274
|
+
expected_signature = hmac.new(
|
|
275
|
+
self._key, signing_input, hashlib.sha256
|
|
276
|
+
).digest()
|
|
277
|
+
if not hmac.compare_digest(supplied_signature, expected_signature):
|
|
278
|
+
raise TokenError("credential is invalid or unavailable")
|
|
279
|
+
try:
|
|
280
|
+
header = json.loads(_b64url_decode(parts[0]))
|
|
281
|
+
payload = json.loads(_b64url_decode(parts[1]))
|
|
282
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
283
|
+
raise TokenError("credential is invalid or unavailable") from exc
|
|
284
|
+
if header != {"alg": "HS256", "typ": "ADTH-AT1"}:
|
|
285
|
+
raise TokenError("credential is invalid or unavailable")
|
|
286
|
+
if not isinstance(payload, dict) or set(payload) != {
|
|
287
|
+
"iss", "aud", "sub", "sid", "jti", "iat", "nbf", "exp"
|
|
288
|
+
}:
|
|
289
|
+
raise TokenError("credential is invalid or unavailable")
|
|
290
|
+
if payload["iss"] != TOKEN_ISSUER or payload["aud"] != TOKEN_AUDIENCE:
|
|
291
|
+
raise TokenError("credential is invalid or unavailable")
|
|
292
|
+
if not all(isinstance(payload[name], str) for name in ("sub", "sid", "jti")):
|
|
293
|
+
raise TokenError("credential is invalid or unavailable")
|
|
294
|
+
if (
|
|
295
|
+
not 1 <= len(payload["sub"]) <= 240
|
|
296
|
+
or not 1 <= len(payload["sid"]) <= 240
|
|
297
|
+
or not 1 <= len(payload["jti"]) <= 128
|
|
298
|
+
or any(
|
|
299
|
+
not all(33 <= ord(char) <= 126 for char in payload[name])
|
|
300
|
+
for name in ("sub", "sid", "jti")
|
|
301
|
+
)
|
|
302
|
+
):
|
|
303
|
+
raise TokenError("credential is invalid or unavailable")
|
|
304
|
+
if not all(
|
|
305
|
+
isinstance(payload[name], int) and not isinstance(payload[name], bool)
|
|
306
|
+
for name in ("iat", "nbf", "exp")
|
|
307
|
+
):
|
|
308
|
+
raise TokenError("credential is invalid or unavailable")
|
|
309
|
+
timestamp = now_seconds() if now is None else int(now)
|
|
310
|
+
if payload["nbf"] != payload["iat"] or payload["iat"] > timestamp + 5:
|
|
311
|
+
raise TokenError("credential is invalid or unavailable")
|
|
312
|
+
if (
|
|
313
|
+
payload["exp"] <= timestamp
|
|
314
|
+
or payload["exp"] <= payload["iat"]
|
|
315
|
+
or payload["exp"] - payload["iat"] > ACCESS_TOKEN_TTL_SECONDS
|
|
316
|
+
):
|
|
317
|
+
raise TokenError("credential is invalid or unavailable")
|
|
318
|
+
return payload
|