spaps 0.5.1__tar.gz → 0.6.2__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.
Files changed (94) hide show
  1. {spaps-0.5.1 → spaps-0.6.2}/.gitignore +4 -0
  2. {spaps-0.5.1 → spaps-0.6.2}/CHANGELOG.md +41 -0
  3. {spaps-0.5.1 → spaps-0.6.2}/FULL_CHECKLIST.md +1 -1
  4. {spaps-0.5.1 → spaps-0.6.2}/PKG-INFO +207 -10
  5. {spaps-0.5.1 → spaps-0.6.2}/README.md +206 -9
  6. {spaps-0.5.1 → spaps-0.6.2}/pyproject.toml +5 -1
  7. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/__init__.py +150 -4
  8. spaps-0.6.2/src/spaps_client/app_links.py +230 -0
  9. spaps-0.6.2/src/spaps_client/app_links_async.py +183 -0
  10. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/async_client.py +21 -0
  11. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/auth.py +456 -26
  12. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/auth_async.py +287 -15
  13. spaps-0.6.2/src/spaps_client/capability.py +785 -0
  14. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/client.py +62 -0
  15. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/crypto.py +11 -6
  16. spaps-0.6.2/src/spaps_client/dayrate.py +239 -0
  17. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/email.py +16 -0
  18. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/email_async.py +3 -0
  19. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/entitlements.py +273 -0
  20. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/entitlements_async.py +224 -1
  21. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/payments.py +7 -5
  22. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/payments_async.py +6 -4
  23. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/sessions.py +4 -3
  24. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/sessions_async.py +4 -4
  25. spaps-0.6.2/src/spaps_client/users.py +409 -0
  26. spaps-0.6.2/src/spaps_client/users_async.py +272 -0
  27. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/whitelist.py +4 -7
  28. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/whitelist_async.py +4 -8
  29. {spaps-0.5.1 → spaps-0.6.2}/tests/integration/test_build.py +23 -1
  30. spaps-0.6.2/tests/unit/test_app_links.py +177 -0
  31. spaps-0.6.2/tests/unit/test_async_client.py +838 -0
  32. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_auth.py +521 -3
  33. spaps-0.6.2/tests/unit/test_capability.py +215 -0
  34. spaps-0.6.2/tests/unit/test_dayrate.py +137 -0
  35. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_email.py +31 -0
  36. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_entitlements.py +340 -0
  37. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_payments.py +18 -21
  38. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_quickstart_full_adoption_async.py +7 -11
  39. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_readme_snippets.py +14 -2
  40. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_sessions.py +2 -4
  41. spaps-0.6.2/tests/unit/test_users.py +343 -0
  42. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_users_async.py +148 -1
  43. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_whitelist.py +3 -7
  44. spaps-0.5.1/src/spaps_client/users.py +0 -156
  45. spaps-0.5.1/src/spaps_client/users_async.py +0 -102
  46. spaps-0.5.1/tests/unit/test_async_client.py +0 -411
  47. spaps-0.5.1/tests/unit/test_users.py +0 -164
  48. {spaps-0.5.1 → spaps-0.6.2}/LICENSE +0 -0
  49. {spaps-0.5.1 → spaps-0.6.2}/TDD_PLAN.md +0 -0
  50. {spaps-0.5.1 → spaps-0.6.2}/TODO.md +0 -0
  51. {spaps-0.5.1 → spaps-0.6.2}/docs/RELEASE_CHECKLIST.md +0 -0
  52. {spaps-0.5.1 → spaps-0.6.2}/scripts/manage_version.py +0 -0
  53. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/config.py +0 -0
  54. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/crypto_async.py +0 -0
  55. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/dayrate_async.py +0 -0
  56. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/device_flow.py +0 -0
  57. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/http.py +0 -0
  58. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/http_async.py +0 -0
  59. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/issue_reporting.py +0 -0
  60. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/issue_reporting_async.py +0 -0
  61. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/marketing.py +0 -0
  62. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/metrics.py +0 -0
  63. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/metrics_async.py +0 -0
  64. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/permissions.py +0 -0
  65. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/py.typed +0 -0
  66. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/secure_messages.py +0 -0
  67. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/secure_messages_async.py +0 -0
  68. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/skill_evals.py +0 -0
  69. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/skill_evals_async.py +0 -0
  70. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/storage.py +0 -0
  71. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/support_telemetry.py +0 -0
  72. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/usage.py +0 -0
  73. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/usage_async.py +0 -0
  74. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/webhooks.py +0 -0
  75. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/x402.py +0 -0
  76. {spaps-0.5.1 → spaps-0.6.2}/src/spaps_client/x402_async.py +0 -0
  77. {spaps-0.5.1 → spaps-0.6.2}/tests/conftest.py +0 -0
  78. {spaps-0.5.1 → spaps-0.6.2}/tests/integration/test_smoke.py +0 -0
  79. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_client.py +0 -0
  80. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_config.py +0 -0
  81. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_device_flow.py +0 -0
  82. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_http.py +0 -0
  83. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_issue_reporting.py +0 -0
  84. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_marketing.py +0 -0
  85. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_metadata.py +0 -0
  86. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_metrics.py +0 -0
  87. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_permissions.py +0 -0
  88. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_secure_messages.py +0 -0
  89. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_skill_evals.py +0 -0
  90. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_storage.py +0 -0
  91. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_support_telemetry.py +0 -0
  92. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_usage.py +0 -0
  93. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_webhooks.py +0 -0
  94. {spaps-0.5.1 → spaps-0.6.2}/tests/unit/test_x402.py +0 -0
