apoa 0.1.0__tar.gz

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.
apoa-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: apoa
3
+ Version: 0.1.0
4
+ Summary: Python SDK for the Agentic Power of Attorney (APOA) standard
5
+ Author-email: Agentic POA <agenticpoa@users.noreply.github.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/agenticpoa/apoa
8
+ Project-URL: Documentation, https://github.com/agenticpoa/apoa/tree/main/sdk-python
9
+ Project-URL: Repository, https://github.com/agenticpoa/apoa
10
+ Project-URL: Issues, https://github.com/agenticpoa/apoa/issues
11
+ Keywords: authorization,ai-agents,delegation,jwt,security
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Security
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: PyJWT[crypto]>=2.8.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
26
+
27
+ # APOA Python SDK
28
+
29
+ Python SDK for the [Agentic Power of Attorney (APOA)](https://github.com/agenticpoa/apoa) standard -- authorization infrastructure for AI agents.
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pip install apoa
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from apoa import (
41
+ APOADefinition, Agent, BrowserSessionConfig, Principal,
42
+ Rule, ServiceAuthorization, SigningOptions,
43
+ create_client, generate_key_pair,
44
+ )
45
+
46
+ # Generate keys and create a client
47
+ private_key, public_key = generate_key_pair()
48
+ client = create_client(default_private_key=private_key)
49
+
50
+ # Create a signed authorization token
51
+ token = client.create_token(APOADefinition(
52
+ principal=Principal(id="did:apoa:you"),
53
+ agent=Agent(id="did:apoa:your-agent", name="HomeBot Pro"),
54
+ services=[ServiceAuthorization(
55
+ service="nationwidemortgage.com",
56
+ scopes=["rate_lock:read", "documents:read"],
57
+ constraints={"signing": False},
58
+ access_mode="browser",
59
+ browser_config=BrowserSessionConfig(
60
+ allowed_urls=["https://portal.nationwidemortgage.com/*"],
61
+ credential_vault_ref="1password://vault/mortgage-portal",
62
+ ),
63
+ )],
64
+ rules=[Rule(id="no-signing", description="Never sign anything", enforcement="hard")],
65
+ expires="2026-09-01",
66
+ ))
67
+
68
+ # Authorize actions
69
+ result = client.authorize(token, "nationwidemortgage.com", "rate_lock:read")
70
+ print(result.authorized) # True
71
+
72
+ result = client.authorize(token, "nationwidemortgage.com", "documents:sign")
73
+ print(result.authorized) # False
74
+ ```
75
+
76
+ ## Features
77
+
78
+ - **Token lifecycle**: create, sign (Ed25519/ES256), validate, parse
79
+ - **Scope matching**: hierarchical pattern matching (`appointments:*` matches `appointments:read`)
80
+ - **Constraint enforcement**: boolean denial at the SDK level, rich constraints at the protocol level
81
+ - **Authorization**: revocation + scope + constraints + hard/soft rules in one call
82
+ - **Delegation chains**: parent-to-child with cryptographically enforced attenuation
83
+ - **Cascade revocation**: revoke parent, kill all children instantly
84
+ - **Audit trail**: append-only action log per token
85
+ - **Cross-SDK compatibility**: tokens created by the TypeScript SDK validate in Python and vice versa
86
+
87
+ ## Cross-SDK Compatibility
88
+
89
+ Tokens are JWTs. A token signed by `@apoa/core` (TypeScript) validates in `apoa` (Python) and vice versa. The serialization layer handles camelCase (JWT payload) to snake_case (Python) mapping automatically.
90
+
91
+ ## API
92
+
93
+ Two usage styles:
94
+
95
+ ```python
96
+ # Style 1: Client instance (recommended)
97
+ client = create_client(default_private_key=key)
98
+ client.authorize(token, "service.com", "action:read")
99
+
100
+ # Style 2: Standalone imports
101
+ from apoa import authorize, check_scope
102
+ check_scope(token, "service.com", "action:read")
103
+ ```
104
+
105
+ See the [full spec](https://github.com/agenticpoa/apoa/blob/main/SPEC.md) and [TypeScript SDK](https://github.com/agenticpoa/apoa/tree/main/sdk) for more.
106
+
107
+ ## License
108
+
109
+ Apache 2.0
apoa-0.1.0/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # APOA Python SDK
2
+
3
+ Python SDK for the [Agentic Power of Attorney (APOA)](https://github.com/agenticpoa/apoa) standard -- authorization infrastructure for AI agents.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install apoa
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from apoa import (
15
+ APOADefinition, Agent, BrowserSessionConfig, Principal,
16
+ Rule, ServiceAuthorization, SigningOptions,
17
+ create_client, generate_key_pair,
18
+ )
19
+
20
+ # Generate keys and create a client
21
+ private_key, public_key = generate_key_pair()
22
+ client = create_client(default_private_key=private_key)
23
+
24
+ # Create a signed authorization token
25
+ token = client.create_token(APOADefinition(
26
+ principal=Principal(id="did:apoa:you"),
27
+ agent=Agent(id="did:apoa:your-agent", name="HomeBot Pro"),
28
+ services=[ServiceAuthorization(
29
+ service="nationwidemortgage.com",
30
+ scopes=["rate_lock:read", "documents:read"],
31
+ constraints={"signing": False},
32
+ access_mode="browser",
33
+ browser_config=BrowserSessionConfig(
34
+ allowed_urls=["https://portal.nationwidemortgage.com/*"],
35
+ credential_vault_ref="1password://vault/mortgage-portal",
36
+ ),
37
+ )],
38
+ rules=[Rule(id="no-signing", description="Never sign anything", enforcement="hard")],
39
+ expires="2026-09-01",
40
+ ))
41
+
42
+ # Authorize actions
43
+ result = client.authorize(token, "nationwidemortgage.com", "rate_lock:read")
44
+ print(result.authorized) # True
45
+
46
+ result = client.authorize(token, "nationwidemortgage.com", "documents:sign")
47
+ print(result.authorized) # False
48
+ ```
49
+
50
+ ## Features
51
+
52
+ - **Token lifecycle**: create, sign (Ed25519/ES256), validate, parse
53
+ - **Scope matching**: hierarchical pattern matching (`appointments:*` matches `appointments:read`)
54
+ - **Constraint enforcement**: boolean denial at the SDK level, rich constraints at the protocol level
55
+ - **Authorization**: revocation + scope + constraints + hard/soft rules in one call
56
+ - **Delegation chains**: parent-to-child with cryptographically enforced attenuation
57
+ - **Cascade revocation**: revoke parent, kill all children instantly
58
+ - **Audit trail**: append-only action log per token
59
+ - **Cross-SDK compatibility**: tokens created by the TypeScript SDK validate in Python and vice versa
60
+
61
+ ## Cross-SDK Compatibility
62
+
63
+ Tokens are JWTs. A token signed by `@apoa/core` (TypeScript) validates in `apoa` (Python) and vice versa. The serialization layer handles camelCase (JWT payload) to snake_case (Python) mapping automatically.
64
+
65
+ ## API
66
+
67
+ Two usage styles:
68
+
69
+ ```python
70
+ # Style 1: Client instance (recommended)
71
+ client = create_client(default_private_key=key)
72
+ client.authorize(token, "service.com", "action:read")
73
+
74
+ # Style 2: Standalone imports
75
+ from apoa import authorize, check_scope
76
+ check_scope(token, "service.com", "action:read")
77
+ ```
78
+
79
+ See the [full spec](https://github.com/agenticpoa/apoa/blob/main/SPEC.md) and [TypeScript SDK](https://github.com/agenticpoa/apoa/tree/main/sdk) for more.
80
+
81
+ ## License
82
+
83
+ Apache 2.0
@@ -0,0 +1,144 @@
1
+ """APOA SDK -- Agentic Power of Attorney for Python."""
2
+
3
+ # Client
4
+ from .client import APOAClient, create_client
5
+
6
+ # Token lifecycle
7
+ from .token import create_token, parse_definition, validate_token
8
+
9
+ # Authorization
10
+ from .authorization import authorize
11
+ from .constraints import check_constraint
12
+ from .scope import check_scope, match_scope
13
+
14
+ # Delegation
15
+ from .delegation import delegate, verify_chain
16
+
17
+ # Revocation
18
+ from .revocation import (
19
+ MemoryRevocationStore,
20
+ RevocationStore,
21
+ cascade_revoke,
22
+ is_revoked,
23
+ revoke,
24
+ )
25
+
26
+ # Audit
27
+ from .audit import (
28
+ AuditStore,
29
+ MemoryAuditStore,
30
+ get_audit_trail,
31
+ get_audit_trail_by_service,
32
+ log_action,
33
+ )
34
+
35
+ # Crypto
36
+ from .crypto import generate_key_pair
37
+
38
+ # Time utilities
39
+ from .utils import is_before_not_before, is_expired
40
+
41
+ # Types
42
+ from .types import (
43
+ APIAccessConfig,
44
+ APOADefinition,
45
+ APOAToken,
46
+ Agent,
47
+ AgentProvider,
48
+ AuditEntry,
49
+ AuditQueryOptions,
50
+ AuthorizationResult,
51
+ BrowserSessionConfig,
52
+ ChainVerificationResult,
53
+ DelegationDefinition,
54
+ LegalFramework,
55
+ Principal,
56
+ RevocationRecord,
57
+ Rule,
58
+ RuleViolation,
59
+ ScopeCheckResult,
60
+ ServiceAuthorization,
61
+ SigningOptions,
62
+ ValidationOptions,
63
+ ValidationResult,
64
+ )
65
+
66
+ # Errors
67
+ from .errors import (
68
+ APOAError,
69
+ AttenuationViolationError,
70
+ ChainVerificationError,
71
+ DefinitionValidationError,
72
+ MetadataValidationError,
73
+ RevocationError,
74
+ RuleEnforcementError,
75
+ ScopeViolationError,
76
+ TokenExpiredError,
77
+ )
78
+
79
+ __all__ = [
80
+ # Client
81
+ "APOAClient",
82
+ "create_client",
83
+ # Token lifecycle
84
+ "create_token",
85
+ "validate_token",
86
+ "parse_definition",
87
+ # Authorization
88
+ "authorize",
89
+ "check_scope",
90
+ "match_scope",
91
+ "check_constraint",
92
+ # Delegation
93
+ "delegate",
94
+ "verify_chain",
95
+ # Revocation
96
+ "revoke",
97
+ "is_revoked",
98
+ "cascade_revoke",
99
+ "RevocationStore",
100
+ "MemoryRevocationStore",
101
+ # Audit
102
+ "log_action",
103
+ "get_audit_trail",
104
+ "get_audit_trail_by_service",
105
+ "AuditStore",
106
+ "MemoryAuditStore",
107
+ # Crypto
108
+ "generate_key_pair",
109
+ # Time
110
+ "is_expired",
111
+ "is_before_not_before",
112
+ # Types
113
+ "Principal",
114
+ "Agent",
115
+ "AgentProvider",
116
+ "LegalFramework",
117
+ "BrowserSessionConfig",
118
+ "APIAccessConfig",
119
+ "ServiceAuthorization",
120
+ "Rule",
121
+ "RuleViolation",
122
+ "APOADefinition",
123
+ "APOAToken",
124
+ "ScopeCheckResult",
125
+ "AuthorizationResult",
126
+ "ValidationResult",
127
+ "RevocationRecord",
128
+ "AuditEntry",
129
+ "AuditQueryOptions",
130
+ "DelegationDefinition",
131
+ "ChainVerificationResult",
132
+ "SigningOptions",
133
+ "ValidationOptions",
134
+ # Errors
135
+ "APOAError",
136
+ "TokenExpiredError",
137
+ "ScopeViolationError",
138
+ "AttenuationViolationError",
139
+ "RevocationError",
140
+ "ChainVerificationError",
141
+ "MetadataValidationError",
142
+ "RuleEnforcementError",
143
+ "DefinitionValidationError",
144
+ ]
@@ -0,0 +1,100 @@
1
+ """Audit store and logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Protocol
7
+
8
+ from .types import AuditEntry, AuditQueryOptions
9
+
10
+
11
+ class AuditStore(Protocol):
12
+ def append(self, entry: AuditEntry) -> None: ...
13
+ def query(self, token_id: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]: ...
14
+ def query_by_service(self, service: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]: ...
15
+
16
+
17
+ class MemoryAuditStore:
18
+ """In-memory audit store for dev/testing."""
19
+
20
+ def __init__(self) -> None:
21
+ self._entries: list[AuditEntry] = []
22
+
23
+ def append(self, entry: AuditEntry) -> None:
24
+ self._entries.append(entry)
25
+
26
+ def query(self, token_id: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
27
+ results = [e for e in self._entries if e.token_id == token_id]
28
+ return _apply_query_options(results, options)
29
+
30
+ def query_by_service(self, service: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
31
+ results = [e for e in self._entries if e.service == service]
32
+ return _apply_query_options(results, options)
33
+
34
+
35
+ def _apply_query_options(entries: list[AuditEntry], options: AuditQueryOptions | None) -> list[AuditEntry]:
36
+ if options is None:
37
+ return entries
38
+
39
+ results = entries
40
+ if options.from_time:
41
+ results = [e for e in results if e.timestamp >= options.from_time]
42
+ if options.to_time:
43
+ results = [e for e in results if e.timestamp <= options.to_time]
44
+ if options.action:
45
+ results = [e for e in results if e.action == options.action]
46
+ if options.service:
47
+ results = [e for e in results if e.service == options.service]
48
+ if options.result:
49
+ results = [e for e in results if e.result == options.result]
50
+ if options.offset:
51
+ results = results[options.offset:]
52
+ if options.limit:
53
+ results = results[: options.limit]
54
+
55
+ return results
56
+
57
+
58
+ def log_action(
59
+ token_id: str,
60
+ action: str,
61
+ service: str,
62
+ result: str,
63
+ store: AuditStore | None = None,
64
+ **details: str | int | float | bool | None,
65
+ ) -> None:
66
+ """Log an action against a token."""
67
+ if store is None:
68
+ raise ValueError("audit store is required")
69
+
70
+ entry = AuditEntry(
71
+ token_id=token_id,
72
+ timestamp=datetime.now(timezone.utc),
73
+ action=action,
74
+ service=service,
75
+ result=result,
76
+ details=details if details else None,
77
+ )
78
+ store.append(entry)
79
+
80
+
81
+ def get_audit_trail(
82
+ token_id: str,
83
+ options: AuditQueryOptions | None = None,
84
+ store: AuditStore | None = None,
85
+ ) -> list[AuditEntry]:
86
+ """Get the audit trail for a token."""
87
+ if store is None:
88
+ raise ValueError("audit store is required")
89
+ return store.query(token_id, options)
90
+
91
+
92
+ def get_audit_trail_by_service(
93
+ service: str,
94
+ options: AuditQueryOptions | None = None,
95
+ store: AuditStore | None = None,
96
+ ) -> list[AuditEntry]:
97
+ """Get the audit trail for a service across all tokens."""
98
+ if store is None:
99
+ raise ValueError("audit store is required")
100
+ return store.query_by_service(service, options)
@@ -0,0 +1,122 @@
1
+ """Full authorization flow: revocation + scope + constraints + rules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ from .scope import check_scope
9
+ from .types import (
10
+ APOAToken,
11
+ AuditEntry,
12
+ AuthorizationResult,
13
+ RuleViolation,
14
+ )
15
+
16
+
17
+ def authorize(
18
+ token: APOAToken,
19
+ service: str,
20
+ action: str,
21
+ revocation_store: Any | None = None,
22
+ audit_store: Any | None = None,
23
+ ) -> AuthorizationResult:
24
+ """One-stop authorization check: revocation + scope + constraints + rules.
25
+
26
+ Enforcement order:
27
+ 1. Check revocation (is the token still alive?)
28
+ 2. Check scope (is the action in the authorized scope set?)
29
+ 3. Check constraints (action segment vs constraint key set to false)
30
+ 4. Check hard rules (deny if rule key appears in action)
31
+ 5. Check soft rules (log violation + invoke callback + continue)
32
+ """
33
+ # 1. Check revocation
34
+ if revocation_store is not None:
35
+ record = revocation_store.check(token.jti)
36
+ if record:
37
+ return AuthorizationResult(
38
+ authorized=False,
39
+ reason="token has been revoked",
40
+ checks={"revoked": True},
41
+ )
42
+
43
+ # 2. Check scope
44
+ scope_result = check_scope(token, service, action)
45
+ if not scope_result.allowed:
46
+ return AuthorizationResult(
47
+ authorized=False,
48
+ reason=scope_result.reason,
49
+ checks={"revoked": False, "scope_allowed": False},
50
+ )
51
+
52
+ # 3. Check constraints
53
+ service_auth = next((s for s in token.definition.services if s.service == service), None)
54
+ if service_auth and service_auth.constraints:
55
+ action_segments = action.split(":")
56
+ for key, value in service_auth.constraints.items():
57
+ if value is False and key in action_segments:
58
+ return AuthorizationResult(
59
+ authorized=False,
60
+ reason=f"constraint '{key}' is set to false",
61
+ checks={"revoked": False, "scope_allowed": True, "constraints_passed": False},
62
+ )
63
+
64
+ # 4 & 5. Check rules
65
+ rules = token.definition.rules
66
+ violations: list[RuleViolation] = []
67
+
68
+ if rules:
69
+ # 4. Hard rules
70
+ for rule in rules:
71
+ if rule.enforcement == "hard":
72
+ rule_key = rule.id[3:] if rule.id.startswith("no-") else rule.id
73
+ if rule_key.lower() in action.lower():
74
+ return AuthorizationResult(
75
+ authorized=False,
76
+ reason=f"hard rule '{rule.id}' violated",
77
+ checks={
78
+ "revoked": False,
79
+ "scope_allowed": True,
80
+ "constraints_passed": True,
81
+ "rules_passed": False,
82
+ },
83
+ )
84
+
85
+ # 5. Soft rules
86
+ for rule in rules:
87
+ if rule.enforcement == "soft":
88
+ violation = RuleViolation(
89
+ rule_id=rule.id,
90
+ token_id=token.jti,
91
+ action=action,
92
+ service=service,
93
+ timestamp=datetime.now(timezone.utc),
94
+ details=rule.description,
95
+ )
96
+ violations.append(violation)
97
+
98
+ # Log to audit store if available
99
+ if audit_store is not None:
100
+ audit_store.append(AuditEntry(
101
+ token_id=token.jti,
102
+ timestamp=violation.timestamp,
103
+ action=action,
104
+ service=service,
105
+ result="escalated",
106
+ details={"ruleId": rule.id, "ruleDescription": rule.description},
107
+ ))
108
+
109
+ # Invoke on_violation callback if provided
110
+ if rule.on_violation:
111
+ rule.on_violation(violation)
112
+
113
+ return AuthorizationResult(
114
+ authorized=True,
115
+ checks={
116
+ "revoked": False,
117
+ "scope_allowed": True,
118
+ "constraints_passed": True,
119
+ "rules_passed": True,
120
+ },
121
+ violations=violations if violations else None,
122
+ )
@@ -0,0 +1,131 @@
1
+ """APOAClient -- configured facade that wires up stores and keys."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from .audit import MemoryAuditStore, get_audit_trail, get_audit_trail_by_service, log_action
8
+ from .authorization import authorize
9
+ from .constraints import check_constraint
10
+ from .crypto import generate_key_pair
11
+ from .delegation import delegate, verify_chain
12
+ from .revocation import (
13
+ MemoryRevocationStore,
14
+ cascade_revoke,
15
+ is_revoked,
16
+ revoke,
17
+ )
18
+ from .scope import check_scope
19
+ from .token import create_token, parse_definition, validate_token
20
+ from .types import (
21
+ APOADefinition,
22
+ APOAToken,
23
+ AuditEntry,
24
+ AuditQueryOptions,
25
+ AuthorizationResult,
26
+ ChainVerificationResult,
27
+ DelegationDefinition,
28
+ RevocationRecord,
29
+ ScopeCheckResult,
30
+ SigningOptions,
31
+ ValidationOptions,
32
+ ValidationResult,
33
+ )
34
+
35
+
36
+ class APOAClient:
37
+ """Configured APOA client with wired-up stores and default signing options."""
38
+
39
+ def __init__(
40
+ self,
41
+ revocation_store: Any | None = None,
42
+ audit_store: Any | None = None,
43
+ key_resolver: Any | None = None,
44
+ default_private_key: Any | None = None,
45
+ default_algorithm: str = "EdDSA",
46
+ ) -> None:
47
+ self._revocation_store = revocation_store or MemoryRevocationStore()
48
+ self._audit_store = audit_store or MemoryAuditStore()
49
+ self._key_resolver = key_resolver
50
+ self._default_private_key = default_private_key
51
+ self._default_algorithm = default_algorithm
52
+
53
+ def _signing_options(self, options: SigningOptions | None = None) -> SigningOptions:
54
+ if options:
55
+ return options
56
+ if self._default_private_key is None:
57
+ raise ValueError("No signing options provided and no default_private_key configured")
58
+ return SigningOptions(private_key=self._default_private_key, algorithm=self._default_algorithm)
59
+
60
+ def create_token(self, definition: APOADefinition, options: SigningOptions | None = None) -> APOAToken:
61
+ return create_token(definition, self._signing_options(options))
62
+
63
+ def validate_token(self, token: str | APOAToken, options: ValidationOptions | None = None) -> ValidationResult:
64
+ opts = options or ValidationOptions()
65
+ # Wire up defaults
66
+ if opts.key_resolver is None and self._key_resolver is not None:
67
+ opts.key_resolver = self._key_resolver
68
+ if opts.revocation_store is None:
69
+ opts.revocation_store = self._revocation_store
70
+ if not opts.check_revocation:
71
+ opts.check_revocation = True
72
+ return validate_token(token, opts)
73
+
74
+ def check_scope(self, token: APOAToken, service: str, action: str) -> ScopeCheckResult:
75
+ return check_scope(token, service, action)
76
+
77
+ def check_constraint(self, token: APOAToken, service: str, constraint: str) -> ScopeCheckResult:
78
+ return check_constraint(token, service, constraint)
79
+
80
+ def authorize(self, token: APOAToken, service: str, action: str) -> AuthorizationResult:
81
+ return authorize(token, service, action, self._revocation_store, self._audit_store)
82
+
83
+ def delegate(self, parent_token: APOAToken, child_def: DelegationDefinition, options: SigningOptions | None = None) -> APOAToken:
84
+ return delegate(parent_token, child_def, self._signing_options(options))
85
+
86
+ def verify_chain(self, chain: list[APOAToken]) -> ChainVerificationResult:
87
+ return verify_chain(chain, self._revocation_store)
88
+
89
+ def revoke(self, token_id: str, revoked_by: str, reason: str | None = None) -> RevocationRecord:
90
+ return revoke(token_id, revoked_by, reason, self._revocation_store)
91
+
92
+ def is_revoked(self, token_id: str) -> bool:
93
+ return is_revoked(token_id, self._revocation_store)
94
+
95
+ def cascade_revoke(self, parent_token_id: str, child_token_ids: list[str], revoked_by: str, reason: str | None = None) -> RevocationRecord:
96
+ return cascade_revoke(parent_token_id, child_token_ids, revoked_by, reason, self._revocation_store)
97
+
98
+ def log_action(self, token_id: str, action: str, service: str, result: str, **details: str | int | float | bool | None) -> None:
99
+ log_action(token_id, action, service, result, self._audit_store, **details)
100
+
101
+ def get_audit_trail(self, token_id: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
102
+ return get_audit_trail(token_id, options, self._audit_store)
103
+
104
+ def get_audit_trail_by_service(self, service: str, options: AuditQueryOptions | None = None) -> list[AuditEntry]:
105
+ return get_audit_trail_by_service(service, options, self._audit_store)
106
+
107
+ def generate_key_pair(self, algorithm: str | None = None) -> tuple:
108
+ return generate_key_pair(algorithm or self._default_algorithm)
109
+
110
+ def parse_definition(self, input_str: str, format: str = "json") -> APOADefinition:
111
+ return parse_definition(input_str, format)
112
+
113
+
114
+ def create_client(
115
+ revocation_store: Any | None = None,
116
+ audit_store: Any | None = None,
117
+ key_resolver: Any | None = None,
118
+ default_private_key: Any | None = None,
119
+ default_algorithm: str = "EdDSA",
120
+ ) -> APOAClient:
121
+ """Create a configured APOA client.
122
+
123
+ Defaults to MemoryRevocationStore + MemoryAuditStore for zero-config dev/testing.
124
+ """
125
+ return APOAClient(
126
+ revocation_store=revocation_store,
127
+ audit_store=audit_store,
128
+ key_resolver=key_resolver,
129
+ default_private_key=default_private_key,
130
+ default_algorithm=default_algorithm,
131
+ )