langchain-atomicmail 0.3.14__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.
- langchain_atomicmail-0.3.14/PKG-INFO +136 -0
- langchain_atomicmail-0.3.14/README.md +110 -0
- langchain_atomicmail-0.3.14/pyproject.toml +59 -0
- langchain_atomicmail-0.3.14/setup.cfg +4 -0
- langchain_atomicmail-0.3.14/src/atomicmail/__init__.py +32 -0
- langchain_atomicmail-0.3.14/src/atomicmail/auth_http.py +177 -0
- langchain_atomicmail-0.3.14/src/atomicmail/cli.py +236 -0
- langchain_atomicmail-0.3.14/src/atomicmail/config.py +112 -0
- langchain_atomicmail-0.3.14/src/atomicmail/constants.py +32 -0
- langchain_atomicmail-0.3.14/src/atomicmail/credentials.py +158 -0
- langchain_atomicmail-0.3.14/src/atomicmail/help.py +118 -0
- langchain_atomicmail-0.3.14/src/atomicmail/jmap_request.py +918 -0
- langchain_atomicmail-0.3.14/src/atomicmail/jwt_utils.py +34 -0
- langchain_atomicmail-0.3.14/src/atomicmail/mcp_server.py +341 -0
- langchain_atomicmail-0.3.14/src/atomicmail/pow.py +71 -0
- langchain_atomicmail-0.3.14/src/atomicmail/session.py +595 -0
- langchain_atomicmail-0.3.14/src/atomicmail/shared_assets.py +67 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/consts.json +11 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/fixtures/pow_vectors.json +32 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/fragments/inbox_cron_agent_prompt.md +1 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/fragments/post_register_cron_reminder.md +5 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/readme_stub.md +3 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/auth.md +8 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/cron.md +217 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/installation.md +35 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/jmap_cheatsheet.md +19 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/multi_account.md +9 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/overview.md +27 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/presets.md +12 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/tools.md +16 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/help/topics/troubleshooting.md +6 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/manifest.json +31 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/messages/errors.json +68 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/messages/hints.json +8 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/list_inbox.json +46 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/reply.json +97 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/send_mail.json +70 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/send_mail_attachment.json +92 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/presets/send_mail_blob_attachment.json +74 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/skill/SKILL.template.md +202 -0
- langchain_atomicmail-0.3.14/src/atomicmail/vendor/shared/skill/manifest.json +89 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail/__init__.py +23 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail/toolkit.py +15 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail/tools.py +251 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/PKG-INFO +136 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/SOURCES.txt +47 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/dependency_links.txt +1 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/requires.txt +4 -0
- langchain_atomicmail-0.3.14/src/langchain_atomicmail.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-atomicmail
|
|
3
|
+
Version: 0.3.14
|
|
4
|
+
Summary: LangChain tools for Atomic Mail register/help/JMAP operations
|
|
5
|
+
Author: Atomic Mail
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://atomicmail.ai
|
|
8
|
+
Project-URL: Source, https://github.com/Atomic-Mail/atomic-mail-agentic
|
|
9
|
+
Project-URL: Documentation, https://atomic-mail.github.io/atomic-mail-agentic/langchain
|
|
10
|
+
Keywords: atomic-mail,atomicmail,langchain,toolkit,agent,ai,jmap,email,proof-of-work
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Communications :: Email
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
Requires-Dist: langchain-core<2,>=0.3.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# langchain-atomicmail
|
|
28
|
+
|
|
29
|
+
LangChain integration package for Atomic Mail.
|
|
30
|
+
|
|
31
|
+
It exposes three tools backed by the same Python Atomic Mail runtime used by the
|
|
32
|
+
CLI/MCP adapters:
|
|
33
|
+
|
|
34
|
+
- `register`
|
|
35
|
+
- `jmap_request`
|
|
36
|
+
- `help`
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
Published package:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install langchain-atomicmail
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The published wheel bundles the Atomic Mail Python runtime and shared presets; no
|
|
47
|
+
separate PyPI package is required.
|
|
48
|
+
|
|
49
|
+
Local development in this monorepo (install core library first):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd py
|
|
53
|
+
pip install -e .
|
|
54
|
+
pip install -e langchain
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from langchain_atomicmail import AtomicMailToolkit
|
|
61
|
+
|
|
62
|
+
toolkit = AtomicMailToolkit()
|
|
63
|
+
tools = toolkit.get_tools()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
You can also import tool factories directly:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from langchain_atomicmail import get_atomicmail_tools
|
|
70
|
+
|
|
71
|
+
tools = get_atomicmail_tools()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Agent example
|
|
75
|
+
|
|
76
|
+
Use the toolkit with a ReAct agent (requires `langgraph` and a chat model):
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from langchain_openai import ChatOpenAI
|
|
80
|
+
from langgraph.prebuilt import create_react_agent
|
|
81
|
+
|
|
82
|
+
from langchain_atomicmail import AtomicMailToolkit
|
|
83
|
+
|
|
84
|
+
llm = ChatOpenAI(temperature=0)
|
|
85
|
+
toolkit = AtomicMailToolkit()
|
|
86
|
+
agent = create_react_agent(llm, toolkit.get_tools())
|
|
87
|
+
|
|
88
|
+
# Register an inbox, then fetch mail
|
|
89
|
+
agent.invoke({"messages": [("user", "Register username myagent and list my inbox")]})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
After a successful `register`, the tool response includes `_next` guidance for
|
|
93
|
+
hourly inbox polling. Call `help` with topic `cron` for full scheduling details.
|
|
94
|
+
|
|
95
|
+
## Inbox polling (after register)
|
|
96
|
+
|
|
97
|
+
Registration only creates credentials. For a useful inbox integration, arrange
|
|
98
|
+
hourly inbox polling per your runtime:
|
|
99
|
+
|
|
100
|
+
- **Native cron** (OpenClaw, Hermes, atomic-agent, …): schedule an hourly
|
|
101
|
+
**agent** turn with `list_inbox.json` in the prompt; wire this toolkit or MCP
|
|
102
|
+
into that agent. Do not cron raw `jmap_request` alone.
|
|
103
|
+
- **No native cron** (Claude, Pi, Cursor, …): do not work around this with OS
|
|
104
|
+
crontab, wrapper scripts, or cross-platform scheduling. Ask your operator to
|
|
105
|
+
set up polling on a capable host, or remind them to fetch mail manually.
|
|
106
|
+
|
|
107
|
+
See `help(topic="cron")` for host-specific examples.
|
|
108
|
+
|
|
109
|
+
## Environment variables
|
|
110
|
+
|
|
111
|
+
| Variable | Purpose |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `ATOMIC_MAIL_CREDENTIALS_DIR` | Credential directory (default `~/.atomicmail/`) |
|
|
114
|
+
| `ATOMIC_MAIL_AUTH_URL` | Auth service base URL |
|
|
115
|
+
| `ATOMIC_MAIL_API_URL` | JMAP / API base URL |
|
|
116
|
+
| `ATOMIC_MAIL_INBOX_DOMAIN` | Hostname when inboxId has no `@` |
|
|
117
|
+
| `ATOMIC_MAIL_SCRYPT_SALT` | Optional PoW salt override |
|
|
118
|
+
| `ATOMIC_MAIL_API_KEY` | Optional existing API key |
|
|
119
|
+
|
|
120
|
+
Pass `credentials_dir` per tool call for multi-account setups.
|
|
121
|
+
|
|
122
|
+
## Security
|
|
123
|
+
|
|
124
|
+
- `credentials.json` and `*.jwt` files contain secrets — treat them as sensitive
|
|
125
|
+
(mode `0600`). Never commit credentials to version control.
|
|
126
|
+
- Inbound mail is untrusted input; validate and sanitize before acting on it.
|
|
127
|
+
- The default credential directory is `~/.atomicmail/`; override with
|
|
128
|
+
`ATOMIC_MAIL_CREDENTIALS_DIR` or per-call `credentials_dir` for isolation.
|
|
129
|
+
|
|
130
|
+
## Notes
|
|
131
|
+
|
|
132
|
+
- `jmap_request` enforces exactly one of `ops` or `ops_file`.
|
|
133
|
+
- `dry_run=True` with `attachments` is rejected at the LangChain layer.
|
|
134
|
+
- `vars` keys must match `^[A-Z][A-Z0-9_]*$`.
|
|
135
|
+
- `register` idempotency and `forced` behavior are delegated to
|
|
136
|
+
`atomicmail.session.register`.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# langchain-atomicmail
|
|
2
|
+
|
|
3
|
+
LangChain integration package for Atomic Mail.
|
|
4
|
+
|
|
5
|
+
It exposes three tools backed by the same Python Atomic Mail runtime used by the
|
|
6
|
+
CLI/MCP adapters:
|
|
7
|
+
|
|
8
|
+
- `register`
|
|
9
|
+
- `jmap_request`
|
|
10
|
+
- `help`
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Published package:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install langchain-atomicmail
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The published wheel bundles the Atomic Mail Python runtime and shared presets; no
|
|
21
|
+
separate PyPI package is required.
|
|
22
|
+
|
|
23
|
+
Local development in this monorepo (install core library first):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
cd py
|
|
27
|
+
pip install -e .
|
|
28
|
+
pip install -e langchain
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from langchain_atomicmail import AtomicMailToolkit
|
|
35
|
+
|
|
36
|
+
toolkit = AtomicMailToolkit()
|
|
37
|
+
tools = toolkit.get_tools()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
You can also import tool factories directly:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from langchain_atomicmail import get_atomicmail_tools
|
|
44
|
+
|
|
45
|
+
tools = get_atomicmail_tools()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Agent example
|
|
49
|
+
|
|
50
|
+
Use the toolkit with a ReAct agent (requires `langgraph` and a chat model):
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from langchain_openai import ChatOpenAI
|
|
54
|
+
from langgraph.prebuilt import create_react_agent
|
|
55
|
+
|
|
56
|
+
from langchain_atomicmail import AtomicMailToolkit
|
|
57
|
+
|
|
58
|
+
llm = ChatOpenAI(temperature=0)
|
|
59
|
+
toolkit = AtomicMailToolkit()
|
|
60
|
+
agent = create_react_agent(llm, toolkit.get_tools())
|
|
61
|
+
|
|
62
|
+
# Register an inbox, then fetch mail
|
|
63
|
+
agent.invoke({"messages": [("user", "Register username myagent and list my inbox")]})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
After a successful `register`, the tool response includes `_next` guidance for
|
|
67
|
+
hourly inbox polling. Call `help` with topic `cron` for full scheduling details.
|
|
68
|
+
|
|
69
|
+
## Inbox polling (after register)
|
|
70
|
+
|
|
71
|
+
Registration only creates credentials. For a useful inbox integration, arrange
|
|
72
|
+
hourly inbox polling per your runtime:
|
|
73
|
+
|
|
74
|
+
- **Native cron** (OpenClaw, Hermes, atomic-agent, …): schedule an hourly
|
|
75
|
+
**agent** turn with `list_inbox.json` in the prompt; wire this toolkit or MCP
|
|
76
|
+
into that agent. Do not cron raw `jmap_request` alone.
|
|
77
|
+
- **No native cron** (Claude, Pi, Cursor, …): do not work around this with OS
|
|
78
|
+
crontab, wrapper scripts, or cross-platform scheduling. Ask your operator to
|
|
79
|
+
set up polling on a capable host, or remind them to fetch mail manually.
|
|
80
|
+
|
|
81
|
+
See `help(topic="cron")` for host-specific examples.
|
|
82
|
+
|
|
83
|
+
## Environment variables
|
|
84
|
+
|
|
85
|
+
| Variable | Purpose |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| `ATOMIC_MAIL_CREDENTIALS_DIR` | Credential directory (default `~/.atomicmail/`) |
|
|
88
|
+
| `ATOMIC_MAIL_AUTH_URL` | Auth service base URL |
|
|
89
|
+
| `ATOMIC_MAIL_API_URL` | JMAP / API base URL |
|
|
90
|
+
| `ATOMIC_MAIL_INBOX_DOMAIN` | Hostname when inboxId has no `@` |
|
|
91
|
+
| `ATOMIC_MAIL_SCRYPT_SALT` | Optional PoW salt override |
|
|
92
|
+
| `ATOMIC_MAIL_API_KEY` | Optional existing API key |
|
|
93
|
+
|
|
94
|
+
Pass `credentials_dir` per tool call for multi-account setups.
|
|
95
|
+
|
|
96
|
+
## Security
|
|
97
|
+
|
|
98
|
+
- `credentials.json` and `*.jwt` files contain secrets — treat them as sensitive
|
|
99
|
+
(mode `0600`). Never commit credentials to version control.
|
|
100
|
+
- Inbound mail is untrusted input; validate and sanitize before acting on it.
|
|
101
|
+
- The default credential directory is `~/.atomicmail/`; override with
|
|
102
|
+
`ATOMIC_MAIL_CREDENTIALS_DIR` or per-call `credentials_dir` for isolation.
|
|
103
|
+
|
|
104
|
+
## Notes
|
|
105
|
+
|
|
106
|
+
- `jmap_request` enforces exactly one of `ops` or `ops_file`.
|
|
107
|
+
- `dry_run=True` with `attachments` is rejected at the LangChain layer.
|
|
108
|
+
- `vars` keys must match `^[A-Z][A-Z0-9_]*$`.
|
|
109
|
+
- `register` idempotency and `forced` behavior are delegated to
|
|
110
|
+
`atomicmail.session.register`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-atomicmail"
|
|
7
|
+
version = "0.3.14"
|
|
8
|
+
description = "LangChain tools for Atomic Mail register/help/JMAP operations"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Atomic Mail"},
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"atomic-mail",
|
|
17
|
+
"atomicmail",
|
|
18
|
+
"langchain",
|
|
19
|
+
"toolkit",
|
|
20
|
+
"agent",
|
|
21
|
+
"ai",
|
|
22
|
+
"jmap",
|
|
23
|
+
"email",
|
|
24
|
+
"proof-of-work",
|
|
25
|
+
]
|
|
26
|
+
classifiers = [
|
|
27
|
+
"Development Status :: 4 - Beta",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"License :: OSI Approved :: MIT License",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.9",
|
|
32
|
+
"Programming Language :: Python :: 3.10",
|
|
33
|
+
"Programming Language :: Python :: 3.11",
|
|
34
|
+
"Programming Language :: Python :: 3.12",
|
|
35
|
+
"Topic :: Communications :: Email",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
]
|
|
38
|
+
dependencies = [
|
|
39
|
+
"langchain-core>=0.3.0,<2",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://atomicmail.ai"
|
|
44
|
+
Source = "https://github.com/Atomic-Mail/atomic-mail-agentic"
|
|
45
|
+
Documentation = "https://atomic-mail.github.io/atomic-mail-agentic/langchain"
|
|
46
|
+
|
|
47
|
+
[project.optional-dependencies]
|
|
48
|
+
dev = ["pytest"]
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-dir]
|
|
53
|
+
"" = "src"
|
|
54
|
+
|
|
55
|
+
[tool.setuptools.packages.find]
|
|
56
|
+
where = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.setuptools.package-data]
|
|
59
|
+
atomicmail = ["vendor/shared/**/*"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Atomic Mail Python shared foundation package."""
|
|
2
|
+
|
|
3
|
+
from .config import ResolvedAgentConfig, resolve_agent_config_from_env
|
|
4
|
+
from .credentials import (
|
|
5
|
+
CredentialStore,
|
|
6
|
+
Credentials,
|
|
7
|
+
SkillFiles,
|
|
8
|
+
default_files_from_out_dir,
|
|
9
|
+
)
|
|
10
|
+
from .help import help
|
|
11
|
+
from .jmap_request import JmapAttachmentInput, JmapRequestResult, jmap_request, run_jmap_request
|
|
12
|
+
from .mcp_server import handle_tool_call
|
|
13
|
+
from .session import AgentSession, RegisterResult, create_agent_session, register
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"AgentSession",
|
|
17
|
+
"Credentials",
|
|
18
|
+
"CredentialStore",
|
|
19
|
+
"JmapRequestResult",
|
|
20
|
+
"JmapAttachmentInput",
|
|
21
|
+
"RegisterResult",
|
|
22
|
+
"ResolvedAgentConfig",
|
|
23
|
+
"SkillFiles",
|
|
24
|
+
"default_files_from_out_dir",
|
|
25
|
+
"help",
|
|
26
|
+
"handle_tool_call",
|
|
27
|
+
"jmap_request",
|
|
28
|
+
"run_jmap_request",
|
|
29
|
+
"register",
|
|
30
|
+
"create_agent_session",
|
|
31
|
+
"resolve_agent_config_from_env",
|
|
32
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Auth-service HTTP helpers: challenge -> session -> capability."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable, Mapping
|
|
8
|
+
from urllib.error import HTTPError
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
from .jwt_utils import decode_jwt_payload
|
|
12
|
+
from .pow import solve_pow
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ChallengeResponse:
|
|
17
|
+
challengeJWT: str
|
|
18
|
+
challenge: str
|
|
19
|
+
difficulty: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SessionResponse:
|
|
24
|
+
sessionJWT: str
|
|
25
|
+
apiKey: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def fetch_challenge(auth_url: str) -> ChallengeResponse:
|
|
29
|
+
base = auth_url.rstrip("/")
|
|
30
|
+
status, text, headers = _http_post(f"{base}/api/v1/challenge")
|
|
31
|
+
if status < 200 or status >= 300:
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"auth-service /api/v1/challenge returned {status}: {text}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
challenge_jwt = _read_bearer_token(
|
|
37
|
+
headers.get("Authorization"),
|
|
38
|
+
"Challenge response missing Authorization bearer token.",
|
|
39
|
+
)
|
|
40
|
+
payload = decode_jwt_payload(challenge_jwt)
|
|
41
|
+
challenge = payload.get("jti")
|
|
42
|
+
difficulty = payload.get("difficulty")
|
|
43
|
+
if not isinstance(challenge, str) or not isinstance(difficulty, (int, float)):
|
|
44
|
+
raise ValueError("Challenge JWT payload malformed (missing jti or difficulty).")
|
|
45
|
+
|
|
46
|
+
return ChallengeResponse(
|
|
47
|
+
challengeJWT=challenge_jwt,
|
|
48
|
+
challenge=challenge,
|
|
49
|
+
difficulty=int(difficulty),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def exchange_session(
|
|
54
|
+
auth_url: str,
|
|
55
|
+
*,
|
|
56
|
+
challenge_jwt: str,
|
|
57
|
+
pow_hex: str,
|
|
58
|
+
nonce: str,
|
|
59
|
+
api_key: str | None = None,
|
|
60
|
+
username: str | None = None,
|
|
61
|
+
) -> SessionResponse:
|
|
62
|
+
base = auth_url.rstrip("/")
|
|
63
|
+
payload: dict[str, str] = {"powHex": pow_hex, "nonce": nonce}
|
|
64
|
+
if api_key:
|
|
65
|
+
payload["apiKey"] = api_key
|
|
66
|
+
if username:
|
|
67
|
+
payload["username"] = username
|
|
68
|
+
|
|
69
|
+
status, text, headers = _http_post(
|
|
70
|
+
f"{base}/api/v1/session",
|
|
71
|
+
headers={"Authorization": f"Bearer {challenge_jwt}"},
|
|
72
|
+
json_body=payload,
|
|
73
|
+
)
|
|
74
|
+
if status < 200 or status >= 300:
|
|
75
|
+
raise ValueError(f"auth-service /api/v1/session returned {status}: {text}")
|
|
76
|
+
|
|
77
|
+
session_jwt = _read_bearer_token(
|
|
78
|
+
headers.get("Authorization"),
|
|
79
|
+
"Session response missing Authorization bearer token.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
data: dict[str, object] = {}
|
|
83
|
+
if text.strip():
|
|
84
|
+
try:
|
|
85
|
+
parsed = json.loads(text)
|
|
86
|
+
except json.JSONDecodeError as err:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"auth-service /api/v1/session returned non-JSON body."
|
|
89
|
+
) from err
|
|
90
|
+
if not isinstance(parsed, dict):
|
|
91
|
+
raise ValueError("auth-service /api/v1/session returned non-JSON body.")
|
|
92
|
+
data = parsed
|
|
93
|
+
|
|
94
|
+
api_key_out = data.get("apiKey")
|
|
95
|
+
return SessionResponse(
|
|
96
|
+
sessionJWT=session_jwt,
|
|
97
|
+
apiKey=api_key_out if isinstance(api_key_out, str) else None,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def fetch_capability(auth_url: str, session_jwt: str) -> str:
|
|
102
|
+
base = auth_url.rstrip("/")
|
|
103
|
+
status, text, headers = _http_post(
|
|
104
|
+
f"{base}/api/v1/capability",
|
|
105
|
+
headers={"Authorization": f"Bearer {session_jwt}"},
|
|
106
|
+
)
|
|
107
|
+
if status < 200 or status >= 300:
|
|
108
|
+
raise ValueError(f"auth-service /api/v1/capability returned {status}: {text}")
|
|
109
|
+
|
|
110
|
+
return _read_bearer_token(
|
|
111
|
+
headers.get("Authorization"),
|
|
112
|
+
"Capability response missing Authorization bearer token.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def perform_pow_and_session(
|
|
117
|
+
*,
|
|
118
|
+
auth_url: str,
|
|
119
|
+
scrypt_salt: str,
|
|
120
|
+
api_key: str | None = None,
|
|
121
|
+
username: str | None = None,
|
|
122
|
+
on_pow_progress: Callable[[int], None] | None = None,
|
|
123
|
+
) -> SessionResponse:
|
|
124
|
+
challenge = fetch_challenge(auth_url)
|
|
125
|
+
solved = solve_pow(
|
|
126
|
+
challenge=challenge.challenge,
|
|
127
|
+
difficulty=challenge.difficulty,
|
|
128
|
+
salt=scrypt_salt,
|
|
129
|
+
on_progress=on_pow_progress,
|
|
130
|
+
)
|
|
131
|
+
return exchange_session(
|
|
132
|
+
auth_url,
|
|
133
|
+
challenge_jwt=challenge.challengeJWT,
|
|
134
|
+
pow_hex=solved.powHex,
|
|
135
|
+
nonce=solved.nonce,
|
|
136
|
+
api_key=api_key,
|
|
137
|
+
username=username,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _read_bearer_token(header_value: str | None, missing_error: str) -> str:
|
|
142
|
+
if not header_value:
|
|
143
|
+
raise ValueError(missing_error)
|
|
144
|
+
|
|
145
|
+
raw = header_value.strip()
|
|
146
|
+
prefix = "bearer "
|
|
147
|
+
if not raw.lower().startswith(prefix):
|
|
148
|
+
raise ValueError("Authorization header must use Bearer scheme.")
|
|
149
|
+
|
|
150
|
+
token = raw[len(prefix) :].strip()
|
|
151
|
+
if not token:
|
|
152
|
+
raise ValueError("Authorization header must use Bearer scheme.")
|
|
153
|
+
return token
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _http_post(
|
|
157
|
+
url: str,
|
|
158
|
+
*,
|
|
159
|
+
headers: Mapping[str, str] | None = None,
|
|
160
|
+
json_body: object | None = None,
|
|
161
|
+
) -> tuple[int, str, Mapping[str, str]]:
|
|
162
|
+
req_headers = dict(headers or {})
|
|
163
|
+
body_bytes: bytes | None = None
|
|
164
|
+
if json_body is not None:
|
|
165
|
+
body_bytes = json.dumps(json_body).encode("utf-8")
|
|
166
|
+
req_headers.setdefault("Content-Type", "application/json")
|
|
167
|
+
|
|
168
|
+
req = Request(url, data=body_bytes, headers=req_headers, method="POST")
|
|
169
|
+
try:
|
|
170
|
+
with urlopen(req) as response:
|
|
171
|
+
return (
|
|
172
|
+
int(response.getcode()),
|
|
173
|
+
response.read().decode("utf-8"),
|
|
174
|
+
response.headers,
|
|
175
|
+
)
|
|
176
|
+
except HTTPError as err:
|
|
177
|
+
return err.code, err.read().decode("utf-8", errors="replace"), err.headers
|