@@ -164,6 +164,7 @@ backup-feb8.sql
164
164
  # Local modes/swarm artifacts
165
165
  .ntm/
166
166
  .skillbox/
167
+ artifacts/openapi-contract/
167
168
  MODES_ANALYSIS_PROGRESS.md
168
169
  MODES_OF_REASONING_REPORT_AND_ANALYSIS_OF_PROJECT.md
169
170
  MODE_OUTPUT_*.md
@@ -200,3 +201,6 @@ refactor/
200
201
 
201
202
  # bv (beads viewer) local config and caches
202
203
  .bv/
204
+
205
+ # Beads compliance audit dir (managed by /beads-compliance-and-completion-verification skill)
206
+ /beads_compliance_audit/
@@ -6,6 +6,47 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.6.2] - 2026-06-16
10
+
11
+ ### Fixed
12
+
13
+ - Accepted `stream`, `owner_id`, `subject_override`, and `body_override` on `EmailClient.send()` and accepted `stream` on `AsyncEmailClient.send()` so Python consumers can delegate SPAPS email sends without hand-rolling the `/api/email/send` envelope.
14
+
15
+ ### Changed
16
+
17
+ - Bumped the Python client package metadata to `0.6.2`.
18
+
19
+ ## [0.6.1] - 2026-06-14
20
+
21
+ ### Added
22
+
23
+ - Added sync `DayrateClient` helpers and sync/async `AppLinksClient` helpers, with high-level `SpapsClient.dayrate`, `SpapsClient.app_links`, and `AsyncSpapsClient.app_links` accessors.
24
+ - Added sync and async `UsersClient.memberships` helpers for listing, inviting/adding, and removing app account users.
25
+ - Added sync and async `EntitlementsClient.project_grants` helpers for granting, revoking, listing, checking, and claiming project access grants.
26
+
27
+ ### Fixed
28
+
29
+ - Accepted canonical SPAPS password reset request envelopes that return `success`, `data.email`, `data.sent_at`, `request_id`, and `timestamp` without requiring a top-level English `message`.
30
+ - Exposed omitted password reset request messages as `None` instead of an empty string placeholder so apps keep ownership of success copy.
31
+ - Preserved password reset confirmation parsing for canonical envelopes where the success copy lives at `data.message`.
32
+
33
+ ### Changed
34
+
35
+ - Bumped the Python client package metadata to `0.6.1`; HTMA should consume `spaps>=0.6.1` or the matching unreleased local wheel/source checkout before retrying the reset flow.
36
+
37
+ ## [0.6.0] - 2026-06-13
38
+
39
+ ### Added
40
+
41
+ - Added sync and async auth helpers for OIDC nonce/sign-in, WebAuthn registration/assertion, TOTP MFA enrollment/verification/disable, and SMS OTP request/verify flows.
42
+ - Added typed MFA challenge responses so token-issuing auth methods can return either tokens or an MFA challenge without storing incomplete credentials.
43
+
44
+ ### Changed
45
+
46
+ - Bumped the Python client package metadata to `0.6.0`.
47
+
48
+ ## [0.5.1] - 2026-06-04
49
+
9
50
  ### Added
10
51
 
11
52
  - Added sync and async issue-reporting screenshot attachment helpers for private upload, list, access URL, delete, and attach-by-ID create/update/reply flows.
@@ -12,7 +12,7 @@
12
12
  - [x] Payments: update payment method
13
13
  - [x] Payments: Stripe checkout (existing)
14
14
  - [x] Payments: Stripe payment intent (existing)
15
- - [x] Payments: wallet deposit/status (existing)
15
+ - [x] Payments: wallet deposit/status helpers intentionally raise `NotImplementedError` for backend 501 stubs
16
16
  - [x] Payments: subscription details/cancel (existing)
17
17
  - [x] Payments: crypto invoices (create/get/reconcile)
