personal-agent-protocol 0.3.2__tar.gz → 0.3.3__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 (192) hide show
  1. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/PKG-INFO +1 -1
  2. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/package.json +1 -1
  3. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__init__.py +12 -0
  4. personal_agent_protocol-0.3.3/personal_agent_protocol/__tests__/test_device.py +229 -0
  5. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__version__.py +1 -1
  6. personal_agent_protocol-0.3.3/personal_agent_protocol/device.py +404 -0
  7. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/reactor.py +1 -1
  8. personal_agent_protocol-0.3.3/src/__tests__/device.test.mjs +145 -0
  9. personal_agent_protocol-0.3.3/src/device.ts +435 -0
  10. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/index.ts +8 -0
  11. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/reactor.ts +1 -1
  12. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/build.yaml +0 -0
  13. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/docs.yaml +0 -0
  14. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/py-code-style.yaml +0 -0
  15. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/py-tests.yaml +0 -0
  16. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/py-typing.yaml +0 -0
  17. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/release.yaml +0 -0
  18. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/reusable-python.yaml +0 -0
  19. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/typescript.yaml +0 -0
  20. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.gitignore +0 -0
  21. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.licenserc.yaml +0 -0
  22. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.pre-commit-config.yaml +0 -0
  23. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.vscode/settings.json +0 -0
  24. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/CHANGELOG.md +0 -0
  25. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/Dockerfile +0 -0
  26. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/LICENSE +0 -0
  27. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/Makefile +0 -0
  28. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/README.md +0 -0
  29. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/RELEASE.md +0 -0
  30. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/.gitignore +0 -0
  31. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/LICENSE +0 -0
  32. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/Makefile +0 -0
  33. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/README.md +0 -0
  34. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/concepts/architecture.mdx +0 -0
  35. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/concepts/glossary.mdx +0 -0
  36. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/concepts/protocol-tour.mdx +0 -0
  37. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/contributing/sdk-design.mdx +0 -0
  38. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/agent-identity.mdx +0 -0
  39. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/browser-session.mdx +0 -0
  40. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/company-discovery.mdx +0 -0
  41. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/conversation-events.mdx +0 -0
  42. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/conversation-messages.mdx +0 -0
  43. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/direct-sign-in-boundary.mdx +0 -0
  44. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/direct-sign-in-lifecycle.mdx +0 -0
  45. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/dpop-proof.mdx +0 -0
  46. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/dpop-transport.mdx +0 -0
  47. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/index.mdx +0 -0
  48. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/session-assertion.mdx +0 -0
  49. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/session-lifecycle.mdx +0 -0
  50. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/getting-started/index.mdx +0 -0
  51. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/agent-runtimes.mdx +0 -0
  52. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/cli.mdx +0 -0
  53. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/company.mdx +0 -0
  54. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/conformance.mdx +0 -0
  55. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/cryptography.mdx +0 -0
  56. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/direct-sign-in.mdx +0 -0
  57. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/extensions.mdx +0 -0
  58. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/personal-agent.mdx +0 -0
  59. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/pydantic-ai.mdx +0 -0
  60. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/sessions.mdx +0 -0
  61. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/index.mdx +0 -0
  62. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/protocol/coverage.mdx +0 -0
  63. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/protocol/open-topics.mdx +0 -0
  64. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/protocol/status.mdx +0 -0
  65. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/python-api.mdx +0 -0
  66. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/python.mdx +0 -0
  67. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/typescript-api.mdx +0 -0
  68. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/typescript.mdx +0 -0
  69. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/security/index.mdx +0 -0
  70. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/studio/integration.mdx +0 -0
  71. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docusaurus.config.js +0 -0
  72. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/package.json +0 -0
  73. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/sidebars.js +0 -0
  74. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/src/css/custom.css +0 -0
  75. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/src/pages/index.module.css +0 -0
  76. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/bricks.svg +0 -0
  77. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/building-construction.svg +0 -0
  78. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/cloud-sun.svg +0 -0
  79. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/cloud.svg +0 -0
  80. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/collaboration.svg +0 -0
  81. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/copy-left.svg +0 -0
  82. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/logo.png +0 -0
  83. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/logo.svg +0 -0
  84. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/personas/business.svg +0 -0
  85. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
  86. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/personas/devops.svg +0 -0
  87. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/project/project-line.svg +0 -0
  88. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/favicon.ico +0 -0
  89. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/jupyter.svg +0 -0
  90. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/open-source.svg +0 -0
  91. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/react-js.svg +0 -0
  92. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/react-js2.svg +0 -0
  93. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/ringed-planet.svg +0 -0
  94. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/robot.svg +0 -0
  95. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/rocket.svg +0 -0
  96. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/shield.svg +0 -0
  97. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/sparkles.svg +0 -0
  98. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/streamlit.gif +0 -0
  99. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/super-hero.svg +0 -0
  100. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/target.svg +0 -0
  101. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/thumbs-up.svg +0 -0
  102. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/README.md +0 -0
  103. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/README.md +0 -0
  104. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/agent-identity/README.md +0 -0
  105. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/agent-identity/app.py +0 -0
  106. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/browser-session/README.md +0 -0
  107. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/browser-session/app.py +0 -0
  108. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/company-discovery/README.md +0 -0
  109. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/company-discovery/app.py +0 -0
  110. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-events/README.md +0 -0
  111. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-events/app.py +0 -0
  112. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-messages/README.md +0 -0
  113. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-messages/app.py +0 -0
  114. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-boundary/README.md +0 -0
  115. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-boundary/app.py +0 -0
  116. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-lifecycle/README.md +0 -0
  117. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-lifecycle/app.py +0 -0
  118. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-proof/README.md +0 -0
  119. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-proof/app.py +0 -0
  120. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-transport/README.md +0 -0
  121. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-transport/app.py +0 -0
  122. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-assertion/README.md +0 -0
  123. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-assertion/app.py +0 -0
  124. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-lifecycle/README.md +0 -0
  125. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-lifecycle/app.py +0 -0
  126. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/__init__.py +0 -0
  127. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/conftest.py +0 -0
  128. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_auth.py +0 -0
  129. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_browser.py +0 -0
  130. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_cli.py +0 -0
  131. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_conversations.py +0 -0
  132. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_crypto.py +0 -0
  133. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_discovery.py +0 -0
  134. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_identity.py +0 -0
  135. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_models.py +0 -0
  136. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_pairwise.py +0 -0
  137. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_reactor.py +0 -0
  138. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_sessions.py +0 -0
  139. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_transport.py +0 -0
  140. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/auth.py +0 -0
  141. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/browser.py +0 -0
  142. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/cli.py +0 -0
  143. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/cli_plugin.py +0 -0
  144. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/config.py +0 -0
  145. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/conversations.py +0 -0
  146. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/crypto.py +0 -0
  147. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/discovery.py +0 -0
  148. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/errors.py +0 -0
  149. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/identity.py +0 -0
  150. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/json.py +0 -0
  151. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/models.py +0 -0
  152. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/pairwise.py +0 -0
  153. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/py.typed +0 -0
  154. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/sessions.py +0 -0
  155. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/transport.py +0 -0
  156. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/UPSTREAM.md +0 -0
  157. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/client-metadata-valid.json +0 -0
  158. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/conversation-events-valid.json +0 -0
  159. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/discovery-valid.json +0 -0
  160. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/jwks-valid.json +0 -0
  161. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/oauth-metadata-valid.json +0 -0
  162. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/operation-valid.json +0 -0
  163. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/session-token-valid.json +0 -0
  164. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/requirements.yaml +0 -0
  165. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/pyproject.toml +0 -0
  166. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/scripts/generate_api_reference.py +0 -0
  167. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/scripts/validate_pydantic_ai_examples.py +0 -0
  168. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/auth.test.mjs +0 -0
  169. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/browser.test.mjs +0 -0
  170. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/conversations.test.mjs +0 -0
  171. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/crypto.test.mjs +0 -0
  172. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/discovery.test.mjs +0 -0
  173. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/identity.test.mjs +0 -0
  174. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/reactor.test.mjs +0 -0
  175. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/server.test.mjs +0 -0
  176. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/sessions.test.mjs +0 -0
  177. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/transport.test.mjs +0 -0
  178. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/auth.ts +0 -0
  179. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/browser-server.ts +0 -0
  180. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/browser.ts +0 -0
  181. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/conversations.ts +0 -0
  182. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/crypto.ts +0 -0
  183. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/discovery.ts +0 -0
  184. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/errors.ts +0 -0
  185. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/identity.ts +0 -0
  186. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/json.ts +0 -0
  187. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/models.ts +0 -0
  188. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/secret.ts +0 -0
  189. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/server.ts +0 -0
  190. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/sessions.ts +0 -0
  191. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/transport.ts +0 -0
  192. {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: personal-agent-protocol
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Python SDK for Personal Agent Protocol
5
5
  Project-URL: Home, https://github.com/datalayer/personal-agent-protocol
6
6
  Project-URL: Documentation, https://personalagentprotocol.org/docs
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/personal-agent-protocol",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "TypeScript SDK for Personal Agent Protocol",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,6 +41,13 @@ from .crypto import (
41
41
  VerifiedDpopProof,
42
42
  verify_dpop_proof,
43
43
  )
