capauth 0.3.7__tar.gz → 0.3.8__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 (386) hide show
  1. {capauth-0.3.7 → capauth-0.3.8}/CHANGELOG.md +12 -0
  2. {capauth-0.3.7 → capauth-0.3.8}/PKG-INFO +1 -1
  3. {capauth-0.3.7 → capauth-0.3.8}/docs/SKDASHBOARD_CONTROL_PLANE_POLICY.md +12 -0
  4. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/__init__.py +2 -0
  5. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/cli.py +16 -2
  6. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/control_plane_authorizer.py +417 -23
  7. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/delegated.py +89 -29
  8. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/tokens.py +26 -3
  9. {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/PKG-INFO +1 -1
  10. {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/SOURCES.txt +1 -0
  11. capauth-0.3.8/tests/test_audience_ttl_seconds.py +128 -0
  12. {capauth-0.3.7 → capauth-0.3.8}/tests/test_control_plane_authorizer.py +298 -3
  13. {capauth-0.3.7 → capauth-0.3.8}/.cursorrules +0 -0
  14. {capauth-0.3.7 → capauth-0.3.8}/.dockerignore +0 -0
  15. {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/ci.yml +0 -0
  16. {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/docs-check.yml +0 -0
  17. {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/nextcloud-appstore.yml +0 -0
  18. {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/publish.yml +0 -0
  19. {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/pytest.yml +0 -0
  20. {capauth-0.3.7 → capauth-0.3.8}/.github/workflows/secret-scan.yml +0 -0
  21. {capauth-0.3.7 → capauth-0.3.8}/.gitignore +0 -0
  22. {capauth-0.3.7 → capauth-0.3.8}/.gitleaks-baseline.json +0 -0
  23. {capauth-0.3.7 → capauth-0.3.8}/AGENTS.md +0 -0
  24. {capauth-0.3.7 → capauth-0.3.8}/AI-ADVOCATE.md +0 -0
  25. {capauth-0.3.7 → capauth-0.3.8}/ARCHITECTURE.md +0 -0
  26. {capauth-0.3.7 → capauth-0.3.8}/CLAUDE.md +0 -0
  27. {capauth-0.3.7 → capauth-0.3.8}/CODE_OF_CONDUCT.md +0 -0
  28. {capauth-0.3.7 → capauth-0.3.8}/CONTRIBUTING.md +0 -0
  29. {capauth-0.3.7 → capauth-0.3.8}/Dockerfile +0 -0
  30. {capauth-0.3.7 → capauth-0.3.8}/Dockerfile.authentik-capauth +0 -0
  31. {capauth-0.3.7 → capauth-0.3.8}/LICENSE +0 -0
  32. {capauth-0.3.7 → capauth-0.3.8}/MANIFEST.in +0 -0
  33. {capauth-0.3.7 → capauth-0.3.8}/MISSION.md +0 -0
  34. {capauth-0.3.7 → capauth-0.3.8}/README.md +0 -0
  35. {capauth-0.3.7 → capauth-0.3.8}/SECURITY.md +0 -0
  36. {capauth-0.3.7 → capauth-0.3.8}/SKILL.md +0 -0
  37. {capauth-0.3.7 → capauth-0.3.8}/SOP.md +0 -0
  38. {capauth-0.3.7 → capauth-0.3.8}/authentik-custom/capauth_migrate.py +0 -0
  39. {capauth-0.3.7 → capauth-0.3.8}/authentik-custom/imghdr.py +0 -0
  40. {capauth-0.3.7 → capauth-0.3.8}/authentik-custom/user_settings.py +0 -0
  41. {capauth-0.3.7 → capauth-0.3.8}/bin/cli.js +0 -0
  42. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/.gitignore +0 -0
  43. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/README.md +0 -0
  44. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/RELEASING.md +0 -0
  45. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/background.js +0 -0
  46. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/autofill.js +0 -0
  47. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/cross_origin_signing.js +0 -0
  48. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/detector.js +0 -0
  49. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/provider.js +0 -0
  50. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/content_scripts/provider_bridge.js +0 -0
  51. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon.svg +0 -0
  52. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon128.png +0 -0
  53. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon16.png +0 -0
  54. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/icons/icon48.png +0 -0
  55. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/bunker-e2e.js +0 -0
  56. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/keyqr.js +0 -0
  57. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/keyvault.js +0 -0
  58. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/openpgp.js +0 -0
  59. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/lib/signer-backends.js +0 -0
  60. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/manifest.firefox.json +0 -0
  61. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/manifest.json +0 -0
  62. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/native-host/capauth_signer.py +0 -0
  63. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/native-host/com.capauth.signer.json +0 -0
  64. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/native-host/install.sh +0 -0
  65. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/options/options.html +0 -0
  66. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/options/options.js +0 -0
  67. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/package-lock.json +0 -0
  68. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/package.json +0 -0
  69. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/popup/popup.css +0 -0
  70. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/popup/popup.html +0 -0
  71. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/popup/popup.js +0 -0
  72. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/scripts/build.js +0 -0
  73. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/scripts/generate-icons.js +0 -0
  74. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/AuthenticatorSelectionStage.js +0 -0
  75. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/CapAuthStage.js +0 -0
  76. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/QRDisplayStage.js +0 -0
  77. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/SignatureVerificationStage.js +0 -0
  78. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/stages/StagePipeline.js +0 -0
  79. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/submission/STORE_LISTING.md +0 -0
  80. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/submission/privacy-policy.md +0 -0
  81. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/fixtures/test_key.json +0 -0
  82. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/background.test.js +0 -0
  83. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/bunker_e2e.test.js +0 -0
  84. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/detector.test.js +0 -0
  85. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/keyqr.test.js +0 -0
  86. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/keyvault.test.js +0 -0
  87. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/openpgp.test.js +0 -0
  88. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/provider_api.test.js +0 -0
  89. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/provider_tierb.test.js +0 -0
  90. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/remote_bunker.test.js +0 -0
  91. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/tests/unit/signer_backends.test.js +0 -0
  92. {capauth-0.3.7 → capauth-0.3.8}/browser-extension/vitest.config.js +0 -0
  93. {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/.env.example +0 -0
  94. {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/deploy.sh +0 -0
  95. {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/docker-compose.yml +0 -0
  96. {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/systemd/capauth-backup.service +0 -0
  97. {capauth-0.3.7 → capauth-0.3.8}/deploy/capauth-service/systemd/capauth-backup.timer +0 -0
  98. {capauth-0.3.7 → capauth-0.3.8}/deploy/forgejo-capauth/.env.example +0 -0
  99. {capauth-0.3.7 → capauth-0.3.8}/deploy/forgejo-capauth/docker-compose.yml +0 -0
  100. {capauth-0.3.7 → capauth-0.3.8}/deploy/forgejo-capauth/preflight.sh +0 -0
  101. {capauth-0.3.7 → capauth-0.3.8}/docs/AIRGAPPED_SIGNING_WORKSTATION.md +0 -0
  102. {capauth-0.3.7 → capauth-0.3.8}/docs/ARCHITECTURE.md +0 -0
  103. {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_CUSTOM_STAGE.md +0 -0
  104. {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_DEPLOYMENT_SKSSO.md +0 -0
  105. {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_FORGEJO_DEPLOYMENT.md +0 -0
  106. {capauth-0.3.7 → capauth-0.3.8}/docs/AUTHENTIK_STRATEGY_DECISION.md +0 -0
  107. {capauth-0.3.7 → capauth-0.3.8}/docs/CAPAUTH_BUNKER_REMOTE_SIGNER.md +0 -0
  108. {capauth-0.3.7 → capauth-0.3.8}/docs/CAPAUTH_OIDC_IDP.md +0 -0
  109. {capauth-0.3.7 → capauth-0.3.8}/docs/CLAIMS.md +0 -0
  110. {capauth-0.3.7 → capauth-0.3.8}/docs/COLD_MACHINE_BOOTSTRAP_AND_DR.md +0 -0
  111. {capauth-0.3.7 → capauth-0.3.8}/docs/CRYPTO_SPEC.md +0 -0
  112. {capauth-0.3.7 → capauth-0.3.8}/docs/DELEGATED_CAPABILITIES.md +0 -0
  113. {capauth-0.3.7 → capauth-0.3.8}/docs/DOWNSTREAM_DURABLE_BACKENDS.md +0 -0
  114. {capauth-0.3.7 → capauth-0.3.8}/docs/ENTERPRISE_MANAGED_KEYS.md +0 -0
  115. {capauth-0.3.7 → capauth-0.3.8}/docs/HUMAN_IDENTITY_SETUP_AND_ROTATION.md +0 -0
  116. {capauth-0.3.7 → capauth-0.3.8}/docs/IDENTITY_ESTATE.md +0 -0
  117. {capauth-0.3.7 → capauth-0.3.8}/docs/INTEGRATION_BLUEPRINT.md +0 -0
  118. {capauth-0.3.7 → capauth-0.3.8}/docs/M0-capauth-api-v1.md +0 -0
  119. {capauth-0.3.7 → capauth-0.3.8}/docs/M0-capauth-importer-inventory.md +0 -0
  120. {capauth-0.3.7 → capauth-0.3.8}/docs/PQC_CAPAUTH_REVISIT.md +0 -0
  121. {capauth-0.3.7 → capauth-0.3.8}/docs/PQC_ROOT_MIGRATION.md +0 -0
  122. {capauth-0.3.7 → capauth-0.3.8}/docs/PROTOCOL.md +0 -0
  123. {capauth-0.3.7 → capauth-0.3.8}/docs/ROOT_ROTATION_CEREMONY.md +0 -0
  124. {capauth-0.3.7 → capauth-0.3.8}/docs/TRUST_GRAPH_COORD_PROJECTION.md +0 -0
  125. {capauth-0.3.7 → capauth-0.3.8}/docs/ZERO_KNOWLEDGE_PROFILE.md +0 -0
  126. {capauth-0.3.7 → capauth-0.3.8}/docs/authentik-capauth.md +0 -0
  127. {capauth-0.3.7 → capauth-0.3.8}/docs/deploy-plan/capauth-bulletproof-deploy.md +0 -0
  128. {capauth-0.3.7 → capauth-0.3.8}/index.d.ts +0 -0
  129. {capauth-0.3.7 → capauth-0.3.8}/index.js +0 -0
  130. {capauth-0.3.7 → capauth-0.3.8}/openclaw-plugin.archived-2026-04-23/package.json +0 -0
  131. {capauth-0.3.7 → capauth-0.3.8}/openclaw-plugin.archived-2026-04-23/src/index.ts +0 -0
  132. {capauth-0.3.7 → capauth-0.3.8}/openclaw-plugin.archived-2026-04-23/src/openclaw.plugin.json +0 -0
  133. {capauth-0.3.7 → capauth-0.3.8}/package.json +0 -0
  134. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/app.js +0 -0
  135. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/icons/icon-192.png +0 -0
  136. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/icons/icon-512.png +0 -0
  137. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/index.html +0 -0
  138. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/bunker-e2e.js +0 -0
  139. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/bunker-signer.js +0 -0
  140. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/canonical.js +0 -0
  141. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/keyqr.js +0 -0
  142. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/lib/keyvault.js +0 -0
  143. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/manifest.webmanifest +0 -0
  144. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/sw.js +0 -0
  145. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/vendor/openpgp.min.js +0 -0
  146. {capauth-0.3.7 → capauth-0.3.8}/phone-signer/vendor/qrcode-generator.js +0 -0
  147. {capauth-0.3.7 → capauth-0.3.8}/pyproject.toml +0 -0
  148. {capauth-0.3.7 → capauth-0.3.8}/ruff-0.15.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +0 -0
  149. {capauth-0.3.7 → capauth-0.3.8}/scripts/build_authentik_capauth_image.sh +0 -0
  150. {capauth-0.3.7 → capauth-0.3.8}/scripts/capauth-backup.sh +0 -0
  151. {capauth-0.3.7 → capauth-0.3.8}/scripts/capauth-restore.sh +0 -0
  152. {capauth-0.3.7 → capauth-0.3.8}/scripts/gitleaks-scope.sh +0 -0
  153. {capauth-0.3.7 → capauth-0.3.8}/scripts/migrate_canonical_subjects.py +0 -0
  154. {capauth-0.3.7 → capauth-0.3.8}/scripts/pqc_ceremony_dryrun.py +0 -0
  155. {capauth-0.3.7 → capauth-0.3.8}/scripts/provision_agent_profiles.py +0 -0
  156. {capauth-0.3.7 → capauth-0.3.8}/setup.cfg +0 -0
  157. {capauth-0.3.7 → capauth-0.3.8}/skill.yaml +0 -0
  158. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/agent_identity.py +0 -0
  159. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/apps.py +0 -0
  160. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/__init__.py +0 -0
  161. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/api.py +0 -0
  162. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/blueprints/capauth-pgp-login.yaml +0 -0
  163. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/claims_mapper.py +0 -0
  164. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/nonce_store.py +0 -0
  165. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/stage.py +0 -0
  166. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/urls.py +0 -0
  167. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authentik/verifier.py +0 -0
  168. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/authz.py +0 -0
  169. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/control_plane.py +0 -0
  170. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/__init__.py +0 -0
  171. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/base.py +0 -0
  172. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/gnupg_backend.py +0 -0
  173. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/pgpy_backend.py +0 -0
  174. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/sequoia_backend.py +0 -0
  175. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/crypto/skpgp_backend.py +0 -0
  176. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/custody.py +0 -0
  177. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/did.py +0 -0
  178. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/__init__.py +0 -0
  179. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/base.py +0 -0
  180. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/file_discovery.py +0 -0
  181. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/mdns.py +0 -0
  182. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/discovery/syncthing.py +0 -0
  183. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/estate.py +0 -0
  184. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/exceptions.py +0 -0
  185. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/identity.py +0 -0
  186. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/identity_class.py +0 -0
  187. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integration.py +0 -0
  188. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/__init__.py +0 -0
  189. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/__init__.py +0 -0
  190. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/auth_flow.py +0 -0
  191. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/cli.py +0 -0
  192. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/config.py +0 -0
  193. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/forgejo_api.py +0 -0
  194. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/oidc_provider.py +0 -0
  195. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/__init__.py +0 -0
  196. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_auth_flow.py +0 -0
  197. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_cli.py +0 -0
  198. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_config.py +0 -0
  199. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_forgejo_api.py +0 -0
  200. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/forgejo/tests/test_oidc_provider.py +0 -0
  201. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/.gitignore +0 -0
  202. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/.nextcloudignore +0 -0
  203. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/CSR-PR.md +0 -0
  204. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/INFO-XML-CHANGES.md +0 -0
  205. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/PUBLISHING.md +0 -0
  206. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/README.md +0 -0
  207. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/appinfo/info.xml +0 -0
  208. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/appinfo/routes.php +0 -0
  209. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/build-release.sh +0 -0
  210. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/certificates/.gitignore +0 -0
  211. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/certificates/capauth.crt +0 -0
  212. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/certificates/capauth.csr +0 -0
  213. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/composer.json +0 -0
  214. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/css/login.css +0 -0
  215. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/js/login.js +0 -0
  216. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/krankerl.toml +0 -0
  217. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/AlternativeLogin/CapAuthLogin.php +0 -0
  218. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/AppInfo/Application.php +0 -0
  219. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/BackgroundJob/PruneExpiredNonces.php +0 -0
  220. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Controller/LoginController.php +0 -0
  221. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Controller/TokenController.php +0 -0
  222. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Db/KeyRegistry.php +0 -0
  223. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Exception/CapAuthUnauthorizedException.php +0 -0
  224. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Middleware/PgpVerificationMiddleware.php +0 -0
  225. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Migration/Version000300Date20260301000000.php +0 -0
  226. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Provider/CapAuthProvider.php +0 -0
  227. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/ChallengeService.php +0 -0
  228. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/GroupSyncService.php +0 -0
  229. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/UserProvisioningService.php +0 -0
  230. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/Service/VerifierService.php +0 -0
  231. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/lib/User/Backend.php +0 -0
  232. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/phpunit.xml +0 -0
  233. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/screenshots/README.md +0 -0
  234. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/screenshots/login.png +0 -0
  235. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/templates/login.php +0 -0
  236. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/bootstrap.php +0 -0
  237. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/integration/FullAuthFlowTest.php +0 -0
  238. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/oidc/oidc_e2e_login_test.py +0 -0
  239. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Controller/LoginControllerTest.php +0 -0
  240. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Db/KeyRegistryTest.php +0 -0
  241. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Provider/CapAuthProviderTest.php +0 -0
  242. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Service/ChallengeServiceTest.php +0 -0
  243. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/Service/VerifierServiceTest.php +0 -0
  244. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/integrations/nextcloud/tests/unit/User/BackendTest.php +0 -0
  245. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/key_transparency.py +0 -0
  246. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/login.py +0 -0
  247. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/manifest.py +0 -0
  248. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/mesh.py +0 -0
  249. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/migrations/0001_initial.py +0 -0
  250. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/migrations/0002_widen_fingerprint_to_64.py +0 -0
  251. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/migrations/__init__.py +0 -0
  252. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/models.py +0 -0
  253. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/__init__.py +0 -0
  254. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/canonicalize.py +0 -0
  255. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/kernel.py +0 -0
  256. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/operator_session.py +0 -0
  257. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/proof.py +0 -0
  258. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/records.py +0 -0
  259. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/store.py +0 -0
  260. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pairing/window.py +0 -0
  261. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pma.py +0 -0
  262. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_confidentiality.py +0 -0
  263. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_identity.py +0 -0
  264. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_root_identity.py +0 -0
  265. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/pqc_tokens.py +0 -0
  266. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/profile.py +0 -0
  267. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/provisioning.py +0 -0
  268. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/registry.py +0 -0
  269. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/seal.py +0 -0
  270. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/__init__.py +0 -0
  271. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/app.py +0 -0
  272. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/bunker.py +0 -0
  273. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/bunker_e2e.py +0 -0
  274. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/keystore.py +0 -0
  275. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/__init__.py +0 -0
  276. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/clients.example.json +0 -0
  277. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/clients.py +0 -0
  278. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/passkey.py +0 -0
  279. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/provider.py +0 -0
  280. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/signing_key.py +0 -0
  281. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/oidc/store.py +0 -0
  282. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/push.py +0 -0
  283. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/service/server.py +0 -0
  284. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/subject.py +0 -0
  285. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/sync.py +0 -0
  286. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/testing.py +0 -0
  287. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/trust/__init__.py +0 -0
  288. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/trust/calibration.py +0 -0
  289. {capauth-0.3.7 → capauth-0.3.8}/src/capauth/trust/graph.py +0 -0
  290. {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/dependency_links.txt +0 -0
  291. {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/entry_points.txt +0 -0
  292. {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/requires.txt +0 -0
  293. {capauth-0.3.7 → capauth-0.3.8}/src/capauth.egg-info/top_level.txt +0 -0
  294. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/.gitignore +0 -0
  295. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/package-lock.json +0 -0
  296. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/package.json +0 -0
  297. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/src/CapAuthStage.test.js +0 -0
  298. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/src/CapAuthStage.vue +0 -0
  299. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/src/index.js +0 -0
  300. {capauth-0.3.7 → capauth-0.3.8}/stages/ak-stage-capauth/vite.config.js +0 -0
  301. {capauth-0.3.7 → capauth-0.3.8}/tests/__init__.py +0 -0
  302. {capauth-0.3.7 → capauth-0.3.8}/tests/conftest.py +0 -0
  303. {capauth-0.3.7 → capauth-0.3.8}/tests/fixtures/bunker_e2e_v1_vector.json +0 -0
  304. {capauth-0.3.7 → capauth-0.3.8}/tests/fixtures/canonical_nonce_v2_vector.json +0 -0
  305. {capauth-0.3.7 → capauth-0.3.8}/tests/test_agent_identity.py +0 -0
  306. {capauth-0.3.7 → capauth-0.3.8}/tests/test_agentrun_capabilities.py +0 -0
  307. {capauth-0.3.7 → capauth-0.3.8}/tests/test_audience_mint_helper.py +0 -0
  308. {capauth-0.3.7 → capauth-0.3.8}/tests/test_audience_tokens.py +0 -0
  309. {capauth-0.3.7 → capauth-0.3.8}/tests/test_authz.py +0 -0
  310. {capauth-0.3.7 → capauth-0.3.8}/tests/test_authz_legacy_grace.py +0 -0
  311. {capauth-0.3.7 → capauth-0.3.8}/tests/test_authz_signature_gate.py +0 -0
  312. {capauth-0.3.7 → capauth-0.3.8}/tests/test_backup_restore.py +0 -0
  313. {capauth-0.3.7 → capauth-0.3.8}/tests/test_bunker.py +0 -0
  314. {capauth-0.3.7 → capauth-0.3.8}/tests/test_bunker_e2e.py +0 -0
  315. {capauth-0.3.7 → capauth-0.3.8}/tests/test_canonicalize.py +0 -0
  316. {capauth-0.3.7 → capauth-0.3.8}/tests/test_capauth_service_deploy.py +0 -0
  317. {capauth-0.3.7 → capauth-0.3.8}/tests/test_change_capabilities.py +0 -0
  318. {capauth-0.3.7 → capauth-0.3.8}/tests/test_claims_mapper.py +0 -0
  319. {capauth-0.3.7 → capauth-0.3.8}/tests/test_control_plane.py +0 -0
  320. {capauth-0.3.7 → capauth-0.3.8}/tests/test_control_plane_capabilities.py +0 -0
  321. {capauth-0.3.7 → capauth-0.3.8}/tests/test_crypto.py +0 -0
  322. {capauth-0.3.7 → capauth-0.3.8}/tests/test_custody.py +0 -0
  323. {capauth-0.3.7 → capauth-0.3.8}/tests/test_delegated_capabilities.py +0 -0
  324. {capauth-0.3.7 → capauth-0.3.8}/tests/test_device_key_proof.py +0 -0
  325. {capauth-0.3.7 → capauth-0.3.8}/tests/test_did_capability_relationships.py +0 -0
  326. {capauth-0.3.7 → capauth-0.3.8}/tests/test_enrollment_proof_helper.py +0 -0
  327. {capauth-0.3.7 → capauth-0.3.8}/tests/test_estate.py +0 -0
  328. {capauth-0.3.7 → capauth-0.3.8}/tests/test_fingerprint_v6.py +0 -0
  329. {capauth-0.3.7 → capauth-0.3.8}/tests/test_forgejo_preflight.py +0 -0
  330. {capauth-0.3.7 → capauth-0.3.8}/tests/test_home_resolution.py +0 -0
  331. {capauth-0.3.7 → capauth-0.3.8}/tests/test_identity.py +0 -0
  332. {capauth-0.3.7 → capauth-0.3.8}/tests/test_identity_class.py +0 -0
  333. {capauth-0.3.7 → capauth-0.3.8}/tests/test_identity_ttl.py +0 -0
  334. {capauth-0.3.7 → capauth-0.3.8}/tests/test_integration.py +0 -0
  335. {capauth-0.3.7 → capauth-0.3.8}/tests/test_key_revocation_expiry.py +0 -0
  336. {capauth-0.3.7 → capauth-0.3.8}/tests/test_key_transparency.py +0 -0
  337. {capauth-0.3.7 → capauth-0.3.8}/tests/test_login.py +0 -0
  338. {capauth-0.3.7 → capauth-0.3.8}/tests/test_manifest.py +0 -0
  339. {capauth-0.3.7 → capauth-0.3.8}/tests/test_mesh.py +0 -0
  340. {capauth-0.3.7 → capauth-0.3.8}/tests/test_models_pqc_q0.py +0 -0
  341. {capauth-0.3.7 → capauth-0.3.8}/tests/test_nextcloud_app.py +0 -0
  342. {capauth-0.3.7 → capauth-0.3.8}/tests/test_no_subapp_dependency.py +0 -0
  343. {capauth-0.3.7 → capauth-0.3.8}/tests/test_nonce_store.py +0 -0
  344. {capauth-0.3.7 → capauth-0.3.8}/tests/test_oidc_idp.py +0 -0
  345. {capauth-0.3.7 → capauth-0.3.8}/tests/test_oidc_idp_e2e.py +0 -0
  346. {capauth-0.3.7 → capauth-0.3.8}/tests/test_operator_session.py +0 -0
  347. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pairing.py +0 -0
  348. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pairing_base_dir_env.py +0 -0
  349. {capauth-0.3.7 → capauth-0.3.8}/tests/test_passkey.py +0 -0
  350. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pma.py +0 -0
  351. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_confidentiality.py +0 -0
  352. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_identity.py +0 -0
  353. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_root_integration.py +0 -0
  354. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_t3_gate.py +0 -0
  355. {capauth-0.3.7 → capauth-0.3.8}/tests/test_pqc_tokens.py +0 -0
  356. {capauth-0.3.7 → capauth-0.3.8}/tests/test_profile.py +0 -0
  357. {capauth-0.3.7 → capauth-0.3.8}/tests/test_provisioning.py +0 -0
  358. {capauth-0.3.7 → capauth-0.3.8}/tests/test_push.py +0 -0
  359. {capauth-0.3.7 → capauth-0.3.8}/tests/test_py313_compat.py +0 -0
  360. {capauth-0.3.7 → capauth-0.3.8}/tests/test_qr_login.py +0 -0
  361. {capauth-0.3.7 → capauth-0.3.8}/tests/test_real_pgp_e2e.py +0 -0
  362. {capauth-0.3.7 → capauth-0.3.8}/tests/test_register_cli.py +0 -0
  363. {capauth-0.3.7 → capauth-0.3.8}/tests/test_registry.py +0 -0
  364. {capauth-0.3.7 → capauth-0.3.8}/tests/test_seal.py +0 -0
  365. {capauth-0.3.7 → capauth-0.3.8}/tests/test_sequoia_backend.py +0 -0
  366. {capauth-0.3.7 → capauth-0.3.8}/tests/test_service.py +0 -0
  367. {capauth-0.3.7 → capauth-0.3.8}/tests/test_service_authz_decide.py +0 -0
  368. {capauth-0.3.7 → capauth-0.3.8}/tests/test_service_v6_fingerprint.py +0 -0
  369. {capauth-0.3.7 → capauth-0.3.8}/tests/test_skpgp_backend.py +0 -0
  370. {capauth-0.3.7 → capauth-0.3.8}/tests/test_subject.py +0 -0
  371. {capauth-0.3.7 → capauth-0.3.8}/tests/test_sync.py +0 -0
  372. {capauth-0.3.7 → capauth-0.3.8}/tests/test_syncthing_discovery.py +0 -0
  373. {capauth-0.3.7 → capauth-0.3.8}/tests/test_testing_helper.py +0 -0
  374. {capauth-0.3.7 → capauth-0.3.8}/tests/test_token_gc.py +0 -0
  375. {capauth-0.3.7 → capauth-0.3.8}/tests/test_token_store_flag.py +0 -0
  376. {capauth-0.3.7 → capauth-0.3.8}/tests/test_tokens.py +0 -0
  377. {capauth-0.3.7 → capauth-0.3.8}/tests/test_trust_calibration.py +0 -0
  378. {capauth-0.3.7 → capauth-0.3.8}/tests/test_trust_graph.py +0 -0
  379. {capauth-0.3.7 → capauth-0.3.8}/tests/test_trust_graph_coord_audit.py +0 -0
  380. {capauth-0.3.7 → capauth-0.3.8}/tests/test_verifier.py +0 -0
  381. {capauth-0.3.7 → capauth-0.3.8}/tests/test_version_honesty.py +0 -0
  382. {capauth-0.3.7 → capauth-0.3.8}/tests/test_zk_profile.py +0 -0
  383. {capauth-0.3.7 → capauth-0.3.8}/tools/build-sq.sh +0 -0
  384. {capauth-0.3.7 → capauth-0.3.8}/urllib.error +0 -0
  385. {capauth-0.3.7 → capauth-0.3.8}/web/README.md +0 -0
  386. {capauth-0.3.7 → capauth-0.3.8}/web/stages/capauth/CapAuthStage.ts +0 -0
@@ -6,6 +6,18 @@ All notable changes to `capauth` are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ - Added an opaque two-phase control-plane currentness verifier for protected
10
+ downstream owner reads. It binds two ordered request-local checks to the exact
11
+ authorizer-issued context, preserves deny versus unavailable, detects current
12
+ issuer, principal, ancestor and leaf revocation, and expiry changes, and
13
+ releases all credential-bearing state on completion or failure (card
14
+ `c7981a0a`).
15
+
16
+ - Added a signed, non-persistent audience-token lifetime in seconds, strictly
17
+ bounded to 1 through 300 seconds. The new path rejects persistence, unsigned
18
+ issuance, and conflicting hour-based lifetimes while preserving the existing
19
+ one-hour default and explicit hour-based API.
20
+
9
21
  - Scoped pull-request and push secret scans to protected `main` plus the exact
10
22
  event commit range so unrelated abandoned refs cannot poison clean changes.
11
23
  Scheduled and manual runs retain an explicit all-ref scan, and a runnable
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: capauth
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: Capability-based Authentication — decentralized, PGP-based identity and authorization
5
5
  Author-email: smilinTux <admin@smilintux.org>
6
6
  License: GPL-3.0-or-later
@@ -103,6 +103,18 @@ domain-separated authenticated identity reference. It omits the raw owner
103
103
  decision and its provider-controlled reason text, as well as the bearer, raw
104
104
  token, signature, and capability chain.
105
105
 
106
+ Callers that must perform a protected owner read after authorization use
107
+ `authorize_with_currentness`. It returns the same sanitized result plus an
108
+ opaque request-local `ControlPlaneCurrentnessVerifier`. The caller must pass the
109
+ exact context object to `check_before_owner_read`, keep the owner result private,
110
+ then pass the same object to `check_after_owner_read` before releasing output.
111
+ The locked verifier enforces that order, consumes two distinct authorizer-bound
112
+ receipts, and returns `allow`, `deny`, or `unavailable` without policy detail.
113
+ Any wrong order, copied context, current-state failure, or close invalidates its
114
+ unused receipt and releases the privately held capability chain. Middleware must
115
+ call `close` in `finally`. This brackets but does not replace the owner system's
116
+ own atomic policy-revision read boundary.
117
+
106
118
  Every non-allow result contains only `allow`, broad truth state, broad decision
107
119
  code, and a null context. Owner reason text, resource identity, principal,
108
120
  policy revision, and credential details are not returned on denial, Unknown, or
@@ -32,6 +32,7 @@ from .control_plane import (
32
32
  from .control_plane_authorizer import (
33
33
  MAX_CONTROL_PLANE_BEARER_BYTES,
34
34
  ControlPlaneAuthorizationResultV1,
35
+ ControlPlaneCurrentnessVerifier,
35
36
  ControlPlaneDecisionAuthorizer,
36
37
  ControlPlaneInvocationV1,
37
38
  OwnerPolicyProvider,
@@ -188,6 +189,7 @@ __all__ = [
188
189
  "MAX_CONTROL_PLANE_BEARER_BYTES",
189
190
  "ClientKind",
190
191
  "ControlPlaneAuthorizationResultV1",
192
+ "ControlPlaneCurrentnessVerifier",
191
193
  "ControlPlaneBinding",
192
194
  "ControlPlaneDecision",
193
195
  "ControlPlaneDecisionAuthorizer",
@@ -1696,10 +1696,16 @@ def token() -> None:
1696
1696
  )
1697
1697
  @click.option(
1698
1698
  "--ttl-hours",
1699
- default=1,
1699
+ default=None,
1700
1700
  type=int,
1701
1701
  help="Token lifetime in hours (default: 1).",
1702
1702
  )
1703
+ @click.option(
1704
+ "--ttl-seconds",
1705
+ default=None,
1706
+ type=click.IntRange(1, 300),
1707
+ help="Non-persistent signed token lifetime in seconds (1-300).",
1708
+ )
1703
1709
  @click.option(
1704
1710
  "--no-sign",
1705
1711
  is_flag=True,
@@ -1719,7 +1725,8 @@ def token_mint_audience(
1719
1725
  agent: Optional[str],
1720
1726
  audience: str,
1721
1727
  scopes: tuple[str, ...],
1722
- ttl_hours: int,
1728
+ ttl_hours: Optional[int],
1729
+ ttl_seconds: Optional[int],
1723
1730
  no_sign: bool,
1724
1731
  export: bool,
1725
1732
  ) -> None:
@@ -1736,14 +1743,21 @@ def token_mint_audience(
1736
1743
 
1737
1744
  scope_list = list(scopes) if scopes else None
1738
1745
 
1746
+ if ttl_seconds is not None and ttl_hours is not None:
1747
+ raise click.UsageError("--ttl-seconds and --ttl-hours are mutually exclusive")
1748
+ if ttl_seconds is not None and no_sign:
1749
+ raise click.UsageError("--ttl-seconds requires signing")
1750
+
1739
1751
  try:
1740
1752
  tok = mint_agent_audience_token(
1741
1753
  agent=agent,
1742
1754
  audience=audience,
1743
1755
  scopes=scope_list,
1744
1756
  ttl_hours=ttl_hours,
1757
+ ttl_seconds=ttl_seconds,
1745
1758
  home=ctx.obj.get("home"),
1746
1759
  sign=not no_sign,
1760
+ store=ttl_seconds is None,
1747
1761
  )
1748
1762
  except (CapAuthError, ValueError) as exc:
1749
1763
  console.print(f"[bold red]Error:[/] {exc}")
@@ -4,9 +4,12 @@ from __future__ import annotations
4
4
 
5
5
  import base64
6
6
  import hashlib
7
+ import hmac
7
8
  import json
9
+ import secrets
8
10
  from collections.abc import Callable
9
11
  from datetime import datetime, timedelta, timezone
12
+ from threading import Lock
10
13
  from typing import Literal, Protocol
11
14
 
12
15
  from pydantic import Field, field_serializer, model_validator
@@ -25,6 +28,7 @@ from .control_plane import (
25
28
  validate_request_boundary,
26
29
  )
27
30
  from .delegated import (
31
+ AuthorizationCurrentnessReceipt,
28
32
  AuthorizationDecision,
29
33
  AuthorizationDeniedError,
30
34
  AuthorizationReceiptError,
@@ -42,6 +46,7 @@ from .delegated import (
42
46
  UTC = timezone.utc
43
47
  MAX_CONTROL_PLANE_BEARER_BYTES = 64 * 1024
44
48
  MAX_CONTROL_PLANE_TTL = timedelta(minutes=5)
49
+ MAX_CONTROL_PLANE_CURRENTNESS_VERIFIERS = 1024
45
50
 
46
51
 
47
52
  class ControlPlaneInvocationV1(StrictValue):
@@ -176,6 +181,190 @@ class ControlPlaneAuthorizationResultV1(StrictValue):
176
181
  return self
177
182
 
178
183
 
184
+ _CURRENTNESS_FACTORY = object()
185
+
186
+
187
+ class ControlPlaneCurrentnessVerifier:
188
+ """Opaque request-local verifier for two downstream currentness checks."""
189
+
190
+ __slots__ = (
191
+ "_authorizer",
192
+ "_context",
193
+ "_context_fingerprint",
194
+ "_issuer",
195
+ "_issue_mac",
196
+ "_issue_nonce",
197
+ "_lock",
198
+ "_owns_receipts",
199
+ "_phase",
200
+ "_presented",
201
+ "_prior",
202
+ "_receipts",
203
+ "_request",
204
+ )
205
+
206
+ def __init__(
207
+ self,
208
+ factory_token: object,
209
+ *,
210
+ issuer: "ControlPlaneDecisionAuthorizer",
211
+ issue_nonce: bytes,
212
+ issue_mac: bytes,
213
+ authorizer: CapabilityAuthorizer,
214
+ presented: PresentedCapability,
215
+ request: AuthorizationRequest,
216
+ prior: AuthorizationDecision,
217
+ context: SanitizedControlPlaneDecisionV1,
218
+ receipts: tuple[AuthorizationCurrentnessReceipt, ...],
219
+ ) -> None:
220
+ if (
221
+ factory_token is not _CURRENTNESS_FACTORY
222
+ or type(issuer) is not ControlPlaneDecisionAuthorizer
223
+ or len(issue_nonce) != 32
224
+ or len(issue_mac) != 32
225
+ or type(authorizer) is not CapabilityAuthorizer
226
+ or not isinstance(presented, PresentedCapability)
227
+ or not isinstance(request, AuthorizationRequest)
228
+ or not isinstance(prior, AuthorizationDecision)
229
+ or not isinstance(context, SanitizedControlPlaneDecisionV1)
230
+ or len(receipts) != 2
231
+ or any(not isinstance(value, AuthorizationCurrentnessReceipt) for value in receipts)
232
+ ):
233
+ raise TypeError("control-plane currentness verifiers are authorizer-issued")
234
+ object.__setattr__(self, "_authorizer", authorizer)
235
+ object.__setattr__(self, "_issuer", issuer)
236
+ object.__setattr__(self, "_issue_nonce", bytes(issue_nonce))
237
+ object.__setattr__(self, "_issue_mac", bytes(issue_mac))
238
+ object.__setattr__(self, "_presented", presented)
239
+ object.__setattr__(self, "_request", request)
240
+ object.__setattr__(self, "_prior", prior)
241
+ object.__setattr__(self, "_context", context)
242
+ object.__setattr__(self, "_context_fingerprint", _context_fingerprint(context))
243
+ object.__setattr__(self, "_receipts", tuple(receipts))
244
+ object.__setattr__(self, "_lock", Lock())
245
+ object.__setattr__(self, "_owns_receipts", False)
246
+ object.__setattr__(self, "_phase", 0)
247
+
248
+ def __setattr__(self, name, value) -> None:
249
+ del name, value
250
+ raise AttributeError("control-plane currentness verifier is immutable")
251
+
252
+ def check_before_owner_read(self, context: SanitizedControlPlaneDecisionV1) -> DecisionState:
253
+ """Consume the pre-read proof for the exact issued context."""
254
+
255
+ return self._check(context, expected_phase=0)
256
+
257
+ def check_after_owner_read(self, context: SanitizedControlPlaneDecisionV1) -> DecisionState:
258
+ """Consume the post-read proof before protected output is released."""
259
+
260
+ return self._check(context, expected_phase=1)
261
+
262
+ def _check(
263
+ self,
264
+ context: SanitizedControlPlaneDecisionV1,
265
+ *,
266
+ expected_phase: int,
267
+ ) -> DecisionState:
268
+ with self._lock:
269
+ if (
270
+ self._phase != expected_phase
271
+ or context is not self._context
272
+ or len(self._receipts) != 2
273
+ ):
274
+ self._close_locked()
275
+ return DecisionState.DENY
276
+ try:
277
+ fingerprint = _context_fingerprint(context)
278
+ except Exception:
279
+ self._close_locked()
280
+ return DecisionState.DENY
281
+ if fingerprint != self._context_fingerprint:
282
+ self._close_locked()
283
+ return DecisionState.DENY
284
+ try:
285
+ issued = self._issuer._claim_currentness_verifier(
286
+ self._issue_nonce,
287
+ self._issue_mac,
288
+ context,
289
+ verifier=self,
290
+ expected_phase=expected_phase,
291
+ )
292
+ except Exception:
293
+ self._close_locked()
294
+ return DecisionState.UNAVAILABLE
295
+ if not issued:
296
+ self._close_locked()
297
+ return DecisionState.DENY
298
+ object.__setattr__(self, "_owns_receipts", True)
299
+ receipt = self._receipts[expected_phase]
300
+ try:
301
+ current = self._authorizer.revalidate_current(
302
+ self._presented,
303
+ self._request,
304
+ self._prior,
305
+ receipt,
306
+ )
307
+ state = DecisionState.ALLOW if current is self._prior else DecisionState.DENY
308
+ except AuthorizationDeniedError as exc:
309
+ self._authorizer.discard_currentness_receipts((receipt,))
310
+ state = _capauth_denial(exc.decision.reason).state
311
+ except AuthorizationReceiptError:
312
+ self._authorizer.discard_currentness_receipts((receipt,))
313
+ state = DecisionState.DENY
314
+ except AuthorizationReceiptUnavailableError:
315
+ self._authorizer.discard_currentness_receipts((receipt,))
316
+ state = DecisionState.UNAVAILABLE
317
+ except Exception:
318
+ self._authorizer.discard_currentness_receipts((receipt,))
319
+ state = DecisionState.UNAVAILABLE
320
+ if state is not DecisionState.ALLOW or expected_phase == 1:
321
+ self._close_locked()
322
+ else:
323
+ object.__setattr__(self, "_phase", 1)
324
+ return state
325
+
326
+ def close(self) -> None:
327
+ """Invalidate unused proofs and release request credential material."""
328
+
329
+ with self._lock:
330
+ self._close_locked()
331
+
332
+ def _close_locked(self) -> None:
333
+ if self._phase == 2:
334
+ return
335
+ registered = self._issuer._discard_currentness_verifier(
336
+ self._issue_nonce,
337
+ verifier=self,
338
+ )
339
+ if self._owns_receipts or registered:
340
+ self._authorizer.discard_currentness_receipts(tuple(self._receipts))
341
+ object.__setattr__(self, "_receipts", ())
342
+ object.__setattr__(self, "_presented", None)
343
+ object.__setattr__(self, "_request", None)
344
+ object.__setattr__(self, "_prior", None)
345
+ object.__setattr__(self, "_context", None)
346
+ object.__setattr__(self, "_context_fingerprint", b"")
347
+ object.__setattr__(self, "_issue_nonce", b"")
348
+ object.__setattr__(self, "_issue_mac", b"")
349
+ object.__setattr__(self, "_owns_receipts", False)
350
+ object.__setattr__(self, "_phase", 2)
351
+
352
+ def __repr__(self) -> str:
353
+ return "<opaque control-plane currentness verifier>"
354
+
355
+ __str__ = __repr__
356
+
357
+ def __copy__(self):
358
+ raise TypeError("control-plane currentness verifier cannot be copied")
359
+
360
+ def __deepcopy__(self, memo):
361
+ del memo
362
+ raise TypeError("control-plane currentness verifier cannot be copied")
363
+
364
+ def __reduce__(self):
365
+ raise TypeError("control-plane currentness verifier cannot be serialized")
366
+
367
+
179
368
  def export_control_plane_bearer(presented: PresentedCapability) -> str:
180
369
  """Encode delegated transport in the canonical padded browser handoff form."""
181
370
 
@@ -207,7 +396,15 @@ def parse_control_plane_bearer(bearer: str) -> PresentedCapability:
207
396
  class ControlPlaneDecisionAuthorizer:
208
397
  """Compose delegated CapAuth and resource-owner policy without storing requests."""
209
398
 
210
- __slots__ = ("_allowed_origins", "_authorizer", "_clock", "_owner_policy")
399
+ __slots__ = (
400
+ "_allowed_origins",
401
+ "_authorizer",
402
+ "_clock",
403
+ "_owner_policy",
404
+ "_verifier_key",
405
+ "_verifier_lock",
406
+ "_verifier_states",
407
+ )
211
408
 
212
409
  def __init__(
213
410
  self,
@@ -225,6 +422,118 @@ class ControlPlaneDecisionAuthorizer:
225
422
  self._owner_policy = owner_policy
226
423
  self._allowed_origins = frozenset(allowed_origins)
227
424
  self._clock = clock or (lambda: datetime.now(UTC))
425
+ self._verifier_key = secrets.token_bytes(32)
426
+ self._verifier_lock = Lock()
427
+ self._verifier_states = {}
428
+
429
+ def _register_currentness_verifier(
430
+ self,
431
+ context: SanitizedControlPlaneDecisionV1,
432
+ ) -> tuple[bytes, bytes]:
433
+ now = _utc(self._clock())
434
+ fingerprint = _context_fingerprint(context)
435
+ with self._verifier_lock:
436
+ self._verifier_states = {
437
+ nonce: state for nonce, state in self._verifier_states.items() if state[0] > now
438
+ }
439
+ if len(self._verifier_states) >= MAX_CONTROL_PLANE_CURRENTNESS_VERIFIERS:
440
+ raise RuntimeError("control-plane currentness verifier capacity exhausted")
441
+ for _attempt in range(16):
442
+ nonce = secrets.token_bytes(32)
443
+ if nonce in self._verifier_states:
444
+ continue
445
+ mac = self._currentness_verifier_mac(nonce, fingerprint)
446
+ self._verifier_states[nonce] = (
447
+ context.expires_at,
448
+ 0,
449
+ id(context),
450
+ fingerprint,
451
+ mac,
452
+ None,
453
+ )
454
+ return nonce, mac
455
+ raise RuntimeError("control-plane currentness verifier nonce unavailable")
456
+
457
+ def _bind_currentness_verifier(
458
+ self,
459
+ nonce: bytes,
460
+ mac: bytes,
461
+ context: SanitizedControlPlaneDecisionV1,
462
+ verifier: ControlPlaneCurrentnessVerifier,
463
+ ) -> bool:
464
+ fingerprint = _context_fingerprint(context)
465
+ with self._verifier_lock:
466
+ state = self._verifier_states.get(nonce)
467
+ valid = (
468
+ state is not None
469
+ and state[1] == 0
470
+ and state[2] == id(context)
471
+ and state[3] == fingerprint
472
+ and hmac.compare_digest(state[4], mac)
473
+ and state[5] is None
474
+ )
475
+ if not valid:
476
+ self._verifier_states.pop(nonce, None)
477
+ return False
478
+ self._verifier_states[nonce] = (*state[:5], id(verifier))
479
+ return True
480
+
481
+ def _claim_currentness_verifier(
482
+ self,
483
+ nonce: bytes,
484
+ mac: bytes,
485
+ context: SanitizedControlPlaneDecisionV1,
486
+ *,
487
+ verifier: ControlPlaneCurrentnessVerifier,
488
+ expected_phase: int,
489
+ ) -> bool:
490
+ now = _utc(self._clock())
491
+ fingerprint = _context_fingerprint(context)
492
+ with self._verifier_lock:
493
+ state = self._verifier_states.get(nonce)
494
+ self._verifier_states = {
495
+ value: item for value, item in self._verifier_states.items() if item[0] > now
496
+ }
497
+ if state is None or state[0] <= now:
498
+ return False
499
+ valid_mac = hmac.compare_digest(
500
+ mac,
501
+ self._currentness_verifier_mac(nonce, fingerprint),
502
+ )
503
+ valid = (
504
+ valid_mac
505
+ and state[1] == expected_phase
506
+ and state[2] == id(context)
507
+ and state[3] == fingerprint
508
+ and hmac.compare_digest(state[4], mac)
509
+ and state[5] == id(verifier)
510
+ )
511
+ if not valid:
512
+ return False
513
+ if expected_phase == 0:
514
+ self._verifier_states[nonce] = (state[0], 1, *state[2:])
515
+ else:
516
+ self._verifier_states.pop(nonce, None)
517
+ return True
518
+
519
+ def _discard_currentness_verifier(
520
+ self,
521
+ nonce: bytes,
522
+ *,
523
+ verifier: ControlPlaneCurrentnessVerifier | None = None,
524
+ ) -> bool:
525
+ with self._verifier_lock:
526
+ state = self._verifier_states.get(nonce)
527
+ if verifier is not None and state is not None and state[5] != id(verifier):
528
+ return False
529
+ return self._verifier_states.pop(nonce, None) is not None
530
+
531
+ def _currentness_verifier_mac(self, nonce: bytes, fingerprint: bytes) -> bytes:
532
+ return hmac.new(
533
+ self._verifier_key,
534
+ b"capauth-control-plane-currentness-verifier/v1\0" + nonce + fingerprint,
535
+ hashlib.sha256,
536
+ ).digest()
228
537
 
229
538
  def authorize(
230
539
  self,
@@ -233,9 +542,41 @@ class ControlPlaneDecisionAuthorizer:
233
542
  ) -> ControlPlaneAuthorizationResultV1:
234
543
  """Return one sanitized typed result and never retain the bearer."""
235
544
 
545
+ result, verifier = self._authorize(bearer, invocation, issue_verifier=False)
546
+ if verifier is not None:
547
+ verifier.close()
548
+ return result
549
+
550
+ def authorize_with_currentness(
551
+ self,
552
+ bearer: str,
553
+ invocation: ControlPlaneInvocationV1,
554
+ ) -> tuple[ControlPlaneAuthorizationResultV1, ControlPlaneCurrentnessVerifier | None]:
555
+ """Return a sanitized result and two-use opaque downstream verifier."""
556
+
557
+ return self._authorize(bearer, invocation, issue_verifier=True)
558
+
559
+ def _authorize(
560
+ self,
561
+ bearer: str,
562
+ invocation: ControlPlaneInvocationV1,
563
+ *,
564
+ issue_verifier: bool,
565
+ ) -> tuple[ControlPlaneAuthorizationResultV1, ControlPlaneCurrentnessVerifier | None]:
566
+ active_receipts = ()
567
+ downstream_receipts = ()
568
+ registered_nonce = b""
569
+
570
+ def closed(
571
+ result: ControlPlaneAuthorizationResultV1,
572
+ ) -> tuple[ControlPlaneAuthorizationResultV1, None]:
573
+ self._authorizer.discard_currentness_receipts(active_receipts + downstream_receipts)
574
+ self._discard_currentness_verifier(registered_nonce)
575
+ return result, None
576
+
236
577
  denied = _result(DecisionState.DENY, DecisionCode.CAPAUTH_DENIED)
237
578
  if not isinstance(invocation, ControlPlaneInvocationV1):
238
- return denied
579
+ return closed(denied)
239
580
  try:
240
581
  presented = parse_control_plane_bearer(bearer)
241
582
  leaf_raw = presented.credentials_for_verification()[-1]
@@ -245,7 +586,7 @@ class ControlPlaneDecisionAuthorizer:
245
586
  expires_at = _utc(leaf.token.payload.expires_at)
246
587
  now = _utc(self._clock())
247
588
  except Exception:
248
- return denied
589
+ return closed(denied)
249
590
 
250
591
  boundary_code = validate_request_boundary(
251
592
  binding,
@@ -254,9 +595,9 @@ class ControlPlaneDecisionAuthorizer:
254
595
  as_of=now,
255
596
  )
256
597
  if boundary_code is not None:
257
- return _result(DecisionState.DENY, boundary_code)
598
+ return closed(_result(DecisionState.DENY, boundary_code))
258
599
  if not issued_at <= now < expires_at:
259
- return _result(DecisionState.DENY, DecisionCode.EXPIRED)
600
+ return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
260
601
 
261
602
  request = AuthorizationRequest(
262
603
  principal=binding.principal,
@@ -265,49 +606,64 @@ class ControlPlaneDecisionAuthorizer:
265
606
  )
266
607
  try:
267
608
  capauth, receipt = self._authorizer.authorize_with_receipt(presented, request)
609
+ active_receipts = (receipt,)
268
610
  except AuthorizationDeniedError as exc:
269
- return _capauth_denial(exc.decision.reason)
611
+ return closed(_capauth_denial(exc.decision.reason))
270
612
  except AuthorizationReceiptUnavailableError:
271
- return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
613
+ return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
272
614
  except Exception:
273
- return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
615
+ return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
274
616
 
275
617
  try:
276
618
  now = _utc(self._clock())
277
619
  except Exception:
278
- return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
620
+ return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
279
621
  if not issued_at <= now < expires_at:
280
- return _result(DecisionState.DENY, DecisionCode.EXPIRED)
622
+ return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
281
623
 
282
624
  first = self._owner_decision(binding, capauth)
283
625
  if first is None:
284
- return _result(DecisionState.UNAVAILABLE, DecisionCode.OWNER_POLICY_UNAVAILABLE)
626
+ return closed(
627
+ _result(DecisionState.UNAVAILABLE, DecisionCode.OWNER_POLICY_UNAVAILABLE)
628
+ )
285
629
  first_joined = join_policy_decisions(binding, capauth, first)
286
630
  if not first_joined.allow:
287
- return _result(first_joined.state, first_joined.code)
631
+ return closed(_result(first_joined.state, first_joined.code))
288
632
  try:
289
- capauth = self._authorizer.revalidate_current(presented, request, capauth, receipt)
633
+ if issue_verifier:
634
+ capauth, downstream_receipts = self._authorizer.revalidate_current_with_receipts(
635
+ presented,
636
+ request,
637
+ capauth,
638
+ receipt,
639
+ count=2,
640
+ )
641
+ else:
642
+ capauth = self._authorizer.revalidate_current(presented, request, capauth, receipt)
643
+ active_receipts = ()
290
644
  except AuthorizationReceiptError:
291
- return _result(DecisionState.DENY, DecisionCode.CAPAUTH_DENIED)
645
+ return closed(_result(DecisionState.DENY, DecisionCode.CAPAUTH_DENIED))
292
646
  except AuthorizationDeniedError as exc:
293
647
  if exc.decision.reason is DecisionReason.EXPIRED:
294
- return _result(DecisionState.DENY, DecisionCode.EXPIRED)
295
- return _capauth_denial(exc.decision.reason)
648
+ return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
649
+ return closed(_capauth_denial(exc.decision.reason))
296
650
  except Exception:
297
- return _result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE)
651
+ return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
298
652
  second = self._owner_decision(binding, capauth)
299
653
  if second is None:
300
- return _result(DecisionState.UNAVAILABLE, DecisionCode.OWNER_POLICY_UNAVAILABLE)
654
+ return closed(
655
+ _result(DecisionState.UNAVAILABLE, DecisionCode.OWNER_POLICY_UNAVAILABLE)
656
+ )
301
657
  if first != second:
302
- return _result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH)
658
+ return closed(_result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH))
303
659
  joined = join_policy_decisions(binding, capauth, second)
304
660
  if not joined.allow:
305
- return _result(joined.state, joined.code)
661
+ return closed(_result(joined.state, joined.code))
306
662
 
307
663
  try:
308
664
  now = _utc(self._clock())
309
665
  if not issued_at <= now < expires_at:
310
- return _result(DecisionState.DENY, DecisionCode.EXPIRED)
666
+ return closed(_result(DecisionState.DENY, DecisionCode.EXPIRED))
311
667
  context = SanitizedControlPlaneDecisionV1(
312
668
  binding=binding,
313
669
  boundary=invocation.boundary,
@@ -318,13 +674,40 @@ class ControlPlaneDecisionAuthorizer:
318
674
  expires_at=expires_at,
319
675
  )
320
676
  except Exception:
321
- return _result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH)
322
- return ControlPlaneAuthorizationResultV1(
677
+ return closed(_result(DecisionState.DENY, DecisionCode.BINDING_MISMATCH))
678
+ result = ControlPlaneAuthorizationResultV1(
323
679
  allow=True,
324
680
  state=DecisionState.ALLOW,
325
681
  code=DecisionCode.ALLOW,
326
682
  context=context,
327
683
  )
684
+ if not issue_verifier:
685
+ return result, None
686
+ try:
687
+ registered_nonce, issue_mac = self._register_currentness_verifier(context)
688
+ verifier = ControlPlaneCurrentnessVerifier(
689
+ _CURRENTNESS_FACTORY,
690
+ issuer=self,
691
+ issue_nonce=registered_nonce,
692
+ issue_mac=issue_mac,
693
+ authorizer=self._authorizer,
694
+ presented=presented,
695
+ request=request,
696
+ prior=capauth,
697
+ context=context,
698
+ receipts=downstream_receipts,
699
+ )
700
+ if not self._bind_currentness_verifier(
701
+ registered_nonce,
702
+ issue_mac,
703
+ context,
704
+ verifier,
705
+ ):
706
+ raise RuntimeError("control-plane currentness verifier binding failed")
707
+ registered_nonce = b""
708
+ except Exception:
709
+ return closed(_result(DecisionState.UNAVAILABLE, DecisionCode.CAPAUTH_UNAVAILABLE))
710
+ return result, verifier
328
711
 
329
712
  def _owner_decision(
330
713
  self,
@@ -425,6 +808,16 @@ def _identity_ref(binding: ControlPlaneBinding, decision: AuthorizationDecision)
425
808
  return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
426
809
 
427
810
 
811
+ def _context_fingerprint(context: SanitizedControlPlaneDecisionV1) -> bytes:
812
+ encoded = json.dumps(
813
+ context.model_dump(mode="json"),
814
+ sort_keys=True,
815
+ separators=(",", ":"),
816
+ ensure_ascii=True,
817
+ ).encode("ascii")
818
+ return hashlib.sha256(encoded).digest()
819
+
820
+
428
821
  def _utc(value: datetime | None) -> datetime:
429
822
  if value is None or value.tzinfo is None or value.utcoffset() != timedelta(0):
430
823
  raise ValueError("time must use UTC offset zero")
@@ -449,6 +842,7 @@ def _result(
449
842
  __all__ = [
450
843
  "MAX_CONTROL_PLANE_BEARER_BYTES",
451
844
  "ControlPlaneAuthorizationResultV1",
845
+ "ControlPlaneCurrentnessVerifier",
452
846
  "ControlPlaneDecisionAuthorizer",
453
847
  "ControlPlaneInvocationV1",
454
848
  "OwnerPolicyProvider",