18
18
  - [x] Payments: crypto webhooks signature helper
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spaps
3
- Version: 0.5.1
3
+ Version: 0.6.2
4
4
  Summary: Sweet Potato Authentication & Payment Service Python client
5
5
  Project-URL: Homepage, https://api.sweetpotato.dev
6
6
  Project-URL: Repository, https://github.com/sweet-potato/spaps
@@ -80,7 +80,7 @@ This package targets Python 3.9+.
80
80
  ## Quick Start
81
81
 
82
82
  ```python
83
- from spaps_client import SpapsClient
83
+ from spaps_client import MfaRequiredChallenge, SpapsClient
84
84
 
85
85
  client = SpapsClient(
86
86
  base_url="http://localhost:3301",
@@ -88,10 +88,20 @@ client = SpapsClient(
88
88
  )
89
89
 
90
90
  try:
91
- tokens = client.auth.sign_in_with_password(
91
+ login = client.auth.sign_in_with_password(
92
92
  email="user@example.com",
93
93
  password="correct-horse-battery-staple",
94
94
  )
95
+ if isinstance(login, MfaRequiredChallenge):
96
+ tokens = client.auth.mfa_verify(
97
+ challenge_id=login.challenge_id,
98
+ challenge=login.challenge,
99
+ code="123456",
100
+ )
101
+ else:
102
+ tokens = login
103
+
104
+ bootstrap = client.auth.get_session_context()
95
105
  session = client.sessions.get_current_session()
96
106
  products = client.payments.list_products(
97
107
  category="subscription",
@@ -100,6 +110,7 @@ try:
100
110
  )
101
111
 
102
112
  print(tokens.user.email)
113
+ print(bootstrap.tier)
103
114
  print(session.session_id)
104
115
  print(products.total)
105
116
  finally:
@@ -133,24 +144,68 @@ async def main() -> None:
133
144
  asyncio.run(main())
134
145
  ```
135
146
 
147
+ ## Modern Auth Methods
148
+
149
+ The auth client exposes the active SPAPS auth routes in both sync and async forms:
150
+
151
+ ```python
152
+ from spaps_client import MfaRequiredChallenge, SpapsClient
153
+
154
+ client = SpapsClient(base_url="http://localhost:3301", api_key="spaps_test_key")
155
+
156
+ methods = client.auth.list_methods()
157
+ print([item.method for item in methods.methods if item.enabled])
158
+
159
+ oidc = client.auth.oidc_nonce()
160
+ login = client.auth.oidc_sign_in(
161
+ provider="google",
162
+ id_token="provider-id-token",
163
+ challenge_id=oidc.challenge_id,
164
+ )
165
+
166
+ if isinstance(login, MfaRequiredChallenge):
167
+ login = client.auth.mfa_verify(
168
+ challenge_id=login.challenge_id,
169
+ challenge=login.challenge,
170
+ recovery_code="recovery-code",
171
+ )
172
+ ```
173
+
174
+ TOTP management and passkey registration are authenticated operations and use stored tokens unless you pass `access_token` explicitly:
175
+
176
+ ```python
177
+ enrollment = client.auth.mfa_totp_enroll()
178
+ activation = client.auth.mfa_totp_activate(code="123456")
179
+ options = client.auth.webauthn_register_options()
180
+ credential = client.auth.webauthn_register_verify(
181
+ challenge_id=options.challenge_id,
182
+ credential={"id": "browser-produced-credential"},
183
+ )
184
+ ```
185
+
186
+ WebAuthn helpers intentionally pass browser-produced credential JSON through to the API; the Python client does not emulate authenticators or perform CTAP operations.
187
+
136
188
  ## Core Surface
137
189
 
138
190
  ### High-Level Clients
139
191
 
140
192
  | Client | Best for |
141
193
  | --- | --- |
142
- | `SpapsClient` | Sync auth, sessions, payments, usage, whitelist, secure messages, issue reporting, marketing, metrics, skill evals, and support telemetry |
143
- | `AsyncSpapsClient` | Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, skill evals, dayrate, and users |
194
+ | `SpapsClient` | Sync auth, sessions, payments, usage, access decisions, capability graph inspection, whitelist, secure messages, issue reporting, marketing, metrics, app links, dayrate, skill evals, and support telemetry |
195
+ | `AsyncSpapsClient` | Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, app links, dayrate, skill evals, and users |
144
196
 
145
197
  ### Standalone Helpers
146
198
 
147
199
  | Helper | Purpose |
148
200
  | --- | --- |
149
- | `EntitlementsClient` / `AsyncEntitlementsClient` | Resource entitlements, purchase history, manual grants, and revokes |
201
+ | `EntitlementsClient` / `AsyncEntitlementsClient` | Resource entitlements, purchase history, manual grants/revokes, and project grants |
150
202
  | `UsageClient` / `AsyncUsageClient` | Secret-key usage authorization and immutable usage recording |