44
+ from .device import (
45
+ DEVICE_CODE_GRANT,
46
+ DeviceAuthorizationResponse,
47
+ DeviceSignInClient,
48
+ DeviceSignInPoll,
49
+ DeviceSignInStart,
50
+ )
44
51
  from .discovery import DiscoveryClient
45
52
  from .errors import (
46
53
  AuthorizationError,
@@ -124,6 +131,7 @@ __all__ = [
124
131
  "BROWSER_HANDOFFS",
125
132
  "CACHES",
126
133
  "CLOCKS",
134
+ "DEVICE_CODE_GRANT",
127
135
  "DISCOVERY_TRANSPORTS",
128
136
  "DPOP_PROOF_PROVIDERS",
129
137
  "EXTENSION_HANDLERS",
@@ -156,6 +164,10 @@ __all__ = [
156
164
  "ConversationReceipt",
157
165
  "ConversationResponse",
158
166
  "CursorExpiredHandler",
167
+ "DeviceAuthorizationResponse",
168
+ "DeviceSignInClient",
169
+ "DeviceSignInPoll",
170
+ "DeviceSignInStart",
159
171
  "DirectSignInCallback",
160
172
  "DirectSignInClient",
161
173
  "DirectSignInRequest",
@@ -0,0 +1,229 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ import asyncio
6
+ from datetime import datetime, timedelta, timezone
7
+ from urllib.parse import parse_qsl
8
+
9
+ import httpx
10
+ import pytest
11
+ from pydantic import SecretStr
12
+ from reactor import PluginManifest # type: ignore[import-untyped]
13
+
14
+ from personal_agent_protocol import (
15
+ AUTHORIZATION_STATE_STORES,
16
+ CLOCKS,
17
+ DPOP_PROOF_PROVIDERS,
18
+ SIGNERS,
19
+ TOKEN_STORES,
20
+ AuthorizationError,
21
+ DeviceSignInClient,
22
+ DpopProofRequest,
23
+ )
24
+ from personal_agent_protocol.reactor import build_pap_reactor
25
+
26
+
27
+ class MutableClock:
28
+ def __init__(self) -> None:
29
+ self.value = datetime(2026, 10, 10, tzinfo=timezone.utc)
30
+
31
+ def now(self) -> datetime:
32
+ return self.value
33
+
34
+ def advance(self, seconds: int) -> None:
35
+ self.value += timedelta(seconds=seconds)
36
+
37
+
38
+ class MemoryState:
39
+ def __init__(self) -> None:
40
+ self.values: dict[str, object] = {}
41
+
42
+ async def put(self, reference: str, value: object, *, expires_at: datetime) -> None:
43
+ assert expires_at.tzinfo is not None
44
+ self.values[reference] = value
45
+
46
+ async def take(self, reference: str) -> object | None:
47
+ return self.values.pop(reference, None)
48
+
49
+
50
+ class MemoryTokens:
51
+ def __init__(self) -> None:
52
+ self.values: dict[str, SecretStr] = {}
53
+
54
+ async def put(self, reference: str, token: SecretStr) -> None:
55
+ self.values[reference] = token
56
+
57
+ async def get(self, reference: str) -> SecretStr | None:
58
+ return self.values.get(reference)
59
+
60
+ async def delete(self, reference: str) -> None:
61
+ self.values.pop(reference, None)
62
+
63
+
64
+ class Signer:
65
+ async def sign(self, _: dict[str, object], *, purpose: str) -> SecretStr:
66
+ assert purpose == "client_assertion"
67
+ return SecretStr("signed-client-assertion")
68
+
69
+
70
+ class Dpop:
71
+ def __init__(self) -> None:
72
+ self.requests: list[DpopProofRequest] = []
73
+
74
+ async def create(self, request: DpopProofRequest) -> SecretStr:
75
+ self.requests.append(request)
76
+ return SecretStr("proof")
77
+
78
+
79
+ class DevicePlugin:
80
+ def __init__(self) -> None:
81
+ self.clock = MutableClock()
82
+ self.state = MemoryState()
83
+ self.tokens = MemoryTokens()
84
+ self.dpop = Dpop()
85
+
86
+ def provide_contributions(self, contributions: object) -> None:
87
+ contribute = contributions.contribute # type: ignore[attr-defined]
88
+ contribute(CLOCKS, self.clock, contribution_id="mutable")
89
+ contribute(AUTHORIZATION_STATE_STORES, self.state, contribution_id="memory")
90
+ contribute(TOKEN_STORES, self.tokens, contribution_id="memory")
91
+ contribute(SIGNERS, Signer(), contribution_id="signer")
92
+ contribute(DPOP_PROOF_PROVIDERS, self.dpop, contribution_id="dpop")
93
+
94
+
95
+ def test_device_sign_in_paces_polling_and_protects_tokens() -> None:
96
+ async def scenario() -> None:
97
+ calls = 0
98
+ forms: list[dict[str, str]] = []
99
+
100
+ def handler(request: httpx.Request) -> httpx.Response:
101
+ nonlocal calls
102
+ calls += 1
103
+ form = dict(parse_qsl(request.content.decode()))
104
+ forms.append(form)
105
+ if calls == 1:
106
+ assert "DPoP" not in request.headers
107
+ return httpx.Response(
108
+ 200,
109
+ json={
110
+ "device_code": "private-device-code",
111
+ "user_code": "WDJB-MJHT",
112
+ "verification_uri": "https://company.example/device",
113
+ "verification_uri_complete": "https://company.example/device?user_code=WDJB-MJHT",
114
+ "expires_in": 600,
115
+ "interval": 5,
116
+ },
117
+ )
118
+ assert request.headers["DPoP"] == "proof"
119
+ if calls == 2:
120
+ return httpx.Response(400, json={"error": "authorization_pending"})
121
+ if calls == 3:
122
+ return httpx.Response(400, json={"error": "slow_down"})
123
+ return httpx.Response(
124
+ 200,
125
+ json={
126
+ "access_token": "signed-in-session-token",
127
+ "token_type": "DPoP",
128
+ "expires_in": 300,
129
+ "scope": "poppy:read",
130
+ "session_id": "ses_existing",
131
+ "signed_in": True,
132
+ "refresh_token": "private-account-token",
133
+ },
134
+ )
135
+
136
+ async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
137
+ reactor, _ = build_pap_reactor(http_client=http)
138
+ plugin = DevicePlugin()
139
+ reactor.register_plugin(PluginManifest(name="pap-test-device", version="1"), plugin)
140
+ client = DeviceSignInClient(reactor=reactor, http_client=http)
141
+ started = await client.begin(
142
+ device_authorization_endpoint="https://auth.example.com/oauth/device",
143
+ token_endpoint="https://auth.example.com/oauth/token", # noqa: S106
144
+ issuer="https://auth.example.com",
145
+ client_id="https://agent.example/agent.json",
146
+ scopes=("poppy:read", "poppy:write"),
147
+ tenant_id="application-one",
148
+ pairwise_user_id="usr_AAECAwQFBgcICQoLDA0ODw",
149
+ session_id="ses_existing",
150
+ )
151
+ assert started.user_code == "WDJB-MJHT"
152
+ assert "private-device-code" not in started.model_dump_json()
153
+ assert calls == 1
154
+
155
+ early = await client.poll(started.reference)
156
+ assert early.status == "pending"
157
+ assert calls == 1
158
+ plugin.clock.advance(5)
159
+ pending = await client.poll(started.reference)
160
+ assert pending.retry_at == plugin.clock.now() + timedelta(seconds=5)
161
+ plugin.clock.advance(5)
162
+ slowed = await client.poll(started.reference)
163
+ assert slowed.retry_at == plugin.clock.now() + timedelta(seconds=10)
164
+ plugin.clock.advance(10)
165
+ complete = await client.poll(started.reference)
166
+ reactor.stop()
167
+
168
+ assert complete.status == "complete"
169
+ assert complete.granted_scopes == ("poppy:read",)
170
+ assert complete.missing_scopes == ("poppy:write",)
171
+ assert complete.token is not None and complete.token.refresh_token is None
172
+ assert complete.account_token_reference is not None
173
+ stored = plugin.tokens.values[complete.account_token_reference]
174
+ assert stored.get_secret_value() == "private-account-token"
175
+ assert forms[0]["scope"] == "poppy:read poppy:write"
176
+ assert all(form.get("device_code") == "private-device-code" for form in forms[1:])
177
+ assert len(plugin.dpop.requests) == 3
178
+
179
+ asyncio.run(scenario())
180
+
181
+
182
+ def test_device_sign_in_denial_is_terminal() -> None:
183
+ async def scenario() -> None:
184
+ def handler(request: httpx.Request) -> httpx.Response:
185
+ if request.url.path.endswith("/device"):
186
+ return httpx.Response(
187
+ 200,
188
+ json={
189
+ "device_code": "private-device-code",
190
+ "user_code": "ABCD-EFGH",
191
+ "verification_uri": "https://company.example/device",
192
+ "expires_in": 60,
193
+ "interval": 1,
194
+ },
195
+ )
196
+ return httpx.Response(
197
+ 400,
198
+ json={"error": "access_denied", "error_description": "private detail"},
199
+ )
200
+
201
+ async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
202
+ reactor, _ = build_pap_reactor(http_client=http)
203
+ plugin = DevicePlugin()
204
+ reactor.register_plugin(
205
+ PluginManifest(name="pap-test-device-denial", version="1"), plugin
206
+ )
207
+ client = DeviceSignInClient(reactor=reactor, http_client=http)
208
+ started = await client.begin(
209
+ device_authorization_endpoint="https://auth.example.com/oauth/device",
210
+ token_endpoint="https://auth.example.com/oauth/token", # noqa: S106
211
+ issuer="https://auth.example.com",
212
+ client_id="https://agent.example/agent.json",
213
+ scopes=("poppy:read",),
214
+ tenant_id="application-one",
215
+ pairwise_user_id="usr_AAECAwQFBgcICQoLDA0ODw",
216
+ session_id="ses_existing",
217
+ )
218
+ plugin.clock.advance(1)
219
+ with pytest.raises(AuthorizationError) as caught:
220
+ await client.poll(started.reference)
221
+ with pytest.raises(AuthorizationError) as reused:
222
+ await client.poll(started.reference)
223
+ reactor.stop()
224
+
225
+ assert caught.value.code == "access_denied"
226
+ assert "private detail" not in str(caught.value)
227
+ assert reused.value.code == "invalid_or_expired_device_session"
228
+
229
+ asyncio.run(scenario())
@@ -3,4 +3,4 @@
3
3
 
4
4
  """Personal Agent Protocol."""
5
5
 
6
- __version__ = "0.3.2"
6
+ __version__ = "0.3.3"
@@ -0,0 +1,404 @@
1
+ # Copyright (c) 2025-2026 Datalayer, Inc.
2
+ #
3
+ # BSD 3-Clause License
4
+
5
+ """PAP Device Sign-In with private polling state and proof-bound tokens."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, replace
10
+ from datetime import datetime, timedelta
11
+ from typing import Literal, cast
12
+
13
+ import httpx
14
+ from pydantic import Field, SecretStr, field_validator
15
+ from reactor import PluginPlatform # type: ignore[import-untyped]
16
+
17
+ from .auth import _https_url, _scopes
18
+ from .errors import AuthorizationError, ProtocolValidationError
19
+ from .json import loads_strict
20
+ from .models import ProtocolModel
21
+ from .reactor import (
22
+ AUTHORIZATION_STATE_STORES,
23
+ CLOCKS,
24
+ DPOP_PROOF_PROVIDERS,
25
+ RANDOM_SOURCES,
26
+ SIGNERS,
27
+ TOKEN_STORES,
28
+ AuthorizationStateStore,
29
+ Clock,
30
+ DpopProofProvider,
31
+ DpopProofRequest,
32
+ RandomSource,
33
+ Signer,
34
+ TokenStore,
35
+ first_contribution,
36
+ )
37
+ from .sessions import JWT_CLIENT_ASSERTION, ClientAssertionClaims, SessionToken, _opaque_id
38
+
39
+ DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
40
+
41
+
42
+ class DeviceAuthorizationResponse(ProtocolModel):
43
+ """Validated response from the OAuth device authorization endpoint."""
44
+
45
+ device_code: SecretStr
46
+ user_code: str = Field(min_length=1, max_length=256)
47
+ verification_uri: str
48
+ verification_uri_complete: str | None = None
49
+ expires_in: int = Field(gt=0, le=3600)
50
+ interval: int = Field(default=5, gt=0, le=300)
51
+
52
+ @field_validator("verification_uri", "verification_uri_complete")
53
+ @classmethod
54
+ def validate_verification_url(cls, value: str | None) -> str | None:
55
+ return None if value is None else _https_url(value, "verification URI")
56
+
57
+
58
+ class DeviceSignInStart(ProtocolModel):
59
+ """User-facing device instructions without the private device code."""
60
+
61
+ reference: str = Field(pattern=r"^device_[A-Za-z0-9_-]{22,}$")
62
+ user_code: str
63
+ verification_uri: str
64
+ verification_uri_complete: str | None = None
65
+ expires_at: datetime
66
+ poll_interval: int
67
+ requested_scopes: tuple[str, ...]
68
+
69
+
70
+ class DeviceSignInPoll(ProtocolModel):
71
+ """One poll result; pending responses tell callers when to try again."""
72
+
73
+ status: Literal["pending", "complete"]
74
+ retry_at: datetime | None = None
75
+ token: SessionToken | None = None
76
+ account_token_reference: str | None = None
77
+ granted_scopes: tuple[str, ...] = ()
78
+ missing_scopes: tuple[str, ...] = ()
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class _PendingDeviceSignIn:
83
+ device_code: SecretStr
84
+ token_endpoint: str
85
+ issuer: str
86
+ client_id: str
87
+ tenant_id: str
88
+ pairwise_user_id: str
89
+ session_id: str
90
+ scopes: tuple[str, ...]
91
+ expires_at: datetime
92
+ next_poll_at: datetime
93
+ interval: int
94
+
95
+
96
+ class DeviceSignInClient:
97
+ """Start and poll PAP Device Sign-In without exposing the device code."""
98
+
99
+ def __init__(
100
+ self,
101
+ *,
102
+ reactor: PluginPlatform,
103
+ http_client: httpx.AsyncClient,
104
+ max_response_bytes: int = 100 * 1024,
105
+ ) -> None:
106
+ if max_response_bytes <= 0:
107
+ raise ValueError("max_response_bytes must be positive")
108
+ self.reactor = reactor
109
+ self.http = http_client
110
+ self.max_response_bytes = max_response_bytes
111
+
112
+ async def begin(
113
+ self,
114
+ *,
115
+ device_authorization_endpoint: str,
116
+ token_endpoint: str,
117
+ issuer: str,
118
+ client_id: str,
119
+ scopes: tuple[str, ...],
120
+ tenant_id: str,
121
+ pairwise_user_id: str,
122
+ session_id: str,
123
+ ) -> DeviceSignInStart:
124
+ """Request a device code and retain it behind an opaque state reference."""
125
+
126
+ clock, random_source, signer = self._proof_inputs()
127
+ now = clock.now()
128
+ endpoint = _https_url(device_authorization_endpoint, "device authorization endpoint")
129
+ token_url = _https_url(token_endpoint, "token endpoint")
130
+ client_url = _https_url(client_id, "client ID")
131
+ issuer_url = _https_url(issuer, "issuer")
132
+ requested_scopes = _scopes(scopes)
133
+ self._validate_context(tenant_id, pairwise_user_id, session_id)
134
+ assertion = await signer.sign(
135
+ self._client_assertion(client_url, endpoint, now, random_source).model_dump(),
136
+ purpose="client_assertion",
137
+ )
138
+ try:
139
+ response = await self.http.post(
140
+ endpoint,
141
+ data={
142
+ "scope": " ".join(requested_scopes),
143
+ "client_id": client_url,
144
+ "client_assertion_type": JWT_CLIENT_ASSERTION,
145
+ "client_assertion": assertion.get_secret_value(),
146
+ },
147
+ headers={"Accept": "application/json"},
148
+ follow_redirects=False,
149
+ )
150
+ except httpx.HTTPError as exc:
151
+ raise AuthorizationError(
152
+ "request_failed", "Device authorization request failed"
153
+ ) from exc
154
+ payload = self._decode(response, "device authorization")
155
+ if not response.is_success:
156
+ raise self._remote_error(response, payload, "Company refused Device Sign-In")
157
+ try:
158
+ authorization = DeviceAuthorizationResponse.model_validate(payload)
159
+ except ValueError as exc:
160
+ raise AuthorizationError(
161
+ "invalid_device_response",
162
+ "Device authorization response is malformed",
163
+ status_code=response.status_code,
164
+ ) from exc
165
+ expires_at = now + timedelta(seconds=authorization.expires_in)
166
+ reference = _opaque_id("device", random_source.bytes)
167
+ pending = _PendingDeviceSignIn(
168
+ device_code=authorization.device_code,
169
+ token_endpoint=token_url,
170
+ issuer=issuer_url,
171
+ client_id=client_url,
172
+ tenant_id=tenant_id,
173
+ pairwise_user_id=pairwise_user_id,
174
+ session_id=session_id,
175
+ scopes=requested_scopes,
176
+ expires_at=expires_at,
177
+ next_poll_at=now + timedelta(seconds=authorization.interval),
178
+ interval=authorization.interval,
179
+ )
180
+ store = self._state_store()
181
+ await store.put(reference, pending, expires_at=expires_at)
182
+ return DeviceSignInStart(
183
+ reference=reference,
184
+ user_code=authorization.user_code,
185
+ verification_uri=authorization.verification_uri,
186
+ verification_uri_complete=authorization.verification_uri_complete,
187
+ expires_at=expires_at,
188
+ poll_interval=authorization.interval,
189
+ requested_scopes=requested_scopes,
190
+ )
191
+
192
+ async def poll(self, reference: str) -> DeviceSignInPoll:
193
+ """Perform at most one protocol poll and enforce RFC 8628 pacing."""
194
+
195
+ store = self._state_store()
196
+ pending = await store.take(reference)
197
+ if not isinstance(pending, _PendingDeviceSignIn):
198
+ raise AuthorizationError(
199
+ "invalid_or_expired_device_session",
200
+ "Device Sign-In is unknown, expired, complete, or already being polled",
201
+ )
202
+ clock, random_source, signer = self._proof_inputs()
203
+ now = clock.now()
204
+ if now >= pending.expires_at:
205
+ raise AuthorizationError("expired_token", "Device Sign-In has expired")
206
+ if now < pending.next_poll_at:
207
+ await store.put(reference, pending, expires_at=pending.expires_at)
208
+ return DeviceSignInPoll(status="pending", retry_at=pending.next_poll_at)
209
+
210
+ try:
211
+ response, payload = await self._poll_token(pending, now, random_source, signer)
212
+ except Exception:
213
+ await store.put(reference, pending, expires_at=pending.expires_at)
214
+ raise
215
+ if response.is_success:
216
+ return await self._complete(response, payload, pending, random_source)
217
+
218
+ error = payload.get("error") if isinstance(payload, dict) else None
219
+ if error in {"authorization_pending", "slow_down"}:
220
+ interval = pending.interval + (5 if error == "slow_down" else 0)
221
+ resumed = replace(
222
+ pending,
223
+ interval=interval,
224
+ next_poll_at=now + timedelta(seconds=interval),
225
+ )
226
+ await store.put(reference, resumed, expires_at=pending.expires_at)
227
+ return DeviceSignInPoll(status="pending", retry_at=resumed.next_poll_at)
228
+ if error in {"access_denied", "expired_token"}:
229
+ raise AuthorizationError(error, "Device Sign-In ended without authorization")
230
+ await store.put(reference, pending, expires_at=pending.expires_at)
231
+ raise self._remote_error(response, payload, "Company refused the device token request")
232
+
233
+ async def _poll_token(
234
+ self,
235
+ pending: _PendingDeviceSignIn,
236
+ now: datetime,
237
+ random_source: RandomSource,
238
+ signer: Signer,
239
+ ) -> tuple[httpx.Response, object]:
240
+ assertion = await signer.sign(
241
+ self._client_assertion(
242
+ pending.client_id, pending.token_endpoint, now, random_source
243
+ ).model_dump(),
244
+ purpose="client_assertion",
245
+ )
246
+ dpop = cast(
247
+ DpopProofProvider,
248
+ first_contribution(self.reactor, DPOP_PROOF_PROVIDERS, "DPoP proof provider"),
249
+ )
250
+ proof = await dpop.create(
251
+ DpopProofRequest(
252
+ method="POST",
253
+ url=pending.token_endpoint,
254
+ tenant_id=pending.tenant_id,
255
+ subject=pending.pairwise_user_id,
256
+ issuer=pending.issuer,
257
+ )
258
+ )
259
+ try:
260
+ response = await self.http.post(
261
+ pending.token_endpoint,
262
+ data={
263
+ "grant_type": DEVICE_CODE_GRANT,
264
+ "device_code": pending.device_code.get_secret_value(),
265
+ "session_id": pending.session_id,
266
+ "client_id": pending.client_id,
267
+ "client_assertion_type": JWT_CLIENT_ASSERTION,
268
+ "client_assertion": assertion.get_secret_value(),
269
+ },
270
+ headers={"Accept": "application/json", "DPoP": proof.get_secret_value()},
271
+ follow_redirects=False,
272
+ )
273
+ except httpx.HTTPError as exc:
274
+ raise AuthorizationError("request_failed", "Device token request failed") from exc
275
+ return response, self._decode(response, "device token")
276
+
277
+ async def _complete(
278
+ self,
279
+ response: httpx.Response,
280
+ payload: object,
281
+ pending: _PendingDeviceSignIn,
282
+ random_source: RandomSource,
283
+ ) -> DeviceSignInPoll:
284
+ try:
285
+ token = SessionToken.model_validate(payload)
286
+ except ValueError as exc:
287
+ raise AuthorizationError(
288
+ "invalid_token_response",
289
+ "Device Sign-In token response is malformed",
290
+ status_code=response.status_code,
291
+ ) from exc
292
+ if (
293
+ token.token_type != "DPoP" # noqa: S105 - OAuth token type, not a credential
294
+ or not token.signed_in
295
+ or token.session_id != pending.session_id
296
+ ):
297
+ raise AuthorizationError(
298
+ "invalid_token_response",
299
+ "Device Sign-In must return the signed-in proof-bound Session",
300
+ status_code=response.status_code,
301
+ )
302
+ if any(scope not in pending.scopes for scope in token.scopes):
303
+ raise AuthorizationError(
304
+ "scope_escalation",
305
+ "Company returned a scope that was not requested",
306
+ status_code=response.status_code,
307
+ )
308
+ if token.refresh_token is None:
309
+ raise AuthorizationError(
310
+ "missing_account_token",
311
+ "Device Sign-In did not return an Account Token",
312
+ status_code=response.status_code,
313
+ )
314
+ token_reference = _opaque_id("account", random_source.bytes)
315
+ token_store = cast(
316
+ TokenStore,
317
+ first_contribution(self.reactor, TOKEN_STORES, "token store"),
318
+ )
319
+ await token_store.put(token_reference, token.refresh_token)
320
+ public_token = token.model_copy(update={"refresh_token": None})
321
+ return DeviceSignInPoll(
322
+ status="complete",
323
+ token=public_token,
324
+ account_token_reference=token_reference,
325
+ granted_scopes=public_token.scopes,
326
+ missing_scopes=tuple(
327
+ scope for scope in pending.scopes if scope not in public_token.scopes
328
+ ),
329
+ )
330
+
331
+ def _proof_inputs(self) -> tuple[Clock, RandomSource, Signer]:
332
+ return (
333
+ cast(Clock, first_contribution(self.reactor, CLOCKS, "clock")),
334
+ cast(RandomSource, first_contribution(self.reactor, RANDOM_SOURCES, "random source")),
335
+ cast(Signer, first_contribution(self.reactor, SIGNERS, "signer")),
336
+ )
337
+
338
+ def _state_store(self) -> AuthorizationStateStore:
339
+ return cast(
340
+ AuthorizationStateStore,
341
+ first_contribution(
342
+ self.reactor, AUTHORIZATION_STATE_STORES, "authorization state store"
343
+ ),
344
+ )
345
+
346
+ @staticmethod
347
+ def _validate_context(tenant_id: str, pairwise_user_id: str, session_id: str) -> None:
348
+ import re
349
+
350
+ if not tenant_id or len(tenant_id) > 256:
351
+ raise ValueError("tenant_id must contain between 1 and 256 characters")
352
+ for label, value in (("pairwise_user_id", pairwise_user_id), ("session_id", session_id)):
353
+ if not re.fullmatch(r"[A-Za-z0-9_-]{1,256}", value):
354
+ raise ValueError(f"{label} must be an opaque URL-safe identifier")
355
+
356
+ @staticmethod
357
+ def _client_assertion(
358
+ client_id: str,
359
+ audience: str,
360
+ now: datetime,
361
+ random_source: RandomSource,
362
+ ) -> ClientAssertionClaims:
363
+ issued_at = int(now.timestamp())
364
+ return ClientAssertionClaims(
365
+ iss=client_id,
366
+ sub=client_id,
367
+ aud=audience,
368
+ iat=issued_at,
369
+ exp=issued_at + 60,
370
+ jti=_opaque_id("jti", random_source.bytes),
371
+ )
372
+
373
+ def _decode(self, response: httpx.Response, label: str) -> object:
374
+ if len(response.content) > self.max_response_bytes:
375
+ raise AuthorizationError(
376
+ "response_too_large",
377
+ f"{label.title()} response exceeds size limit",
378
+ status_code=response.status_code,
379
+ )
380
+ if response.headers.get("content-type", "").split(";", 1)[0].strip() != "application/json":
381
+ raise AuthorizationError(
382
+ "invalid_content_type",
383
+ f"{label.title()} endpoint must return JSON",
384
+ status_code=response.status_code,
385
+ )
386
+ try:
387
+ return loads_strict(response.content)
388
+ except ProtocolValidationError as exc:
389
+ raise AuthorizationError(
390
+ "invalid_json",
391
+ f"{label.title()} endpoint returned invalid JSON",
392
+ status_code=response.status_code,
393
+ ) from exc
394
+
395
+ @staticmethod
396
+ def _remote_error(
397
+ response: httpx.Response, payload: object, message: str
398
+ ) -> AuthorizationError:
399
+ remote = payload.get("error") if isinstance(payload, dict) else None
400
+ return AuthorizationError(
401
+ remote if isinstance(remote, str) else "http_error",
402
+ message,
403
+ status_code=response.status_code,
404
+ )