openshell-shared 0.1.2__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.
Files changed (62) hide show
  1. api/__init__.py +1 -0
  2. api/manager/__init__.py +1 -0
  3. api/manager/v1/__init__.py +78 -0
  4. api/manager/v1/authentication.py +278 -0
  5. api/manager/v1/client.py +111 -0
  6. api/manager/v1/domains.py +64 -0
  7. api/manager/v1/entities.py +97 -0
  8. api/manager/v1/exceptions.py +98 -0
  9. api/manager/v1/identity.py +44 -0
  10. api/manager/v1/models.py +342 -0
  11. api/manager/v1/passports.py +131 -0
  12. api/manager/v1/sessions.py +83 -0
  13. api/manager/v1/transport.py +253 -0
  14. api/manager/v1/tunnels.py +120 -0
  15. cryptography/__init__.py +0 -0
  16. cryptography/certificate.py +390 -0
  17. cryptography/encoding.py +0 -0
  18. cryptography/identity.py +124 -0
  19. cryptography/keys.py +463 -0
  20. cryptography/signatures.py +63 -0
  21. cryptography/utils.py +0 -0
  22. domain/__init__.py +0 -0
  23. domain/domain.py +80 -0
  24. domain/membership.py +21 -0
  25. domain/permissions.py +14 -0
  26. domain/policies.py +2 -0
  27. identity/__init__.py +0 -0
  28. identity/identification.py +64 -0
  29. identity/store.py +150 -0
  30. modules/__init__.py +0 -0
  31. modules/shell/__init__.py +0 -0
  32. modules/shell/client.py +361 -0
  33. modules/shell/models.py +61 -0
  34. modules/shell/protocol.py +249 -0
  35. modules/shell/server.py +511 -0
  36. modules/shell/session.py +339 -0
  37. modules/utils.py +212 -0
  38. openshell_shared-0.1.2.dist-info/METADATA +59 -0
  39. openshell_shared-0.1.2.dist-info/RECORD +62 -0
  40. openshell_shared-0.1.2.dist-info/WHEEL +5 -0
  41. openshell_shared-0.1.2.dist-info/top_level.txt +7 -0
  42. protocols/__init__.py +0 -0
  43. protocols/negotiation/challenge.py +127 -0
  44. protocols/negotiation/models.py +28 -0
  45. standards/__init__.py +0 -0
  46. standards/certificates/__init__.py +0 -0
  47. standards/certificates/status.py +12 -0
  48. standards/certificates/types.py +11 -0
  49. standards/entities/__init__.py +0 -0
  50. standards/entities/types.py +14 -0
  51. standards/events/__init__.py +0 -0
  52. standards/events/schemas/__init__.py +0 -0
  53. standards/events/schemas/entity_registered.py +13 -0
  54. standards/events/types.py +18 -0
  55. standards/passports/__init__.py +0 -0
  56. standards/passports/types.py +5 -0
  57. standards/permissions/__init__.py +0 -0
  58. standards/permissions/types.py +14 -0
  59. standards/roles/__init__.py +0 -0
  60. standards/roles/types.py +8 -0
  61. standards/transports/__init__.py +0 -0
  62. standards/transports/types.py +24 -0