203
+ | `CapabilityClient` | Access decisions, action preparation, decision traces, graph inspection, and contract discovery |
151
204
  | `EmailClient` / `AsyncEmailClient` | Template lookup, preview, and send flows |
152
- | `UsersClient` / `AsyncUsersClient` | Batch user and email lookups |
205
+ | `UsersClient` / `AsyncUsersClient` | Batch user/email lookups and app membership administration |
153
206
  | `MarketingClient` | Browser-safe marketing event emission and server-side experiment results |
207
+ | `AppLinksClient` / `AsyncAppLinksClient` | Authenticated short-link creation/update and public link resolution |
208
+ | `DayrateClient` / `AsyncDayrateClient` | Availability, booking, cancellation, x402 booking holds, and admin booking reads |
154
209
  | `DeviceFlowClient` | Device-code login workflows |
155
210
  | `PermissionChecker` | Role and admin convenience checks |
156
211
  | `verify_spaps_webhook` | Signature verification for incoming SPAPS webhooks |
@@ -184,6 +239,76 @@ Common parameters:
184
239
 
185
240
  ## Common Flows
186
241
 
242
+ ### Agent Access Decisions
243
+
244
+ `client.access.check_access(...)` returns a typed decision for both allowed and
245
+ denied outcomes. A blocked action is not an exception; inspect `allowed`,
246
+ `outcome`, and `next_actions`.
247
+
248
+ ```python
249
+ from spaps_client import AccessDecisionRequest, CapabilityError, SpapsClient
250
+
251
+ client = SpapsClient(
252
+ base_url="https://api.example.test",
253
+ api_key="spaps_sec_example",
254
+ )
255
+ client.set_tokens(access_token="admin-access-token")
256
+
257
+ try:
258
+ decision = client.access.check_access(
259
+ AccessDecisionRequest.model_validate(
260
+ {
261
+ "actor": {"actor_type": "user", "actor_ref": "user_123"},
262
+ "action": "checkout.create",
263
+ "resource": {"resource_type": "product", "resource_ref": "dayrate"},
264
+ "controls": {"entitlement_key": "bookme_paid"},
265
+ }
266
+ )
267
+ )
268
+
269
+ if not decision.allowed:
270
+ print(decision.outcome, decision.next_actions)
271
+
272
+ prepared = client.access.prepare_action(
273
+ {
274
+ "access": {
275
+ "actor": {"actor_ref": "admin_123"},
276
+ "action": "admin.delete_user",
277
+ "resource": {"resource_type": "user", "resource_ref": "user_123"},
278
+ },
279
+ "include_command_templates": True,
280
+ "environment": "production",
281
+ }
282
+ )
283
+ print(prepared.status, prepared.execution.operator_gate_required)
284
+
285
+ contract = client.contract.get_contract()
286
+ refresh = client.graph.refresh_graph(correlation_id="local-refresh")
287
+ nodes = client.graph.list_graph_nodes(node_type="x402_resource", q="dayrate")
288
+ explanation = client.access.explain_decision(decision.decision_trace_id)
289
+ print(contract.version, refresh.status, refresh.diagnostics.phase2_gate.recommendation if refresh.diagnostics.phase2_gate else None)
290
+ print(nodes.projection.projection_status if nodes.projection else None, explanation.graph_node_keys)
291
+ except CapabilityError as exc:
292
+ print(exc.status_code, exc.code, exc.request_id, exc.diagnostics, exc.remediations)
293
+ finally:
294
+ client.close()
295
+ ```
296
+
297
+ `client.contract.get_contract()` includes graph vocabulary fields:
298
+ `graph_node_types`, `graph_edge_types`, `graph_source_domains`, and
299
+ `source_domain_notes`. Use them before filtering graph nodes by stable v1 types
300
+ such as `wallet`, `api_key`, `role`, and `approver`.
301
+
302
+ `CapabilityError` exposes both Python-native names (`status_code`,
303
+ `error_code`, `request_id`) and parity aliases (`status`, `code`). Diagnostics
304
+ and remediations preserve server-provided `details` fields.
305
+
306
+ `operator-gated` is compatibility/descriptive input when a client supplies it;
307
+ it does not authorize mutation command templates. SPAPS returns mutation
308
+ command templates only when the request has a server-recognized non-publishable
309
+ key and an authenticated admin/operator user context. Publishable callers
310
+ cannot self-attest the gate.
311
+
187
312
  ### Magic Links and Password Reset
188
313
 
