agent-enrollment-protocol 0.1.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.
Files changed (35) hide show
  1. agent_enrollment_protocol/__init__.py +7 -0
  2. agent_enrollment_protocol/adapters/__init__.py +29 -0
  3. agent_enrollment_protocol/adapters/asgi.py +445 -0
  4. agent_enrollment_protocol/agent/__init__.py +96 -0
  5. agent_enrollment_protocol/agent/client.py +934 -0
  6. agent_enrollment_protocol/agent/platform_provider.py +659 -0
  7. agent_enrollment_protocol/agent/stores.py +101 -0
  8. agent_enrollment_protocol/agent/transport.py +51 -0
  9. agent_enrollment_protocol/agent/types.py +218 -0
  10. agent_enrollment_protocol/core/__init__.py +250 -0
  11. agent_enrollment_protocol/core/assertions.py +141 -0
  12. agent_enrollment_protocol/core/claims.py +38 -0
  13. agent_enrollment_protocol/core/constants.py +46 -0
  14. agent_enrollment_protocol/core/did_web.py +77 -0
  15. agent_enrollment_protocol/core/errors.py +48 -0
  16. agent_enrollment_protocol/core/http.py +102 -0
  17. agent_enrollment_protocol/core/inspect.py +70 -0
  18. agent_enrollment_protocol/core/models.py +929 -0
  19. agent_enrollment_protocol/core/openapi.py +108 -0
  20. agent_enrollment_protocol/core/validation.py +77 -0
  21. agent_enrollment_protocol/platform/__init__.py +79 -0
  22. agent_enrollment_protocol/platform/document.py +158 -0
  23. agent_enrollment_protocol/platform/platform.py +674 -0
  24. agent_enrollment_protocol/platform/stores.py +219 -0
  25. agent_enrollment_protocol/platform/types.py +282 -0
  26. agent_enrollment_protocol/py.typed +1 -0
  27. agent_enrollment_protocol/service/__init__.py +95 -0
  28. agent_enrollment_protocol/service/credentials.py +355 -0
  29. agent_enrollment_protocol/service/service.py +871 -0
  30. agent_enrollment_protocol/service/stores.py +135 -0
  31. agent_enrollment_protocol/service/types.py +337 -0
  32. agent_enrollment_protocol-0.1.0.dist-info/METADATA +257 -0
  33. agent_enrollment_protocol-0.1.0.dist-info/RECORD +35 -0
  34. agent_enrollment_protocol-0.1.0.dist-info/WHEEL +4 -0
  35. agent_enrollment_protocol-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,7 @@