@@ -0,0 +1,98 @@
1
+ # shared/api/manager/v1/exceptions.py
2
+ """
3
+ Jerarquía de excepciones de la SDK de OSAM.
4
+
5
+ Diseño:
6
+
7
+ OSAMError -> raíz de toda excepción producida por la SDK
8
+ ├── NetworkError -> fallos de transporte (timeout, DNS, conexión rechazada...)
9
+ ├── InvalidResponseError -> el servidor respondió pero el cuerpo no es JSON
10
+ │ válido o no contiene los campos esperados
11
+ └── APIError -> el servidor respondió con un código de error HTTP
12
+ ├── ValidationError (400 / 422 - payload inválido o incompleto)
13
+ ├── AuthenticationError (401 - token/credencial inválida o ausente)
14
+ ├── AuthorizationError (403 - la entidad no tiene permiso sobre el recurso)
15
+ ├── EntityNotFoundError (404 - el recurso solicitado no existe)
16
+ └── ServerError (5xx - error interno del servidor OSAM)
17
+
18
+ Ningún método de la SDK debe propagar excepciones de ``httpx`` ni errores
19
+ ambiguos (``KeyError``, ``ValueError`` genéricos, etc.) hacia el código que
20
+ consume la librería. Toda esa traducción ocurre en ``transport.py``.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ from typing import Any, Optional
26
+
27
+
28
+ class OSAMError(Exception):
29
+ """Excepción base de la SDK de OSAM. Todo error propio hereda de esta clase."""
30
+
31
+ def __init__(self, message: str, *, detail: Optional[Any] = None) -> None:
32
+ super().__init__(message)
33
+ self.message = message
34
+ self.detail = detail
35
+
36
+ def __str__(self) -> str: # pragma: no cover - cosmético
37
+ if self.detail is not None and str(self.detail) != self.message:
38
+ return f"{self.message} (detail={self.detail!r})"
39
+ return self.message
40
+
41
+
42
+ class NetworkError(OSAMError):
43
+ """
44
+ La petición no pudo completarse a nivel de transporte: timeout, host
45
+ inalcanzable, conexión rechazada, error de DNS, error de TLS, etc.
46
+
47
+ No hubo respuesta HTTP que interpretar; el problema es la red o el
48
+ servidor remoto está caído/inaccesible.
49
+ """
50
+
51
+
52
+ class InvalidResponseError(OSAMError):
53
+ """
54
+ El servidor respondió, pero el contenido no se pudo interpretar como se
55
+ esperaba: JSON malformado, o ausencia de campos que la SDK considera
56
+ obligatorios para construir el modelo de respuesta correspondiente.
57
+ """
58
+
59
+
60
+ class APIError(OSAMError):
61
+ """
62
+ Clase base para errores reportados explícitamente por la API mediante un
63
+ código de estado HTTP de error. Contiene siempre el código de estado y,
64
+ si estuvo disponible, el cuerpo de error devuelto por el servidor (que en
65
+ OSAM sigue el formato estándar de FastAPI: ``{"detail": ...}``).
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ message: str,
71
+ *,
72
+ status_code: int,
73
+ detail: Optional[Any] = None,
74
+ response_body: Optional[Any] = None,
75
+ ) -> None:
76
+ super().__init__(message, detail=detail)
77
+ self.status_code = status_code
78
+ self.response_body = response_body
79
+
80
+
81
+ class ValidationError(APIError):
82
+ """400 / 422 — la petición enviada por el cliente es inválida o incompleta."""
83
+
84
+
85
+ class AuthenticationError(APIError):
86
+ """401 — token de autenticación ausente, inválido o expirado."""
87
+
88
+
89
+ class AuthorizationError(APIError):
90
+ """403 — la entidad autenticada no tiene permisos sobre el recurso solicitado."""
91
+
92
+
93
+ class EntityNotFoundError(APIError):
94
+ """404 — el recurso solicitado (entidad, dominio, sesión...) no existe."""
95
+
96
+
97
+ class ServerError(APIError):
98
+ """5xx — error interno no controlado dentro del servidor OSAM."""
@@ -0,0 +1,44 @@
1
+ # shared/api/manager/v1/identity.py
2
+ """
3
+ Dominio: Identity.
4
+
5
+ Expone la identidad lógica, criptográfica y de tipo de la entidad que
6
+ responde en el otro extremo de la conexión HTTP (típicamente el Manager,
7
+ pero el mismo contrato aplica a cualquier entidad OSAM que exponga esta
8
+ misma API). Ninguno de estos endpoints requiere autenticación: son el
9
+ primer paso antes de iniciar el protocolo de challenge-response.
10
+
11
+ Endpoints cubiertos (todos bajo ``/api/v/1/identity``):
12
+ GET /logical
13
+ GET /cryptographic
14
+ GET /type
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from .models import CryptographicIdentity, EntityTypeInfo, LogicalIdentity
20
+ from .transport import HttpTransport
21
+
22
+ _PREFIX = "/api/v/1/identity"
23
+
24
+
25
+ class IdentityAPI:
26
+ """API especializada para consultar la identidad de una entidad OSAM."""
27
+
28
+ def __init__(self, transport: HttpTransport) -> None:
29
+ self._transport = transport
30
+
31
+ async def get_logical_identity(self) -> LogicalIdentity:
32
+ """GET /api/v/1/identity/logical -> identificador lógico (uid)."""
33
+ body = await self._transport.get(f"{_PREFIX}/logical")
34
+ return LogicalIdentity.from_dict(body)
35
+
36
+ async def get_cryptographic_identity(self) -> CryptographicIdentity:
37
+ """GET /api/v/1/identity/cryptographic -> identidad criptográfica."""
38
+ body = await self._transport.get(f"{_PREFIX}/cryptographic")
39
+ return CryptographicIdentity.from_dict(body)
40
+
41
+ async def get_entity_type(self) -> EntityTypeInfo:
42
+ """GET /api/v/1/identity/type -> tipo de entidad (AGENT/CONSOLE/...)."""
43
+ body = await self._transport.get(f"{_PREFIX}/type")
44
+ return EntityTypeInfo.from_dict(body)
@@ -0,0 +1,342 @@
1
+ # shared/api/manager/v1/models.py
2
+ """
3
+ Modelos tipados (dataclasses) en los que la SDK convierte las respuestas
4
+ JSON crudas de la API HTTP de OSAM.
5
+
6
+ Nota de transparencia sobre el origen de estos modelos
7
+ -------------------------------------------------------
8
+ Esta SDK se construyó a partir del código fuente de la capa
9
+ ``api/http/routes/*.py`` del Manager (no se tuvo acceso al paquete
10
+ ``core/`` donde viven las implementaciones reales de ``core.auth``,
11
+ ``core.tunnel``, ``core.session``, ``core.passports``, etc.). Por lo tanto:
12
+
13
+ * Los campos marcados sin comentario están **confirmados**: se ve
14
+ explícitamente en las rutas que el servidor los lee o los escribe
15
+ (ej. ``entity["entity_uid"]``, ``payload.get("challenge_id")``).
16
+ * Los campos marcados con ``# inferido`` son una suposición razonable
17
+ basada en la convención de nombres del propio proyecto (ver
18
+ jerarquía auth_token / session_token / tunnel_token / connection_uid),
19
+ pero no se observaron directamente en el código de rutas.
20
+
21
+ En ambos casos, cualquier clave adicional que el servidor incluya en la
22
+ respuesta y que no esté declarada explícitamente en el dataclass **no se
23
+ pierde**: queda disponible en el atributo ``.extra`` (y, en conjunto con los
24
+ campos conocidos, a través de ``.raw``). Así, si el equipo de ``core``
25
+ cambia o añade un campo, el código que use atributos explícitos sigue
26
+ funcionando, y el dato sigue siendo accesible sin tener que actualizar la
27
+ SDK de inmediato.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import dataclasses
33
+ from dataclasses import dataclass, field
34
+ from typing import Any, ClassVar, Dict, Mapping, Optional, Type, TypeVar
35
+
36
+ T = TypeVar("T", bound="_BaseModel")
37
+
38
+
39
+ @dataclass
40
+ class _BaseModel:
41
+ """
42
+ Clase base para todos los modelos de respuesta de la SDK.
43
+
44
+ Provee:
45
+ * ``from_dict``: construye una instancia a partir de un dict JSON,
46
+ asignando las claves conocidas a sus campos declarados y guardando
47
+ el resto en ``extra``.
48
+ * ``raw``: reconstruye el dict completo (campos conocidos + extra),
49
+ útil para depuración/logging sin exponer dicts crudos como tipo de
50
+ retorno habitual de la SDK.
51
+ * ``get``: acceso conveniente a un campo conocido o a ``extra`` sin
52
+ tener que recordar en cuál de los dos vive.
53
+ """
54
+
55
+ extra: Dict[str, Any] = field(default_factory=dict, repr=False)
56
+
57
+ # Nombre de las claves "id" alternativas que algunas respuestas usan
58
+ # indistintamente (uid / <recurso>_uid). Se usa en from_dict para no
59
+ # duplicar lógica de aliasing en cada subclase.
60
+ _id_aliases: ClassVar[Dict[str, str]] = {}
61
+
62
+ @classmethod
63
+ def from_dict(cls: Type[T], data: Mapping[str, Any]) -> T:
64
+ if not isinstance(data, Mapping):
65
+ data = {}
66
+
67
+ known_fields = {
68
+ f.name for f in dataclasses.fields(cls) if f.name != "extra"
69
+ }
70
+
71
+ working = dict(data)
72
+
73
+ # Resuelve alias de identificadores (ej. "uid" -> "passport_uid")
74
+ # sin pisar un valor que ya venga con el nombre canónico.
75
+ for canonical, alias in cls._id_aliases.items():
76
+ if canonical in known_fields and canonical not in working:
77
+ if alias in working:
78
+ working[canonical] = working[alias]
79
+
80
+ kwargs = {k: v for k, v in working.items() if k in known_fields}
81
+ extra = {k: v for k, v in working.items() if k not in known_fields}
82
+
83
+ return cls(**kwargs, extra=extra)
84
+
85
+ @property
86
+ def raw(self) -> Dict[str, Any]:
87
+ """Devuelve el payload completo (campos tipados + extra) como dict."""
88
+ result = {
89
+ f.name: getattr(self, f.name)
90
+ for f in dataclasses.fields(self)
91
+ if f.name != "extra"
92
+ }
93
+ result.update(self.extra)
94
+ return result
95
+
96
+ def get(self, key: str, default: Any = None) -> Any:
97
+ """Acceso conveniente a un campo conocido o a un campo en `extra`."""
98
+ if hasattr(self, key) and key != "extra":
99
+ return getattr(self, key)
100
+ return self.extra.get(key, default)
101
+
102
+
103
+ # =============================================================================
104
+ # IDENTITY
105
+ # =============================================================================
106
+
107
+
108
+ @dataclass
109
+ class LogicalIdentity(_BaseModel):
110
+ """Respuesta de GET /api/v/1/identity/logical."""
111
+
112
+ uid: Optional[str] = None
113
+
114
+
115
+ @dataclass
116
+ class CryptographicIdentity(_BaseModel):
117
+ """
118
+ Respuesta de GET /api/v/1/identity/cryptographic.
119
+
120
+ El endpoint devuelve ``core.get_public_identity()`` sin envoltorio, cuyo
121
+ esquema exacto vive en ``core`` (no incluido en este paquete). Se modelan
122
+ los campos que, de acuerdo a la convención del proyecto (identidad
123
+ criptográfica Ed25519 por entidad), son casi seguros.
124
+ """
125
+
126
+ public_key: Optional[str] = None # inferido
127
+ uid: Optional[str] = None # inferido
128
+ algorithm: Optional[str] = None # inferido
129
+
130
+
131
+ @dataclass
132
+ class EntityTypeInfo(_BaseModel):
133
+ """Respuesta de GET /api/v/1/identity/type."""
134
+
135
+ type: Optional[str] = None
136
+
137
+
138
+ # =============================================================================
139
+ # AUTHENTICATION
140
+ # =============================================================================
141
+
142
+
143
+ @dataclass
144
+ class ClientChallenge(_BaseModel):
145
+ """
146
+ Respuesta de POST /api/v/1/auth/client/challenge.
147
+
148
+ Confirmado: la respuesta es ``{"ok": True, **result}`` donde ``result``
149
+ proviene de ``core.auth.create_client_authentication_challenge(...)``.
150
+ El campo ``challenge_id`` es el único que se puede inferir con certeza
151
+ razonable, ya que es el identificador que luego se debe reenviar a
152
+ ``verify_client_challenge``. Cualquier otro dato del reto (nonce, datos a
153
+ firmar, expiración) quedará disponible en ``.extra``.
154
+ """
155
+
156
+ ok: Optional[bool] = None
157
+ challenge: Optional[dict] = None
158
+
159
+
160
+ @dataclass
161
+ class ClientChallengeVerification(_BaseModel):
162
+ """
163
+ Respuesta de POST /api/v/1/auth/verify.
164
+
165
+ Confirmado: ``{"ok": True, **result}`` con ``result`` proveniente de
166
+ ``core.auth.verify_client_authentication(...)``. Tras una verificación
167
+ exitosa, lo esperable es que el servidor emita un ``auth_token`` para la
168
+ entidad (ver jerarquía de tokens del proyecto); se modela como campo
169
+ inferido.
170
+ """
171
+
172
+ ok: Optional[bool] = None
173
+ auth_token: Optional[str] = None # inferido
174
+ uid: Optional[str] = None # inferido
175
+
176
+
177
+ @dataclass
178
+ class ServerChallengeResponse(_BaseModel):
179
+ """
180
+ Respuesta de POST /api/v/1/auth/server/challenge.
181
+
182
+ Confirmado: ``{"ok": True, **result}`` con ``result`` proveniente de
183
+ ``core.auth.create_server_authentication_response(challenge_id)``.
184
+ """
185
+
186
+ ok: Optional[bool] = None
187
+ response: Optional[Any] = None # inferido
188
+
189
+
190
+ # =============================================================================
191
+ # DOMAINS
192
+ # =============================================================================
193
+
194
+
195
+ @dataclass
196
+ class Domain(_BaseModel):
197
+ """
198
+ Elemento de la lista devuelta por POST /api/v/1/domains/query.
199
+
200
+ El esquema exacto de cada dominio no se observa en las rutas (la lista se
201
+ retorna tal cual la entrega ``core.domain_manager.query_entity_domains``).
202
+ Se modelan los campos más probables según la convención del proyecto.
203
+ """
204
+
205
+ _id_aliases: ClassVar[Dict[str, str]] = {"uid": "domain_uid"}
206
+
207
+ uid: Optional[str] = None # inferido (o domain_uid, ver alias)
208
+ name: Optional[str] = None # inferido
209
+ role: Optional[str] = None # inferido (rol de membresía del solicitante)
210
+
211
+
212
+ # =============================================================================
213
+ # ENTITIES
214
+ # =============================================================================
215
+
216
+
217
+ @dataclass
218
+ class Entity(_BaseModel):
219
+ """
220
+ Elemento de la lista ``entities`` devuelta por
221
+ POST /api/v/1/entities/agent/query.
222
+
223
+ Confirmado en el código de la ruta: cada entidad expone ``entity_uid``,
224
+ y la ruta le añade explícitamente la clave ``status`` (resultado de
225
+ ``core.services.tunnel_service.get_entity_status(...)``).
226
+ """
227
+
228
+ entity_uid: Optional[str] = None
229
+ entity_type: Optional[str] = None # inferido
230
+ status: Optional[Any] = None
231
+
232
+
233
+ # =============================================================================
234
+ # PASSPORTS
235
+ # =============================================================================
236
+
237
+
238
+ @dataclass
239
+ class Passport(_BaseModel):
240
+ """
241
+ Objeto ``passport`` devuelto dentro de
242
+ POST /api/v/1/passports/open/create -> {"created": True, "passport": ...}.
243
+
244
+ Confirmado en otras rutas que consumen un passport ya creado: expone al
245
+ menos ``entity_type`` (usado en ``passports.py`` para verificar que solo
246
+ una CONSOLE pueda emitir pasaportes OPEN). El resto de campos son los
247
+ parámetros de creación, que el servidor normalmente devuelve reflejados
248
+ en el objeto creado, más un identificador propio.
249
+ """
250
+
251
+ _id_aliases: ClassVar[Dict[str, str]] = {"uid": "passport_uid"}
252
+
253
+ uid: Optional[str] = None # inferido (o passport_uid, ver alias)
254
+ domain_uid: Optional[str] = None # inferido (eco del parámetro de entrada)
255
+ passport_type: Optional[str] = None # inferido ("OPEN" / "CLOSED")
256
+ entity_type: Optional[str] = None
257
+ role: Optional[str] = None # inferido
258
+ expiration_hours: Optional[int] = None # inferido
259
+ usage_limit: Optional[int] = None # inferido
260
+ security_code: Optional[str] = None # inferido (código que luego se usa
261
+ # para "integrar" una entidad vía /integration/open o /integration/closed)
262
+
263
+
264
+ @dataclass
265
+ class IntegrationResult(_BaseModel):
266
+ """
267
+ Resultado de POST /api/v/1/integration/open o
268
+ POST /api/v/1/integration/closed.
269
+
270
+ El cuerpo de respuesta es completamente opaco desde la capa de rutas
271
+ (se retorna tal cual lo entrega ``core.integration.integrate_open/closed``),
272
+ por lo que no se declaran campos propios más allá de los heredados de
273
+ ``_BaseModel``. Todo el contenido de la respuesta queda en ``.extra`` /
274
+ accesible vía ``.raw``.
275
+ """
276
+
277
+
278
+ # =============================================================================
279
+ # SESSIONS
280
+ # =============================================================================
281
+
282
+
283
+ @dataclass
284
+ class SessionInfo(_BaseModel):
285
+ """
286
+ Respuesta de POST /api/v/1/sessions/request, y forma esperada de cada
287
+ elemento dentro de la lista ``sessions`` de
288
+ POST /api/v/1/sessions/query.
289
+
290
+ Confirmado: el envoltorio ``{"ok": True, **result}`` para `/request`.
291
+ El campo ``session_token`` es el identificador central definido por el
292
+ propio proyecto para este recurso.
293
+ """
294
+
295
+ ok: Optional[bool] = None
296
+ session_token: Optional[str] = None # inferido
297
+ tunnel_token: Optional[str] = None # inferido (eco del parámetro de entrada)
298
+ destination_uid: Optional[str] = None # inferido (eco del parámetro de entrada)
299
+
300
+
301
+ @dataclass
302
+ class SessionDeletionResult(_BaseModel):
303
+ """Respuesta de POST /api/v/1/sessions/delete. Confirmado en su totalidad."""
304
+
305
+ ok: Optional[bool] = None
306
+ revoked: Optional[bool] = None
307
+ session_token: Optional[str] = None
308
+
309
+
310
+ # =============================================================================
311
+ # TUNNELS
312
+ # =============================================================================
313
+
314
+
315
+ @dataclass
316
+ class TunnelInfo(_BaseModel):
317
+ """
318
+ Respuesta de POST /api/v/1/services/tunnels/request.
319
+
320
+ Confirmado: el envoltorio es
321
+ ``{"ok": True, "tunnel_port": <int>, **result}`` donde ``tunnel_port`` se
322
+ lee literalmente de ``core.services.tunnel_service.ws._port`` en la ruta.
323
+ El resto de campos del ``result`` (lo que retorna
324
+ ``core.tunnel.create_tunnel(auth_token)``) son inferidos según la
325
+ convención de tokens del proyecto.
326
+ """
327
+
328
+ ok: Optional[bool] = None
329
+ tunnel_port: Optional[int] = None
330
+ tunnel_token: Optional[str] = None # inferido
331
+ tunnel_uid: Optional[str] = None # inferido
332
+
333
+
334
+ @dataclass
335
+ class TunnelOperationResult(_BaseModel):
336
+ """
337
+ Resultado de las operaciones legacy/deprecated
338
+ POST /api/v/1/services/tunnels/open y .../link.
339
+
340
+ El cuerpo de respuesta es opaco desde la capa de rutas (se retorna tal
341
+ cual lo entrega ``tunnel_service.open_tunnel`` / ``.link_tunnels``).
342
+ """
@@ -0,0 +1,131 @@
1
+ # shared/api/manager/v1/passports.py
2
+ """
3
+ Dominio: Passports.
4
+
5
+ Encapsula el ciclo de vida de los "pasaportes" OSAM (OPEN / CLOSED), que
6
+ son el mecanismo por el cual una entidad ya integrada (típicamente una
7
+ CONSOLE) autoriza el ingreso de nuevas entidades a un dominio.
8
+
9
+ Cobertura real de la API del servidor en esta versión:
10
+
11
+ * Creación de pasaportes **OPEN**: soportada
12
+ (``POST /api/v/1/passports/open/create``).
13
+ * Creación de pasaportes **CLOSED**: NO existe endpoint todavía. Se deja
14
+ ``create_closed`` como punto de extensión explícito.
15
+ * "Validación" de un pasaporte: en la API actual, validar un pasaporte y
16
+ consumirlo ocurren en el mismo paso, a través de los endpoints de
17
+ integración (``integrate_open`` / ``integrate_closed``), que reciben el
18
+ ``security_code`` emitido al crear el pasaporte. No existe un endpoint
19
+ de validación "de solo lectura" independiente.
20
+ * "Consulta" de pasaportes ya creados: NO existe endpoint todavía. Se deja
21
+ ``get`` como punto de extensión explícito.
22
+
23
+ Nota importante sobre autenticación de estos endpoints: a diferencia del
24
+ resto de la API (que recibe ``auth_token`` en el cuerpo JSON), los
25
+ endpoints de integración lo esperan como cabecera ``Authorization: Bearer``.
26
+ La SDK respeta esa asimetría: ``HttpTransport`` soporta ambos esquemas y
27
+ cada método de este módulo usa el que corresponde según la ruta real.
28
+
29
+ Endpoints cubiertos:
30
+ POST /api/v/1/passports/open/create (auth_token en el body)
31
+ POST /api/v/1/integration/open (auth_token como Bearer token)
32
+ POST /api/v/1/integration/closed (auth_token como Bearer token)
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ from typing import Optional
38
+
39
+ from .models import IntegrationResult, Passport
40
+ from .transport import HttpTransport
41
+
42
+ _PASSPORTS_PREFIX = "/api/v/1/passports"
43
+ _INTEGRATION_PREFIX = "/api/v/1/integration"
44
+
45
+
46
+ class PassportsAPI:
47
+ """API especializada para la gestión de pasaportes OSAM."""
48
+
49
+ def __init__(self, transport: HttpTransport) -> None:
50
+ self._transport = transport
51
+
52
+ async def create_open(
53
+ self,
54
+ auth_token: str,
55
+ domain_uid: str,
56
+ entity_role: str,
57
+ expiration_hours: int,
58
+ usage_limit: int,
59
+ ) -> Passport:
60
+ """
61
+ POST /api/v/1/passports/open/create
62
+
63
+ Crea un pasaporte OPEN para ``domain_uid``, válido para el rol
64
+ ``entity_role``, con expiración y límite de usos indicados.
65
+ Solo entidades CONSOLE integradas pueden invocar esta operación
66
+ (el servidor lo valida; ver ``passports.py`` en las rutas).
67
+ """
68
+ body = await self._transport.post(
69
+ f"{_PASSPORTS_PREFIX}/open/create",
70
+ json={
71
+ "auth_token": auth_token,
72
+ "domain_uid": domain_uid,
73
+ "entity_role": entity_role,
74
+ "expiration_hours": expiration_hours,
75
+ "usage_limit": usage_limit,
76
+ },
77
+ )
78
+ return Passport.from_dict(body.get("passport", {}))
79
+
80
+ async def create_closed(self, *args: object, **kwargs: object) -> Passport:
81
+ """
82
+ Punto de extensión: el servidor de OSAM no expone todavía un
83
+ endpoint de creación de pasaportes CLOSED.
84
+ """
85
+ raise NotImplementedError(
86
+ "El servidor de OSAM no expone todavía un endpoint de creación "
87
+ "de pasaportes CLOSED."
88
+ )
89
+
90
+ async def integrate_open(
91
+ self, auth_token: str, security_code: str, entity_type: str
92
+ ) -> IntegrationResult:
93
+ """
94
+ POST /api/v/1/integration/open
95
+
96
+ Redime un ``security_code`` de un pasaporte OPEN, declarando el
97
+ ``entity_type`` con el que la entidad solicitante se integra.
98
+ ``auth_token`` se envía como cabecera ``Authorization: Bearer``.
99
+ """
100
+ body = await self._transport.post(
101
+ f"{_INTEGRATION_PREFIX}/open",
102
+ json={"security_code": security_code, "entity_type": entity_type},
103
+ bearer_token=auth_token,
104
+ )
105
+ return body
106
+
107
+ async def integrate_closed(
108
+ self, auth_token: str, security_code: str
109
+ ) -> IntegrationResult:
110
+ """
111
+ POST /api/v/1/integration/closed
112
+
113
+ Redime un ``security_code`` de un pasaporte CLOSED.
114
+ ``auth_token`` se envía como cabecera ``Authorization: Bearer``.
115
+ """
116
+ body = await self._transport.post(
117
+ f"{_INTEGRATION_PREFIX}/closed",
118
+ json={"security_code": security_code},
119
+ bearer_token=auth_token,
120
+ )
121
+ return body
122
+
123
+ async def get(self, auth_token: str, passport_uid: str) -> Optional[Passport]:
124
+ """
125
+ Punto de extensión: el servidor de OSAM no expone todavía un
126
+ endpoint de consulta de un pasaporte por identificador.
127
+ """
128
+ raise NotImplementedError(
129
+ "El servidor de OSAM no expone todavía un endpoint de consulta "
130
+ "de pasaportes existentes."
131
+ )