189
314
  ```python
@@ -196,15 +321,36 @@ client = SpapsClient(
196
321
 
197
322
  try:
198
323
  client.auth.send_magic_link(email="user@example.com")
199
- client.auth.request_password_reset(email="user@example.com")
200
- client.auth.confirm_password_reset(
324
+ reset_request = client.auth.request_password_reset(email="user@example.com")
325
+ reset_confirm = client.auth.confirm_password_reset(
201
326
  token="reset-token-from-email",
202
327
  new_password="Sup3rStrong!",
203
328
  )
329
+ print(reset_request.email, reset_request.sent_at, reset_confirm.message)
204
330
  finally:
205
331
  client.close()
206
332
  ```
207
333
 
334
+ Password reset responses use the standard SPAPS success envelope with top-level
335
+ `success`, `data`, `request_id`, and `timestamp`. The Python client returns the
336
+ nested `data` payload: reset requests expose `email` and `sent_at`, while reset
337
+ confirmation exposes the nested `message`. A top-level English `message` is not
338
+ canonical and should not drive UI copy. Prefer app-owned success copy, or map
339
+ future nested `data.message_key`/`data.status` values in your app.
340
+
341
+ #### Downstream Adoption
342
+
343
+ HTMA and future SPAPS consumers should use `spaps>=0.6.2` for password reset
344
+ flows and stream-aware email sends. Until `0.6.2` is published to PyPI, consume
345
+ a local wheel or source checkout from the Sweet Potato commit that carries the
346
+ `0.6.2` changelog entry.
347
+
348
+ Release execution is intentionally separate from this handoff: run the manual
349
+ `Publish Python Client` workflow when ready so release automation builds the
350
+ already-bumped package metadata and publishes to PyPI. Do not use
351
+ `npm run publish:python-client`, deploy SPAPS, or publish the package as part of
352
+ a consumer-only adoption check.
353
+
208
354
  ### Authenticated Stripe Checkout
209
355
 
210
356
  `create_checkout_session` is the convenience path for authenticated checkout.
@@ -286,6 +432,57 @@ For dashboard and policy views, use `client.usage.get_features()`,
286
432
  `client.usage.get_status(...)`, and `client.usage.get_history(...)` from the
287
433
  same trusted backend context.
288
434
 
435
+ ### Account Memberships and Project Grants
436
+
437
+ Use these helpers from a trusted backend with a secret SPAPS key. Admin mutation
438
+ helpers also require an admin access token; do not call membership invitation,
439
+ project grant, or project revoke helpers from browser code.
440
+
441
+ ```python
442
+ from spaps_client import EntitlementsClient, UsersClient
443
+
444
+ admin_token = "admin-access-token"
445
+
446
+ users = UsersClient(
447
+ base_url="https://api.example.test",
448
+ api_key="spaps_sec_example",
449
+ access_token=admin_token,
450
+ )
451
+ entitlements = EntitlementsClient(
452
+ base_url="https://api.example.test",
453
+ api_key="spaps_sec_example",
454
+ access_token=admin_token,
455
+ )
456
+
457
+ try:
458
+ users.memberships.add_account_user(
459
+ email="teammate@example.com",
460
+ capabilities={"projects": ["project_123"]},
461
+ metadata={"invited_by": "admin@example.com"},
462
+ )
463
+ entitlements.project_grants.grant_user(
464
+ project_id="project_123",
465
+ entitlement_key="pds.project.viewer",
466
+ email="teammate@example.com",
467
+ reason="Project teammate invite",
468
+ )
469
+
470
+ project_users = entitlements.project_grants.list_project_users(
471
+ "project_123",
472
+ entitlement_key="pds.project.viewer",
473
+ )
474
+ user_projects = entitlements.project_grants.list_user_projects("user_123")
475
+ access = entitlements.project_grants.check_access(
476
+ project_id="project_123",
477
+ entitlement_key="pds.project.viewer",
478
+ user_id="user_123",
479
+ )
480
+ print(project_users.count, user_projects.count, access.has_access)
481
+ finally:
482
+ users.close()
483
+ entitlements.close()
484
+ ```
485
+
289
486
  ### Marketing Events
290
487
 
291
488
  Use `client.marketing.emit_event(...)` with a publishable key to record anonymous
@@ -589,7 +786,7 @@ It can. Pass `RetryConfig` if you want retry and backoff behavior.
589
786
  ## Metadata
590
787
 
591
788
  - `package_name`: `spaps`
592
- - `latest_version`: `0.5.0`
789
+ - `latest_version`: `0.6.2`
593
790
  - `minimum_runtime`: `Python >=3.9`
594
791
  - `api_base_url`: `https://api.sweetpotato.dev`
595
792
 
@@ -24,7 +24,7 @@ This package targets Python 3.9+.
24
24
  ## Quick Start
25
25
 