1
+ """Agent Enrollment Protocol SDK."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("agent-enrollment-protocol")
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,29 @@
1
+ """Optional framework and server integration surfaces."""
2
+
3
+ from .asgi import (
4
+ AEP_PRINCIPAL_SCOPE_KEY,
5
+ DEFAULT_INSPECT_CACHE_CONTROL,
6
+ DEFAULT_MAXIMUM_REQUEST_BODY_BYTES,
7
+ AepAsgiApplication,
8
+ AepAuthenticationMiddleware,
9
+ AsgiApplication,
10
+ AsgiMessage,
11
+ AsgiReceive,
12
+ AsgiScope,
13
+ AsgiSend,
14
+ principal_from_scope,
15
+ )
16
+
17
+ __all__ = [
18
+ "AEP_PRINCIPAL_SCOPE_KEY",
19
+ "DEFAULT_INSPECT_CACHE_CONTROL",
20
+ "DEFAULT_MAXIMUM_REQUEST_BODY_BYTES",
21
+ "AepAsgiApplication",
22
+ "AepAuthenticationMiddleware",
23
+ "AsgiApplication",
24
+ "AsgiMessage",
25
+ "AsgiReceive",
26
+ "AsgiScope",
27
+ "AsgiSend",
28
+ "principal_from_scope",
29
+ ]
@@ -0,0 +1,445 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import ipaddress
5
+ import json
6
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, MutableMapping, Sequence
7
+ from typing import Any, Protocol
8
+ from urllib.parse import quote, quote_from_bytes, urlsplit, urlunsplit
9
+
10
+ from pydantic import BaseModel
11
+
12
+ from agent_enrollment_protocol.core import (
13
+ AEP_MEDIA_TYPE,
14
+ AEP_PROBLEM_MEDIA_TYPE,
15
+ AEP_WELL_KNOWN_PATH,
16
+ AuthorizationCarrier,
17
+ AuthorizationScheme,
18
+ Command,
19
+ command_path_from_inspect,
20
+ media_type_essence,
21
+ parse_authorization,
22
+ )
23
+ from agent_enrollment_protocol.core.errors import AepAuthorizationError
24
+ from agent_enrollment_protocol.service import (
25
+ AuthenticatedPrincipal,
26
+ CommandOptions,
27
+ ProtectedResourceRequest,
28
+ Service,
29
+ ServiceResult,
30
+ )
31
+
32
+ AsgiScope = MutableMapping[str, Any]
33
+ AsgiMessage = MutableMapping[str, Any]
34
+ AsgiReceive = Callable[[], Awaitable[AsgiMessage]]
35
+ AsgiSend = Callable[[AsgiMessage], Awaitable[None]]
36
+
37
+ DEFAULT_INSPECT_CACHE_CONTROL = "public, max-age=300"
38
+ DEFAULT_MAXIMUM_REQUEST_BODY_BYTES = 1 << 20
39
+ AEP_PRINCIPAL_SCOPE_KEY = "aep.principal"
40
+ _HTTP_TOKEN_CHARACTERS = frozenset(
41
+ "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
42
+ )
43
+
44
+
45
+ class _RequestDisconnected(Exception):
46
+ pass
47
+
48
+
49
+ class AsgiApplication(Protocol):
50
+ async def __call__(self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend) -> None: ...
51
+
52
+
53
+ class AepAsgiApplication:
54
+ def __init__(
55
+ self,
56
+ service: Service,
57
+ application: AsgiApplication | None = None,
58
+ *,
59
+ inspect_cache_control: str = DEFAULT_INSPECT_CACHE_CONTROL,
60
+ maximum_request_body_bytes: int = DEFAULT_MAXIMUM_REQUEST_BODY_BYTES,
61
+ ) -> None:
62
+ if not isinstance(service, Service):
63
+ raise TypeError("AEP ASGI application requires a Service")
64
+ if (
65
+ not isinstance(maximum_request_body_bytes, int)
66
+ or isinstance(maximum_request_body_bytes, bool)
67
+ or maximum_request_body_bytes < 1
68
+ ):
69
+ raise ValueError("AEP ASGI request body limit must be positive")
70
+ try:
71
+ encoded_cache_control = inspect_cache_control.encode("latin-1")
72
+ except (AttributeError, UnicodeEncodeError) as error:
73
+ raise ValueError(
74
+ "AEP Inspect Cache-Control must be a valid HTTP field value"
75
+ ) from error
76
+ if (
77
+ not inspect_cache_control.strip()
78
+ or b"\r" in encoded_cache_control
79
+ or b"\n" in encoded_cache_control
80
+ ):
81
+ raise ValueError("AEP Inspect Cache-Control must be a valid HTTP field value")
82
+
83
+ document = service.inspect_document
84
+ inspect_body = _json_bytes(document.to_wire())
85
+ self._application = application if application is not None else _default_application
86
+ self._command_paths = {
87
+ command_path_from_inspect(document, command): command
88
+ for command in Command
89
+ if command is not Command.INSPECT and command.value in document.commands.supported
90
+ }
91
+ self._inspect_body = inspect_body
92
+ self._inspect_cache_control = inspect_cache_control
93
+ self._inspect_etag = f'"sha256:{hashlib.sha256(inspect_body).hexdigest()}"'
94
+ self._maximum_request_body_bytes = maximum_request_body_bytes
95
+ self._service = service
96
+
97
+ async def __call__(self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend) -> None:
98
+ if scope.get("type") != "http":
99
+ await self._application(scope, receive, send)
100
+ return
101
+
102
+ path = scope.get("path")
103
+ if path == AEP_WELL_KNOWN_PATH:
104
+ await self._serve_inspect(scope, send)
105
+ return
106
+
107
+ command = self._command_paths.get(path) if isinstance(path, str) else None
108
+ if command is None:
109
+ await self._application(scope, receive, send)
110
+ return
111
+ await self._serve_command(scope, receive, send, command)
112
+
113
+ async def _serve_inspect(self, scope: AsgiScope, send: AsgiSend) -> None:
114
+ if scope.get("method") != "GET":
115
+ await _send_problem(
116
+ send,
117
+ 400,
118
+ "Invalid request",
119
+ code="invalid_request",
120
+ headers={"Allow": "GET"},
121
+ )
122
+ return
123
+
124
+ headers = _request_headers(scope)
125
+ if _etag_matches(headers.get("if-none-match"), self._inspect_etag):
126
+ await _send_response(
127
+ send,
128
+ 304,
129
+ b"",
130
+ {
131
+ "Cache-Control": self._inspect_cache_control,
132
+ "ETag": self._inspect_etag,
133
+ },
134
+ )
135
+ return
136
+ await _send_response(
137
+ send,
138
+ 200,
139
+ self._inspect_body,
140
+ {
141
+ "Cache-Control": self._inspect_cache_control,
142
+ "Content-Type": AEP_MEDIA_TYPE,
143
+ "ETag": self._inspect_etag,
144
+ },
145
+ )
146
+
147
+ async def _serve_command(
148
+ self,
149
+ scope: AsgiScope,
150
+ receive: AsgiReceive,
151
+ send: AsgiSend,
152
+ command: Command,
153
+ ) -> None:
154
+ expected_method = "GET" if command is Command.STATUS else "POST"
155
+ if scope.get("method") != expected_method:
156
+ await _send_problem(
157
+ send,
158
+ 400,
159
+ "Invalid request",
160
+ code="invalid_request",
161
+ headers={"Allow": expected_method},
162
+ )
163
+ return
164
+
165
+ headers = _request_headers(scope)
166
+ assertion = _command_assertion(headers.get("authorization"))
167
+ if command is Command.STATUS:
168
+ status_result = await self._service.status(CommandOptions(client_assertion=assertion))
169
+ await _send_service_result(send, status_result)
170
+ return
171
+
172
+ if not _single_media_type(headers.get("content-type"), AEP_MEDIA_TYPE):
173
+ await _send_problem(send, 400, "Invalid request", code="invalid_request")
174
+ return
175
+ try:
176
+ body = await _read_body(receive, self._maximum_request_body_bytes)
177
+ except _RequestDisconnected:
178
+ return
179
+ if body is None:
180
+ await _send_problem(send, 400, "Invalid request", code="invalid_request")
181
+ return
182
+ options = CommandOptions(
183
+ client_assertion=assertion,
184
+ idempotency_key=_single_header(headers.get("idempotency-key")) or "",
185
+ )
186
+ result: ServiceResult[Any]
187
+ if command is Command.ENROLL:
188
+ result = await self._service.enroll(body, options)
189
+ elif command is Command.GRANT:
190
+ result = await self._service.grant(body, options)
191
+ else:
192
+ result = await self._service.revoke(body, options)
193
+ await _send_service_result(send, result)
194
+
195
+
196
+ class AepAuthenticationMiddleware:
197
+ def __init__(
198
+ self,
199
+ application: AsgiApplication,
200
+ service: Service,
201
+ *,
202
+ resource_origin: str,
203
+ allow_insecure_loopback: bool = False,
204
+ ) -> None:
205
+ if not callable(application):
206
+ raise TypeError("AEP authentication middleware requires an ASGI application")
207
+ if not isinstance(service, Service):
208
+ raise TypeError("AEP authentication middleware requires a Service")
209
+ self._application = application
210
+ self._resource_origin = _resource_origin(resource_origin, allow_insecure_loopback)
211
+ self._service = service
212
+
213
+ async def __call__(self, scope: AsgiScope, receive: AsgiReceive, send: AsgiSend) -> None:
214
+ if scope.get("type") != "http":
215
+ await self._application(scope, receive, send)
216
+ return
217
+
218
+ result = await self._service.authenticate_protected_resource(
219
+ ProtectedResourceRequest(
220
+ headers=_request_headers(scope),
221
+ method=_scope_text(scope, "method"),
222
+ url=_resource_url(self._resource_origin, scope),
223
+ )
224
+ )
225
+ if not result.authenticated:
226
+ if result.response is None:
227
+ raise RuntimeError("AEP Service returned an incomplete authentication result")
228
+ await _send_service_result(send, result.response)
229
+ return
230
+ if result.principal is None:
231
+ raise RuntimeError("AEP Service returned an authenticated result without a principal")
232
+ authenticated_scope = dict(scope)
233
+ authenticated_scope[AEP_PRINCIPAL_SCOPE_KEY] = result.principal
234
+ await self._application(authenticated_scope, receive, send)
235
+
236
+
237
+ def principal_from_scope(scope: Mapping[str, Any]) -> AuthenticatedPrincipal | None:
238
+ principal = scope.get(AEP_PRINCIPAL_SCOPE_KEY)
239
+ return principal if isinstance(principal, AuthenticatedPrincipal) else None
240
+
241
+
242
+ async def _default_application(scope: AsgiScope, receive: AsgiReceive, send: AsgiSend) -> None:
243
+ scope_type = scope.get("type")
244
+ if scope_type == "http":
245
+ await _send_problem(send, 404, "Not Found")
246
+ return
247
+ if scope_type != "lifespan":
248
+ raise RuntimeError("AEP ASGI application received an unsupported scope")
249
+ while True:
250
+ message_type = (await receive()).get("type")
251
+ if message_type == "lifespan.startup":
252
+ await send({"type": "lifespan.startup.complete"})
253
+ elif message_type == "lifespan.shutdown":
254
+ await send({"type": "lifespan.shutdown.complete"})
255
+ return
256
+ else:
257
+ raise RuntimeError("AEP ASGI application received an invalid lifespan message")
258
+
259
+
260
+ async def _read_body(receive: AsgiReceive, maximum: int) -> bytes | None:
261
+ body = bytearray()
262
+ while True:
263
+ message = await receive()
264
+ message_type = message.get("type")
265
+ if message_type == "http.disconnect":
266
+ raise _RequestDisconnected
267
+ if message_type != "http.request":
268
+ raise RuntimeError("AEP ASGI application received an invalid request message")
269
+ chunk = message.get("body", b"")
270
+ if not isinstance(chunk, bytes):
271
+ raise RuntimeError("AEP ASGI request body chunks must be bytes")
272
+ if len(body) + len(chunk) > maximum:
273
+ return None
274
+ body.extend(chunk)
275
+ if not message.get("more_body", False):
276
+ return bytes(body)
277
+
278
+
279
+ async def _send_service_result(send: AsgiSend, result: ServiceResult[Any]) -> None:
280
+ value: object = result.problem if result.problem is not None else result.body
281
+ if isinstance(value, BaseModel):
282
+ value = value.model_dump(by_alias=True, exclude_none=True, mode="json")
283
+ body = _json_bytes(value)
284
+ headers = dict(result.headers)
285
+ headers["Cache-Control"] = "no-store"
286
+ headers["Content-Type"] = result.content_type
287
+ await _send_response(send, result.status, body, headers)
288
+
289
+
290
+ async def _send_problem(
291
+ send: AsgiSend,
292
+ status: int,
293
+ title: str,
294
+ *,
295
+ code: str | None = None,
296
+ headers: Mapping[str, str] | None = None,
297
+ ) -> None:
298
+ response_headers = dict(headers or {})
299
+ response_headers["Cache-Control"] = "no-store"
300
+ response_headers["Content-Type"] = AEP_PROBLEM_MEDIA_TYPE
301
+ problem = {
302
+ "status": status,
303
+ "title": title,
304
+ "type": f"urn:aep:error:{code}" if code is not None else "about:blank",
305
+ }
306
+ if code is not None:
307
+ problem["code"] = code
308
+ await _send_response(send, status, _json_bytes(problem), response_headers)
309
+
310
+
311
+ async def _send_response(
312
+ send: AsgiSend,
313
+ status: int,
314
+ body: bytes,
315
+ headers: Mapping[str, str],
316
+ ) -> None:
317
+ values = {
318
+ name.lower(): value for name, value in headers.items() if name.lower() != "content-length"
319
+ }
320
+ if status != 304:
321
+ values["content-length"] = str(len(body))
322
+ encoded: list[tuple[bytes, bytes]] = []
323
+ for name, value in values.items():
324
+ if (
325
+ not name
326
+ or any(character not in _HTTP_TOKEN_CHARACTERS for character in name)
327
+ or "\r" in value
328
+ or "\n" in value
329
+ or any(ord(character) > 255 for character in value)
330
+ ):
331
+ raise ValueError("AEP ASGI response contains an invalid HTTP field")
332
+ encoded.append((name.encode("ascii"), value.encode("latin-1")))
333
+ await send({"type": "http.response.start", "status": status, "headers": encoded})
334
+ await send({"type": "http.response.body", "body": body})
335
+
336
+
337
+ def _request_headers(scope: Mapping[str, Any]) -> Mapping[str, str | Sequence[str]]:
338
+ grouped: dict[str, list[str]] = {}
339
+ raw_headers = scope.get("headers", ())
340
+ if not isinstance(raw_headers, Iterable):
341
+ raise RuntimeError("AEP ASGI request headers must be iterable")
342
+ for item in raw_headers:
343
+ if not isinstance(item, Iterable):
344
+ raise RuntimeError("AEP ASGI request header entries must be pairs")
345
+ try:
346
+ raw_name, raw_value = item
347
+ except (TypeError, ValueError) as error:
348
+ raise RuntimeError("AEP ASGI request header entries must be pairs") from error
349
+ if not isinstance(raw_name, bytes) or not isinstance(raw_value, bytes):
350
+ raise RuntimeError("AEP ASGI request headers must contain bytes")
351
+ name = raw_name.decode("latin-1").lower()
352
+ grouped.setdefault(name, []).append(raw_value.decode("latin-1"))
353
+ return {
354
+ name: values[0] if len(values) == 1 else tuple(values) for name, values in grouped.items()
355
+ }
356
+
357
+
358
+ def _single_header(value: str | Sequence[str] | None) -> str | None:
359
+ return value if isinstance(value, str) else None
360
+
361
+
362
+ def _single_media_type(value: str | Sequence[str] | None, expected: str) -> bool:
363
+ return isinstance(value, str) and media_type_essence(value) == expected
364
+
365
+
366
+ def _command_assertion(value: str | Sequence[str] | None) -> str:
367
+ if not isinstance(value, str):
368
+ return ""
369
+ try:
370
+ presentation = parse_authorization(value, AuthorizationCarrier.STANDARD)
371
+ except AepAuthorizationError:
372
+ return ""
373
+ return presentation.credentials if presentation.scheme is AuthorizationScheme.AEP else ""
374
+
375
+
376
+ def _etag_matches(value: str | Sequence[str] | None, expected: str) -> bool:
377
+ values = (value,) if isinstance(value, str) else value or ()
378
+ for field_value in values:
379
+ for candidate in field_value.split(","):
380
+ candidate = candidate.strip()
381
+ if (
382
+ candidate == "*"
383
+ or candidate == expected
384
+ or candidate.removeprefix("W/") == expected
385
+ ):
386
+ return True
387
+ return False
388
+
389
+
390
+ def _resource_origin(value: str, allow_insecure_loopback: bool) -> tuple[str, str]:
391
+ parsed = urlsplit(value)
392
+ try:
393
+ _ = parsed.port
394
+ except ValueError as error:
395
+ raise ValueError("AEP protected-resource origin is invalid") from error
396
+ if (
397
+ not parsed.hostname
398
+ or parsed.username is not None
399
+ or parsed.password is not None
400
+ or parsed.path not in ("", "/")
401
+ or parsed.query
402
+ or parsed.fragment
403
+ or "?" in value
404
+ or "#" in value
405
+ ):
406
+ raise ValueError(
407
+ "AEP protected-resource origin must not contain credentials, path, query, or fragment"
408
+ )
409
+ secure = parsed.scheme == "https"
410
+ loopback = parsed.scheme == "http" and allow_insecure_loopback and _is_loopback(parsed.hostname)
411
+ if not secure and not loopback:
412
+ raise ValueError("AEP protected-resource origin must use HTTPS")
413
+ return parsed.scheme, parsed.netloc
414
+
415
+
416
+ def _is_loopback(hostname: str) -> bool:
417
+ if hostname == "localhost":
418
+ return True
419
+ try:
420
+ return ipaddress.ip_address(hostname).is_loopback
421
+ except ValueError:
422
+ return False
423
+
424
+
425
+ def _resource_url(origin: tuple[str, str], scope: Mapping[str, Any]) -> str:
426
+ raw_path = scope.get("raw_path")
427
+ if isinstance(raw_path, bytes):
428
+ path = quote_from_bytes(raw_path, safe="/%:@!$&'()*+,;=-._~")
429
+ else:
430
+ path = quote(_scope_text(scope, "path"), safe="/:@!$&'()*+,;=-._~")
431
+ raw_query = scope.get("query_string", b"")
432
+ if not isinstance(raw_query, bytes):
433
+ raise RuntimeError("AEP ASGI query string must be bytes")
434
+ return urlunsplit((origin[0], origin[1], path, raw_query.decode("ascii"), ""))
435
+
436
+
437
+ def _scope_text(scope: Mapping[str, Any], name: str) -> str:
438
+ value = scope.get(name)
439
+ if not isinstance(value, str):
440
+ raise RuntimeError(f'AEP ASGI scope field "{name}" must be text')
441
+ return value
442
+
443
+
444
+ def _json_bytes(value: object) -> bytes:
445
+ return json.dumps(value, separators=(",", ":"), sort_keys=True).encode()
@@ -0,0 +1,96 @@
1
+ """Agent-side enrollment, credential, and authentication workflows."""
2
+
3
+ from .client import Agent, AgentOptions, ServiceSession
4
+ from .platform_provider import (
5
+ MemoryPlatformDiscoveryCache,
6
+ PlatformAuthenticationHeaders,
7
+ PlatformCommandError,
8
+ PlatformContextProvider,
9
+ PlatformDiscoveryCache,
10
+ PlatformDiscoveryCacheEntry,
11
+ PlatformIdempotencyKeyFactory,
12
+ PlatformIdentityProvider,
13
+ PlatformIdentityProviderOptions,
14
+ PlatformPendingSign,
15
+ PlatformPendingSignResolver,
16
+ PlatformSignPendingError,
17
+ )
18
+ from .stores import (
19
+ MemoryCredentialStore,
20
+ MemoryIdentityStore,
21
+ MemoryInspectCache,
22
+ RandomIdempotencyKeyProvider,
23
+ )
24
+ from .transport import AsyncHttpTransport, HttpxTransport
25
+ from .types import (
26
+ AgentCommandError,
27
+ AssertionSigner,
28
+ AuthenticationOptions,
29
+ BuiltInCredential,
30
+ ClaimRequirementsError,
31
+ CommandResult,
32
+ CredentialRecord,
33
+ CredentialStore,
34
+ EnrollmentStateError,
35
+ EnrollOptions,
36
+ GrantOptions,
37
+ GrantResult,
38
+ IdempotencyKeyProvider,
39
+ IdentityProvider,
40
+ IdentityRequest,
41
+ IdentityStore,
42
+ InspectCache,
43
+ InspectCacheEntry,
44
+ Inspection,
45
+ OperationKey,
46
+ RevokeOptions,
47
+ ServiceIdentity,
48
+ WaitOptions,
49
+ )
50
+
51
+ __all__ = [
52
+ "Agent",
53
+ "AgentCommandError",
54
+ "AgentOptions",
55
+ "AssertionSigner",
56
+ "AsyncHttpTransport",
57
+ "AuthenticationOptions",
58
+ "BuiltInCredential",
59
+ "ClaimRequirementsError",
60
+ "CommandResult",
61
+ "CredentialRecord",
62
+ "CredentialStore",
63
+ "EnrollOptions",
64
+ "EnrollmentStateError",
65
+ "GrantOptions",
66
+ "GrantResult",
67
+ "HttpxTransport",
68
+ "IdempotencyKeyProvider",
69
+ "IdentityProvider",
70
+ "IdentityRequest",
71
+ "IdentityStore",
72
+ "InspectCache",
73
+ "InspectCacheEntry",
74
+ "Inspection",
75
+ "MemoryCredentialStore",
76
+ "MemoryIdentityStore",
77
+ "MemoryInspectCache",
78
+ "MemoryPlatformDiscoveryCache",
79
+ "OperationKey",
80
+ "PlatformAuthenticationHeaders",
81
+ "PlatformCommandError",
82
+ "PlatformContextProvider",
83
+ "PlatformDiscoveryCache",
84
+ "PlatformDiscoveryCacheEntry",
85
+ "PlatformIdempotencyKeyFactory",
86
+ "PlatformIdentityProvider",
87
+ "PlatformIdentityProviderOptions",
88
+ "PlatformPendingSign",
89
+ "PlatformPendingSignResolver",
90
+ "PlatformSignPendingError",
91
+ "RandomIdempotencyKeyProvider",
92
+ "RevokeOptions",
93
+ "ServiceIdentity",
94
+ "ServiceSession",
95
+ "WaitOptions",
96
+ ]