26
26
  ```python
27
- from spaps_client import SpapsClient
27
+ from spaps_client import MfaRequiredChallenge, SpapsClient
28
28
 
29
29
  client = SpapsClient(
30
30
  base_url="http://localhost:3301",
@@ -32,10 +32,20 @@ client = SpapsClient(
32
32
  )
33
33
 
34
34
  try:
35
- tokens = client.auth.sign_in_with_password(
35
+ login = client.auth.sign_in_with_password(
36
36
  email="user@example.com",
37
37
  password="correct-horse-battery-staple",
38
38
  )
39
+ if isinstance(login, MfaRequiredChallenge):
40
+ tokens = client.auth.mfa_verify(
41
+ challenge_id=login.challenge_id,
42
+ challenge=login.challenge,
43
+ code="123456",
44
+ )
45
+ else:
46
+ tokens = login
47
+
48
+ bootstrap = client.auth.get_session_context()
39
49
  session = client.sessions.get_current_session()
40
50
  products = client.payments.list_products(
41
51
  category="subscription",
@@ -44,6 +54,7 @@ try:
44
54
  )
45
55
 
46
56
  print(tokens.user.email)
57
+ print(bootstrap.tier)
47
58
  print(session.session_id)
48
59
  print(products.total)
49
60
  finally:
@@ -77,24 +88,68 @@ async def main() -> None:
77
88
  asyncio.run(main())
78
89
  ```
79
90
 
91
+ ## Modern Auth Methods
92
+
93
+ The auth client exposes the active SPAPS auth routes in both sync and async forms:
94
+
95
+ ```python
96
+ from spaps_client import MfaRequiredChallenge, SpapsClient
97
+
98
+ client = SpapsClient(base_url="http://localhost:3301", api_key="spaps_test_key")
99
+
100
+ methods = client.auth.list_methods()
101
+ print([item.method for item in methods.methods if item.enabled])
102
+
103
+ oidc = client.auth.oidc_nonce()
104
+ login = client.auth.oidc_sign_in(
105
+ provider="google",
106
+ id_token="provider-id-token",
107
+ challenge_id=oidc.challenge_id,
108
+ )
109
+
110
+ if isinstance(login, MfaRequiredChallenge):
111
+ login = client.auth.mfa_verify(
112
+ challenge_id=login.challenge_id,
113
+ challenge=login.challenge,
114
+ recovery_code="recovery-code",
115
+ )
116
+ ```
117
+
118
+ TOTP management and passkey registration are authenticated operations and use stored tokens unless you pass `access_token` explicitly:
119
+
120
+ ```python
121
+ enrollment = client.auth.mfa_totp_enroll()
122
+ activation = client.auth.mfa_totp_activate(code="123456")
123
+ options = client.auth.webauthn_register_options()
124
+ credential = client.auth.webauthn_register_verify(
125
+ challenge_id=options.challenge_id,
126
+ credential={"id": "browser-produced-credential"},
127
+ )
128
+ ```
129
+
130
+ WebAuthn helpers intentionally pass browser-produced credential JSON through to the API; the Python client does not emulate authenticators or perform CTAP operations.
131
+
80
132
  ## Core Surface
81
133
 
82
134
  ### High-Level Clients
83
135
 
84
136
  | Client | Best for |
85
137
  | --- | --- |
86
- | `SpapsClient` | Sync auth, sessions, payments, usage, whitelist, secure messages, issue reporting, marketing, metrics, skill evals, and support telemetry |
87
- | `AsyncSpapsClient` | Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, skill evals, dayrate, and users |
138
+ | `SpapsClient` | Sync auth, sessions, payments, usage, access decisions, capability graph inspection, whitelist, secure messages, issue reporting, marketing, metrics, app links, dayrate, skill evals, and support telemetry |
139
+ | `AsyncSpapsClient` | Async auth, sessions, payments, usage, whitelist, secure messages, issue reporting, metrics, entitlements, app links, dayrate, skill evals, and users |
88
140
 
89
141
  ### Standalone Helpers
90
142
 
91
143
  | Helper | Purpose |
92
144
  | --- | --- |
93
- | `EntitlementsClient` / `AsyncEntitlementsClient` | Resource entitlements, purchase history, manual grants, and revokes |
145
+ | `EntitlementsClient` / `AsyncEntitlementsClient` | Resource entitlements, purchase history, manual grants/revokes, and project grants |
94
146
  | `UsageClient` / `AsyncUsageClient` | Secret-key usage authorization and immutable usage recording |
147
+ | `CapabilityClient` | Access decisions, action preparation, decision traces, graph inspection, and contract discovery |
95
148
  | `EmailClient` / `AsyncEmailClient` | Template lookup, preview, and send flows |
96
- | `UsersClient` / `AsyncUsersClient` | Batch user and email lookups |
149
+ | `UsersClient` / `AsyncUsersClient` | Batch user/email lookups and app membership administration |
97
150
  | `MarketingClient` | Browser-safe marketing event emission and server-side experiment results |
151
+ | `AppLinksClient` / `AsyncAppLinksClient` | Authenticated short-link creation/update and public link resolution |
152
+ | `DayrateClient` / `AsyncDayrateClient` | Availability, booking, cancellation, x402 booking holds, and admin booking reads |
98
153
  | `DeviceFlowClient` | Device-code login workflows |
99
154
  | `PermissionChecker` | Role and admin convenience checks |
100
155
  | `verify_spaps_webhook` | Signature verification for incoming SPAPS webhooks |
@@ -128,6 +183,76 @@ Common parameters:
128
183
 
129
184
  ## Common Flows
130
185
 
186
+ ### Agent Access Decisions
187
+
188
+ `client.access.check_access(...)` returns a typed decision for both allowed and
189
+ denied outcomes. A blocked action is not an exception; inspect `allowed`,
190
+ `outcome`, and `next_actions`.
191
+
192
+ ```python
193
+ from spaps_client import AccessDecisionRequest, CapabilityError, SpapsClient
194
+
195
+ client = SpapsClient(
196
+ base_url="https://api.example.test",
197
+ api_key="spaps_sec_example",
198
+ )
199
+ client.set_tokens(access_token="admin-access-token")
200
+
201
+ try:
202
+ decision = client.access.check_access(
203
+ AccessDecisionRequest.model_validate(
204
+ {
205
+ "actor": {"actor_type": "user", "actor_ref": "user_123"},
206
+ "action": "checkout.create",
207
+ "resource": {"resource_type": "product", "resource_ref": "dayrate"},
208
+ "controls": {"entitlement_key": "bookme_paid"},
209
+ }
210
+ )
211
+ )
212
+
213
+ if not decision.allowed:
214
+ print(decision.outcome, decision.next_actions)
215
+
216
+ prepared = client.access.prepare_action(
217
+ {
218
+ "access": {
219
+ "actor": {"actor_ref": "admin_123"},
220
+ "action": "admin.delete_user",
221
+ "resource": {"resource_type": "user", "resource_ref": "user_123"},
222
+ },
223
+ "include_command_templates": True,
224
+ "environment": "production",
225
+ }
226
+ )
227
+ print(prepared.status, prepared.execution.operator_gate_required)
228
+
229
+ contract = client.contract.get_contract()
230
+ refresh = client.graph.refresh_graph(correlation_id="local-refresh")
231
+ nodes = client.graph.list_graph_nodes(node_type="x402_resource", q="dayrate")
232
+ explanation = client.access.explain_decision(decision.decision_trace_id)
233
+ print(contract.version, refresh.status, refresh.diagnostics.phase2_gate.recommendation if refresh.diagnostics.phase2_gate else None)
234
+ print(nodes.projection.projection_status if nodes.projection else None, explanation.graph_node_keys)
235
+ except CapabilityError as exc:
236
+ print(exc.status_code, exc.code, exc.request_id, exc.diagnostics, exc.remediations)
237
+ finally:
238
+ client.close()
239
+ ```
240
+
241
+ `client.contract.get_contract()` includes graph vocabulary fields:
242
+ `graph_node_types`, `graph_edge_types`, `graph_source_domains`, and
243
+ `source_domain_notes`. Use them before filtering graph nodes by stable v1 types
244
+ such as `wallet`, `api_key`, `role`, and `approver`.
245
+
246
+ `CapabilityError` exposes both Python-native names (`status_code`,
247
+ `error_code`, `request_id`) and parity aliases (`status`, `code`). Diagnostics
248
+ and remediations preserve server-provided `details` fields.
249
+
250
+ `operator-gated` is compatibility/descriptive input when a client supplies it;
251
+ it does not authorize mutation command templates. SPAPS returns mutation
252
+ command templates only when the request has a server-recognized non-publishable
253
+ key and an authenticated admin/operator user context. Publishable callers
254
+ cannot self-attest the gate.
255
+
131
256
  ### Magic Links and Password Reset
132
257
 
133
258
  ```python
@@ -140,15 +265,36 @@ client = SpapsClient(
140
265
 
141
266
  try:
142
267
  client.auth.send_magic_link(email="user@example.com")
143
- client.auth.request_password_reset(email="user@example.com")
144
- client.auth.confirm_password_reset(
268
+ reset_request = client.auth.request_password_reset(email="user@example.com")
269
+ reset_confirm = client.auth.confirm_password_reset(
145
270
  token="reset-token-from-email",
146
271
  new_password="Sup3rStrong!",
147
272
  )
273
+ print(reset_request.email, reset_request.sent_at, reset_confirm.message)
148
274
  finally:
149
275
  client.close()
150
276
  ```
151
277
 
278
+ Password reset responses use the standard SPAPS success envelope with top-level
279
+ `success`, `data`, `request_id`, and `timestamp`. The Python client returns the
280
+ nested `data` payload: reset requests expose `email` and `sent_at`, while reset
281
+ confirmation exposes the nested `message`. A top-level English `message` is not
282
+ canonical and should not drive UI copy. Prefer app-owned success copy, or map
283
+ future nested `data.message_key`/`data.status` values in your app.
284
+
285
+ #### Downstream Adoption
286
+
287
+ HTMA and future SPAPS consumers should use `spaps>=0.6.2` for password reset
288
+ flows and stream-aware email sends. Until `0.6.2` is published to PyPI, consume
289
+ a local wheel or source checkout from the Sweet Potato commit that carries the
290
+ `0.6.2` changelog entry.
291
+
292
+ Release execution is intentionally separate from this handoff: run the manual
293
+ `Publish Python Client` workflow when ready so release automation builds the
294
+ already-bumped package metadata and publishes to PyPI. Do not use
295
+ `npm run publish:python-client`, deploy SPAPS, or publish the package as part of
296
+ a consumer-only adoption check.
297
+
152
298
  ### Authenticated Stripe Checkout
153
299
 
154
300
  `create_checkout_session` is the convenience path for authenticated checkout.
@@ -230,6 +376,57 @@ For dashboard and policy views, use `client.usage.get_features()`,
230
376
  `client.usage.get_status(...)`, and `client.usage.get_history(...)` from the
231
377
  same trusted backend context.
232
378
 
379
+ ### Account Memberships and Project Grants
380
+
381
+ Use these helpers from a trusted backend with a secret SPAPS key. Admin mutation
382
+ helpers also require an admin access token; do not call membership invitation,
383
+ project grant, or project revoke helpers from browser code.
384
+
385
+ ```python
386
+ from spaps_client import EntitlementsClient, UsersClient
387
+
388
+ admin_token = "admin-access-token"
389
+
390
+ users = UsersClient(
391
+ base_url="https://api.example.test",
392
+ api_key="spaps_sec_example",
393
+ access_token=admin_token,
394
+ )
395
+ entitlements = EntitlementsClient(
396
+ base_url="https://api.example.test",
397
+ api_key="spaps_sec_example",
398
+ access_token=admin_token,
399
+ )
400
+
401
+ try:
402
+ users.memberships.add_account_user(
403
+ email="teammate@example.com",
404
+ capabilities={"projects": ["project_123"]},
405
+ metadata={"invited_by": "admin@example.com"},
406
+ )
407
+ entitlements.project_grants.grant_user(
408
+ project_id="project_123",
409
+ entitlement_key="pds.project.viewer",
410
+ email="teammate@example.com",
411
+ reason="Project teammate invite",
412
+ )
413
+
414
+ project_users = entitlements.project_grants.list_project_users(
415
+ "project_123",
416
+ entitlement_key="pds.project.viewer",
417
+ )
418
+ user_projects = entitlements.project_grants.list_user_projects("user_123")
419
+ access = entitlements.project_grants.check_access(
420
+ project_id="project_123",
421
+ entitlement_key="pds.project.viewer",
422
+ user_id="user_123",
423
+ )
424
+ print(project_users.count, user_projects.count, access.has_access)
425
+ finally:
426
+ users.close()
427
+ entitlements.close()
428
+ ```
429
+
233
430
  ### Marketing Events
234
431
 
235
432
  Use `client.marketing.emit_event(...)` with a publishable key to record anonymous
@@ -533,7 +730,7 @@ It can. Pass `RetryConfig` if you want retry and backoff behavior.
533
730
  ## Metadata
534
731
 
535
732
  - `package_name`: `spaps`
536
- - `latest_version`: `0.5.0`
733
+ - `latest_version`: `0.6.2`
537
734
  - `minimum_runtime`: `Python >=3.9`
538
735
  - `api_base_url`: `https://api.sweetpotato.dev`
539
736
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "spaps"
7
- version = "0.5.1"
7
+ version = "0.6.2"
8
8
  description = "Sweet Potato Authentication & Payment Service Python client"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -70,6 +70,10 @@ strict_optional = true
70
70
  warn_unused_configs = true
71
71
  ignore_missing_imports = true
72
72
 
73
+ [[tool.mypy.overrides]]
74
+ module = ["click", "click.*"]
75
+ follow_imports = "skip"
76
+
73
77
  [tool.coverage.run]
74
78
  omit = [
75
79
  "src/spaps_client/*_async.py",