@atomicmail/mcp 0.2.1 → 0.2.2
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.
- package/README.md +135 -9
- package/esm/lib/agent/auth/agent-auth-http.d.ts.map +1 -1
- package/esm/lib/agent/auth/agent-auth-http.js +47 -38
- package/esm/lib/agent/auth/agent-jwt.d.ts +0 -2
- package/esm/lib/agent/auth/agent-jwt.d.ts.map +1 -1
- package/esm/lib/agent/auth/agent-jwt.js +0 -2
- package/esm/lib/agent/jmap/agent-help-content.d.ts +1 -0
- package/esm/lib/agent/jmap/agent-help-content.d.ts.map +1 -1
- package/esm/lib/agent/jmap/agent-help-content.js +35 -16
- package/esm/lib/agent/jmap/agent-jmap.d.ts +7 -0
- package/esm/lib/agent/jmap/agent-jmap.d.ts.map +1 -1
- package/esm/lib/agent/jmap/agent-jmap.js +16 -0
- package/esm/lib/agent/session/agent-credentials-store.d.ts +2 -0
- package/esm/lib/agent/session/agent-credentials-store.d.ts.map +1 -1
- package/esm/lib/agent/session/agent-credentials-store.js +2 -0
- package/esm/lib/agent/session/agent-session.d.ts +5 -0
- package/esm/lib/agent/session/agent-session.d.ts.map +1 -1
- package/esm/lib/agent/session/agent-session.js +44 -8
- package/esm/lib/core/read-npm-package-readme.d.ts +6 -0
- package/esm/lib/core/read-npm-package-readme.d.ts.map +1 -0
- package/esm/lib/core/read-npm-package-readme.js +66 -0
- package/esm/lib/mod.d.ts +1 -0
- package/esm/lib/mod.d.ts.map +1 -1
- package/esm/lib/mod.js +1 -0
- package/esm/lib/network/auth-client.d.ts.map +1 -1
- package/esm/lib/network/auth-client.js +48 -25
- package/esm/mcp/main.js +2 -1
- package/esm/mcp/tools/help.d.ts.map +1 -1
- package/esm/mcp/tools/help.js +30 -7
- package/esm/mcp/tools/jmap.d.ts.map +1 -1
- package/esm/mcp/tools/jmap.js +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Install and configure the @atomicmail/mcp stdio server, tools (register, jmap_request, help), and host-specific notes for chat-based agents.
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# @atomicmail/mcp
|
|
2
6
|
|
|
3
7
|
Atomic Mail MCP server — a local stdio Model Context Protocol server that gives
|
|
@@ -23,11 +27,11 @@ Your MCP host spawns this process; see configuration below.
|
|
|
23
27
|
|
|
24
28
|
## Tools exposed
|
|
25
29
|
|
|
26
|
-
| Tool | Description
|
|
27
|
-
| -------------- |
|
|
28
|
-
| `register` | PoW signup; persists credentials. Idempotent when username matches inbox.
|
|
29
|
-
| `jmap_request` | JMAP batch via `ops` or `ops_file`. Uppercase `$VAR_NAME` tokens are substituted (`$ACCOUNT_ID` / `$INBOX` from session; others via optional `vars` map). |
|
|
30
|
-
| `help` | Built-in docs (`topic` optional).
|
|
30
|
+
| Tool | Description |
|
|
31
|
+
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `register` | PoW signup; persists credentials. Idempotent when username matches inbox. |
|
|
33
|
+
| `jmap_request` | JMAP batch via `ops` or `ops_file`. Uppercase `$VAR_NAME` tokens are substituted (`$ACCOUNT_ID` / `$INBOX` / `$UPLOAD_URL` / `$DOWNLOAD_URL` from session; others via optional `vars` map). |
|
|
34
|
+
| `help` | Built-in docs (`topic` optional); use `topic: "readme"` for the published package `README.md`. |
|
|
31
35
|
|
|
32
36
|
## Typical MCP workflow
|
|
33
37
|
|
|
@@ -62,11 +66,12 @@ Resolution order for `ops_file`:
|
|
|
62
66
|
Placeholder rules:
|
|
63
67
|
|
|
64
68
|
- Pattern: `$VAR_NAME`, where `VAR_NAME` matches `^[A-Z][A-Z0-9_]*$`.
|
|
65
|
-
- Built-ins: `$ACCOUNT_ID`, `$INBOX`.
|
|
69
|
+
- Built-ins: `$ACCOUNT_ID`, `$INBOX`, `$UPLOAD_URL`, `$DOWNLOAD_URL`.
|
|
66
70
|
- Lowercase `$tokens` such as JMAP back-references (`$draft`) are not matched.
|
|
67
71
|
- Custom placeholders: passed in `vars` as string values.
|
|
68
72
|
- Resolution order per variable: `vars` first, then built-in auto-resolvers.
|
|
69
|
-
- Built-ins can be overridden by providing `ACCOUNT_ID
|
|
73
|
+
- Built-ins can be overridden by providing `ACCOUNT_ID`, `INBOX`, `UPLOAD_URL`,
|
|
74
|
+
or `DOWNLOAD_URL` in `vars`.
|
|
70
75
|
- If any referenced variable is unresolved, `jmap_request` fails with a missing
|
|
71
76
|
variables error.
|
|
72
77
|
- Substitution is single-pass: inserted values are not scanned again for nested
|
|
@@ -89,13 +94,134 @@ Example:
|
|
|
89
94
|
|
|
90
95
|
Mode `0600`:
|
|
91
96
|
|
|
92
|
-
- `credentials.json` —
|
|
93
|
-
|
|
97
|
+
- `credentials.json` —
|
|
98
|
+
`{ apiKey, inboxId, authUrl, apiUrl, scryptSalt, uploadUrl, downloadUrl }`
|
|
99
|
+
- `session.jwt` — 1h TTL, rotated via PoW
|
|
94
100
|
- `capability.jwt` — 2m TTL, rotated before JMAP calls
|
|
95
101
|
|
|
96
102
|
These files are created and rotated automatically by MCP tool calls. AgentSkill
|
|
97
103
|
CLI uses the same files.
|
|
98
104
|
|
|
105
|
+
During PoW auth, the challenge JWT is exchanged via `Authorization: Bearer ...`
|
|
106
|
+
for both `POST /api/v1/challenge` (response header) and `POST /api/v1/session`
|
|
107
|
+
(request header), and session JWT is read from the `POST /api/v1/session`
|
|
108
|
+
response header. `POST /api/v1/capability` accepts session JWT via bearer header
|
|
109
|
+
and returns capability JWT in the response bearer header. PoW values (`powHex`,
|
|
110
|
+
`nonce`) remain in the JSON body for session creation.
|
|
111
|
+
|
|
112
|
+
## Attachments and blobs
|
|
113
|
+
|
|
114
|
+
Two blob paths are supported:
|
|
115
|
+
|
|
116
|
+
- **RFC 9404 in-band blobs** via `Blob/upload` and `Blob/get` over
|
|
117
|
+
`jmap_request`.
|
|
118
|
+
- **RFC 8620 out-of-band blobs** via `uploadUrl` and `downloadUrl` templates
|
|
119
|
+
from JMAP session.
|
|
120
|
+
|
|
121
|
+
### Inline blob flow (RFC 9404)
|
|
122
|
+
|
|
123
|
+
Add `urn:ietf:params:jmap:blob` and upload data in the same JMAP batch:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"using": [
|
|
128
|
+
"urn:ietf:params:jmap:core",
|
|
129
|
+
"urn:ietf:params:jmap:mail",
|
|
130
|
+
"urn:ietf:params:jmap:submission",
|
|
131
|
+
"urn:ietf:params:jmap:blob"
|
|
132
|
+
],
|
|
133
|
+
"methodCalls": [
|
|
134
|
+
[
|
|
135
|
+
"Blob/upload",
|
|
136
|
+
{
|
|
137
|
+
"accountId": "$ACCOUNT_ID",
|
|
138
|
+
"create": {
|
|
139
|
+
"b1": {
|
|
140
|
+
"data:asText": "Hello attachment",
|
|
141
|
+
"type": "text/plain"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"b0"
|
|
146
|
+
],
|
|
147
|
+
[
|
|
148
|
+
"Email/set",
|
|
149
|
+
{
|
|
150
|
+
"accountId": "$ACCOUNT_ID",
|
|
151
|
+
"create": {
|
|
152
|
+
"m1": {
|
|
153
|
+
"from": [{ "email": "$INBOX" }],
|
|
154
|
+
"to": [{ "email": "$TO" }],
|
|
155
|
+
"subject": "With attachment",
|
|
156
|
+
"bodyValues": {
|
|
157
|
+
"body1": { "value": "See attachment." }
|
|
158
|
+
},
|
|
159
|
+
"textBody": [{ "partId": "body1", "type": "text/plain" }],
|
|
160
|
+
"attachments": [
|
|
161
|
+
{
|
|
162
|
+
"blobId": "#b1",
|
|
163
|
+
"type": "text/plain",
|
|
164
|
+
"name": "note.txt"
|
|
165
|
+
}
|
|
166
|
+
]
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"m0"
|
|
171
|
+
],
|
|
172
|
+
[
|
|
173
|
+
"EmailSubmission/set",
|
|
174
|
+
{
|
|
175
|
+
"accountId": "$ACCOUNT_ID",
|
|
176
|
+
"create": { "s1": { "emailId": "#m1" } }
|
|
177
|
+
},
|
|
178
|
+
"s0"
|
|
179
|
+
]
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Separate upload/download flow (RFC 8620)
|
|
185
|
+
|
|
186
|
+
Use the templated URLs from session/credentials:
|
|
187
|
+
|
|
188
|
+
- `$UPLOAD_URL` template contains `{accountId}`.
|
|
189
|
+
- `$DOWNLOAD_URL` template contains `{accountId}`, `{blobId}`, `{name}`, and
|
|
190
|
+
`{type}`.
|
|
191
|
+
|
|
192
|
+
Example (MCP flow):
|
|
193
|
+
|
|
194
|
+
1. Call `jmap_request` to get attachment metadata (for example `Email/get` with
|
|
195
|
+
`attachments`).
|
|
196
|
+
2. Expand `$DOWNLOAD_URL` template with account/blob metadata and fetch bytes
|
|
197
|
+
via HTTP bearer auth.
|
|
198
|
+
3. To upload bytes, expand `$UPLOAD_URL` with account id and POST binary content
|
|
199
|
+
per RFC 8620 upload endpoint.
|
|
200
|
+
|
|
201
|
+
### Blob retrieval (RFC 9404)
|
|
202
|
+
|
|
203
|
+
For in-band retrieval, use `Blob/get`:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"using": [
|
|
208
|
+
"urn:ietf:params:jmap:core",
|
|
209
|
+
"urn:ietf:params:jmap:blob"
|
|
210
|
+
],
|
|
211
|
+
"methodCalls": [
|
|
212
|
+
[
|
|
213
|
+
"Blob/get",
|
|
214
|
+
{
|
|
215
|
+
"accountId": "$ACCOUNT_ID",
|
|
216
|
+
"ids": ["$BLOB_ID"],
|
|
217
|
+
"properties": ["id", "data:asBase64", "size", "type"]
|
|
218
|
+
},
|
|
219
|
+
"g0"
|
|
220
|
+
]
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
99
225
|
## Defaults
|
|
100
226
|
|
|
101
227
|
- auth endpoint: `https://auth.atomicmail.ai`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-auth-http.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/auth/agent-auth-http.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agent-auth-http.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/auth/agent-auth-http.ts"],"names":[],"mappings":"AAKA,wBAAsB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC,CA8BD;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IACJ,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,eAAe,CAAC,CA8B1B;AAED,wBAAsB,eAAe,CACnC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CAejB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,eAAe,CAAC,CAgB1B"}
|
|
@@ -1,66 +1,65 @@
|
|
|
1
1
|
// auth-service HTTP: challenge → session → capability.
|
|
2
2
|
import { decodeJwtPayload } from "./agent-jwt.js";
|
|
3
3
|
import { solvePow } from "./agent-pow.js";
|
|
4
|
-
async function
|
|
5
|
-
const res = await fetch(
|
|
4
|
+
export async function fetchChallenge(authUrl) {
|
|
5
|
+
const res = await fetch(`${authUrl}/api/v1/challenge`, {
|
|
6
6
|
method: "POST",
|
|
7
|
-
headers: {
|
|
8
|
-
...(body ? { "Content-Type": "application/json" } : {}),
|
|
9
|
-
...headers,
|
|
10
|
-
},
|
|
11
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
12
7
|
});
|
|
13
8
|
const text = await res.text();
|
|
14
|
-
const path = (() => {
|
|
15
|
-
try {
|
|
16
|
-
return new URL(url).pathname;
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
return url;
|
|
20
|
-
}
|
|
21
|
-
})();
|
|
22
9
|
if (!res.ok) {
|
|
23
|
-
throw new Error(`auth-service
|
|
24
|
-
}
|
|
25
|
-
try {
|
|
26
|
-
return JSON.parse(text);
|
|
10
|
+
throw new Error(`auth-service /api/v1/challenge returned ${res.status}: ${text}`);
|
|
27
11
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
export async function fetchChallenge(authUrl) {
|
|
33
|
-
const data = await postJson(`${authUrl}/api/v1/challenge`, undefined);
|
|
34
|
-
if (typeof data.challengeJWT !== "string") {
|
|
35
|
-
throw new Error("Challenge response missing challengeJWT.");
|
|
36
|
-
}
|
|
37
|
-
const payload = decodeJwtPayload(data.challengeJWT);
|
|
12
|
+
const challengeJWT = readBearerToken(res.headers.get("Authorization"), "Challenge response missing Authorization bearer token.");
|
|
13
|
+
const payload = decodeJwtPayload(challengeJWT);
|
|
38
14
|
if (typeof payload.jti !== "string" ||
|
|
39
15
|
typeof payload.difficulty !== "number") {
|
|
40
16
|
throw new Error("Challenge JWT payload malformed (missing jti or difficulty).");
|
|
41
17
|
}
|
|
42
18
|
return {
|
|
43
|
-
challengeJWT
|
|
19
|
+
challengeJWT,
|
|
44
20
|
challenge: payload.jti,
|
|
45
21
|
difficulty: payload.difficulty,
|
|
46
22
|
};
|
|
47
23
|
}
|
|
48
24
|
export async function exchangeSession(authUrl, body) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
25
|
+
const { challengeJWT, ...payload } = body;
|
|
26
|
+
const res = await fetch(`${authUrl}/api/v1/session`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
Authorization: `Bearer ${challengeJWT}`,
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(payload),
|
|
33
|
+
});
|
|
34
|
+
const text = await res.text();
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`auth-service /api/v1/session returned ${res.status}: ${text}`);
|
|
37
|
+
}
|
|
38
|
+
const sessionJWT = readBearerToken(res.headers.get("Authorization"), "Session response missing Authorization bearer token.");
|
|
39
|
+
let data = {};
|
|
40
|
+
if (text.trim().length > 0) {
|
|
41
|
+
try {
|
|
42
|
+
data = JSON.parse(text);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error("auth-service /api/v1/session returned non-JSON body.");
|
|
46
|
+
}
|
|
52
47
|
}
|
|
53
48
|
return {
|
|
54
|
-
sessionJWT
|
|
49
|
+
sessionJWT,
|
|
55
50
|
apiKey: typeof data.apiKey === "string" ? data.apiKey : undefined,
|
|
56
51
|
};
|
|
57
52
|
}
|
|
58
53
|
export async function fetchCapability(authUrl, sessionJWT) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
const res = await fetch(`${authUrl}/api/v1/capability`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { Authorization: `Bearer ${sessionJWT}` },
|
|
57
|
+
});
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
throw new Error(`auth-service /api/v1/capability returned ${res.status}: ${text}`);
|
|
62
61
|
}
|
|
63
|
-
return
|
|
62
|
+
return readBearerToken(res.headers.get("Authorization"), "Capability response missing Authorization bearer token.");
|
|
64
63
|
}
|
|
65
64
|
export async function performPoWAndSession(input) {
|
|
66
65
|
const { authUrl, scryptSalt } = input;
|
|
@@ -74,3 +73,13 @@ export async function performPoWAndSession(input) {
|
|
|
74
73
|
username: input.username,
|
|
75
74
|
});
|
|
76
75
|
}
|
|
76
|
+
function readBearerToken(headerValue, missingError) {
|
|
77
|
+
if (!headerValue) {
|
|
78
|
+
throw new Error(missingError);
|
|
79
|
+
}
|
|
80
|
+
const match = /^\s*Bearer\s+(.+?)\s*$/i.exec(headerValue);
|
|
81
|
+
if (!match || !match[1]) {
|
|
82
|
+
throw new Error("Authorization header must use Bearer scheme.");
|
|
83
|
+
}
|
|
84
|
+
return match[1];
|
|
85
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-jwt.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/auth/agent-jwt.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,
|
|
1
|
+
{"version":3,"file":"agent-jwt.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/auth/agent-jwt.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAC/C,eAAO,MAAM,2BAA2B,QAAS,CAAC;AAElD,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,CAc/D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAQnE"}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
// JWT helpers for capability/session expiry checks.
|
|
2
|
-
export const SESSION_TTL_MS = 4 * 60 * 60 * 1000;
|
|
3
|
-
export const CAPABILITY_TTL_MS = 2 * 60 * 1000;
|
|
4
2
|
export const SESSION_SAFETY_MARGIN_MS = 60_000;
|
|
5
3
|
export const CAPABILITY_SAFETY_MARGIN_MS = 20_000;
|
|
6
4
|
export function decodeJwtPayload(jwt) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const HELP_TOPICS: Record<string, string>;
|
|
2
2
|
export declare const HELP_TOPIC_LIST: string[];
|
|
3
|
+
export declare function normalizeHelpTopic(topic: string): string;
|
|
3
4
|
export declare function getHelp(topic?: string): string;
|
|
4
5
|
//# sourceMappingURL=agent-help-content.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-help-content.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-help-content.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"agent-help-content.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-help-content.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA0P9C,CAAC;AAEF,eAAO,MAAM,eAAe,UAA2B,CAAC;AAExD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAkB9C"}
|
|
@@ -16,10 +16,12 @@ Three operations only:
|
|
|
16
16
|
2. **jmap_request** — Send a JMAP method-call batch. Auth and JWT rotation are
|
|
17
17
|
automatic. Pass inline \`ops\` JSON or an \`ops_file\` preset path (same
|
|
18
18
|
substitution applies to both). Uppercase tokens like \`$ACCOUNT_ID\`,
|
|
19
|
-
\`$INBOX\`, \`$TO\`, \`$SUBJECT\` are
|
|
20
|
-
\`$ACCOUNT_ID\` / \`$INBOX\`
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
\`$INBOX\`, \`$UPLOAD_URL\`, \`$DOWNLOAD_URL\`, \`$TO\`, \`$SUBJECT\` are
|
|
20
|
+
replaced before the request is sent. \`$ACCOUNT_ID\` / \`$INBOX\` /
|
|
21
|
+
\`$UPLOAD_URL\` / \`$DOWNLOAD_URL\` come from the JMAP session and
|
|
22
|
+
credentials; pass any other names via MCP \`vars\` or skill \`--vars\`.
|
|
23
|
+
3. **help** — This documentation (optional \`topic\` / \`--topic\`), or the
|
|
24
|
+
published package README (\`topic\` / \`--topic\` \`readme\`).
|
|
23
25
|
|
|
24
26
|
## Typical workflow
|
|
25
27
|
|
|
@@ -29,7 +31,7 @@ Three operations only:
|
|
|
29
31
|
3. If stuck, read error hints and call \`help\`.
|
|
30
32
|
|
|
31
33
|
Available topics: overview, installation, auth, jmap_cheatsheet, tools,
|
|
32
|
-
presets, troubleshooting
|
|
34
|
+
presets, troubleshooting. Use \`readme\` for the npm package \`README.md\`.`,
|
|
33
35
|
installation: `\
|
|
34
36
|
# Atomic Mail — Installation
|
|
35
37
|
|
|
@@ -86,18 +88,22 @@ See each package README for details.`,
|
|
|
86
88
|
Auth is automatic after \`register\` (or when \`credentials.json\` + API key
|
|
87
89
|
exist).
|
|
88
90
|
|
|
89
|
-
1. **Challenge** — \`POST /api/v1/challenge
|
|
91
|
+
1. **Challenge** — \`POST /api/v1/challenge\`, read challenge JWT from
|
|
92
|
+
\`Authorization: Bearer <challengeJWT>\`
|
|
90
93
|
2. **Proof-of-work** — scrypt until difficulty satisfied
|
|
91
|
-
3. **Session JWT** — \`POST /api/v1/session\`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
3. **Session JWT** — \`POST /api/v1/session\` with challenge JWT in
|
|
95
|
+
\`Authorization: Bearer ...\` and PoW fields (\`powHex\`, \`nonce\`) in JSON
|
|
96
|
+
body; read session JWT from response \`Authorization: Bearer ...\` (1h TTL);
|
|
97
|
+
signup returns \`apiKey\` once
|
|
98
|
+
4. **Capability JWT** — \`POST /api/v1/capability\` with session JWT in
|
|
99
|
+
\`Authorization: Bearer ...\`; read capability JWT from response
|
|
100
|
+
\`Authorization: Bearer ...\` (2 min TTL) used as the JMAP bearer
|
|
95
101
|
|
|
96
102
|
JWTs are rotated before expiry and written back to disk.
|
|
97
103
|
|
|
98
104
|
## Credential files (mode 0600)
|
|
99
105
|
|
|
100
|
-
\`credentials.json\` — \`{ apiKey, inboxId, authUrl, apiUrl, scryptSalt }\`
|
|
106
|
+
\`credentials.json\` — \`{ apiKey, inboxId, authUrl, apiUrl, scryptSalt, uploadUrl, downloadUrl }\`
|
|
101
107
|
\`session.jwt\` — session token
|
|
102
108
|
\`capability.jwt\` — capability token
|
|
103
109
|
|
|
@@ -119,7 +125,8 @@ JWTs are rotated before expiry and written back to disk.
|
|
|
119
125
|
|
|
120
126
|
## Examples
|
|
121
127
|
|
|
122
|
-
Use \`$ACCOUNT_ID
|
|
128
|
+
Use \`$ACCOUNT_ID\`, \`$INBOX\`, \`$UPLOAD_URL\`, and \`$DOWNLOAD_URL\` for
|
|
129
|
+
session fields; use \`$TO\`, \`$SUBJECT\`,
|
|
123
130
|
etc., and supply values via MCP \`vars\` or \`--vars\` (JSON object of strings).
|
|
124
131
|
|
|
125
132
|
### Mailboxes
|
|
@@ -174,7 +181,8 @@ replaces credentials in the directory and registers a new inbox.
|
|
|
174
181
|
**Skill:** \`help [--topic TOPIC]\`
|
|
175
182
|
|
|
176
183
|
Topics: overview, installation, auth, jmap_cheatsheet, tools, presets,
|
|
177
|
-
troubleshooting
|
|
184
|
+
troubleshooting. Topic \`readme\` prints the published package \`README.md\`
|
|
185
|
+
(same layout as npm; requires install from npm).`,
|
|
178
186
|
presets: `\
|
|
179
187
|
# JMAP presets
|
|
180
188
|
|
|
@@ -199,11 +207,14 @@ keywords like \`$draft\` stay untouched).
|
|
|
199
207
|
- \`$ACCOUNT_ID\` — primary mail account id (from \`GET /.well-known/jmap\` when
|
|
200
208
|
referenced).
|
|
201
209
|
- \`$INBOX\` — inbox email address from credentials.
|
|
210
|
+
- \`$UPLOAD_URL\` — RFC 8620 upload URL template from JMAP session.
|
|
211
|
+
- \`$DOWNLOAD_URL\` — RFC 8620 download URL template from JMAP session.
|
|
202
212
|
- Any other \`$FOO\` — must appear in MCP \`vars\` or skill \`--vars\` as
|
|
203
213
|
\`"FOO": "..."\` (string values only; JSON escaping in the preset body is your
|
|
204
214
|
responsibility).
|
|
205
215
|
|
|
206
|
-
You may override \`ACCOUNT_ID\` / \`INBOX\`
|
|
216
|
+
You may override \`ACCOUNT_ID\` / \`INBOX\` / \`UPLOAD_URL\` / \`DOWNLOAD_URL\`
|
|
217
|
+
via \`vars\` / \`--vars\` if needed.`,
|
|
207
218
|
troubleshooting: `\
|
|
208
219
|
# Troubleshooting
|
|
209
220
|
|
|
@@ -234,11 +245,19 @@ Pass every custom placeholder in MCP \`vars\` or \`--vars\` as a JSON object of
|
|
|
234
245
|
strings. Ensure \`register\` completed so \`$ACCOUNT_ID\` / \`$INBOX\` can resolve.`,
|
|
235
246
|
};
|
|
236
247
|
export const HELP_TOPIC_LIST = Object.keys(HELP_TOPICS);
|
|
248
|
+
export function normalizeHelpTopic(topic) {
|
|
249
|
+
return topic.toLowerCase().replace(/[\s-]/g, "_");
|
|
250
|
+
}
|
|
237
251
|
export function getHelp(topic) {
|
|
238
252
|
if (!topic) {
|
|
239
253
|
return HELP_TOPICS["overview"];
|
|
240
254
|
}
|
|
241
|
-
const key = topic
|
|
255
|
+
const key = normalizeHelpTopic(topic);
|
|
256
|
+
if (key === "readme") {
|
|
257
|
+
return ("Topic \"readme\" prints the package README.md from the npm install. " +
|
|
258
|
+
"From MCP use {\"topic\":\"readme\"}; from the CLI: " +
|
|
259
|
+
"`atomicmail help --topic readme`.");
|
|
260
|
+
}
|
|
242
261
|
return (HELP_TOPICS[key] ??
|
|
243
|
-
`Unknown topic "${topic}". Available topics: ${HELP_TOPIC_LIST.join(", ")}`);
|
|
262
|
+
`Unknown topic "${topic}". Available topics: ${HELP_TOPIC_LIST.join(", ")}, readme`);
|
|
244
263
|
}
|
|
@@ -8,6 +8,11 @@ export declare function parseJmapEnvelope(raw: string, defaultUsing: string[], s
|
|
|
8
8
|
export declare function resolveOpsFilePath(credentialDir: string, opsFile: string): string;
|
|
9
9
|
export declare function readOpsFile(credentialDir: string, opsFile: string): Promise<string>;
|
|
10
10
|
export declare function extractPrimaryMailAccountId(session: Record<string, unknown>): string;
|
|
11
|
+
export interface JmapBlobEndpoints {
|
|
12
|
+
uploadUrl: string;
|
|
13
|
+
downloadUrl: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function extractBlobEndpoints(session: Record<string, unknown>): JmapBlobEndpoints;
|
|
11
16
|
export declare function fetchJmapWellKnown(apiUrl: string, capabilityJwt: string): Promise<Record<string, unknown>>;
|
|
12
17
|
/** Minimal surface for JMAP execution (implemented by AgentSession). */
|
|
13
18
|
export interface JmapSessionPort {
|
|
@@ -18,6 +23,8 @@ export interface JmapSessionPort {
|
|
|
18
23
|
getPrimaryMailAccountId(): Promise<string>;
|
|
19
24
|
getCapabilityToken(): Promise<string>;
|
|
20
25
|
readonly currentInboxId?: string;
|
|
26
|
+
readonly currentUploadUrl?: string;
|
|
27
|
+
readonly currentDownloadUrl?: string;
|
|
21
28
|
}
|
|
22
29
|
export interface RunJmapRequestInput {
|
|
23
30
|
session: JmapSessionPort;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-jmap.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-jmap.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,kBAAkB,qEAGrB,CAAC;AAEX,eAAO,MAAM,aAAa,EAAG,2BAAoC,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,OAAO,EAAE,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EAAE,EACtB,MAAM,EAAE,MAAM,GACb,YAAY,CAyBd;AAED,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED,wBAAsB,WAAW,CAC/B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAiBjB;AA8CD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,CAcR;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAclC;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE;QAAE,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3C,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-jmap.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-jmap.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,kBAAkB,qEAGrB,CAAC;AAEX,eAAO,MAAM,aAAa,EAAG,2BAAoC,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,OAAO,EAAE,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EAAE,EACtB,MAAM,EAAE,MAAM,GACb,YAAY,CAyBd;AAED,wBAAgB,kBAAkB,CAChC,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,MAAM,CAER;AAED,wBAAsB,WAAW,CAC/B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CAiBjB;AA8CD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,MAAM,CAcR;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,iBAAiB,CAUnB;AAED,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAclC;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE;QAAE,eAAe,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5C,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3C,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,eAAe,CAAC;IACzB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wFAAwF;IACxF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC/B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAmD5D;AAED,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,YAAY,GACrB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAY5D;AAQD,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAU5D"}
|
|
@@ -93,6 +93,17 @@ export function extractPrimaryMailAccountId(session) {
|
|
|
93
93
|
}
|
|
94
94
|
return id;
|
|
95
95
|
}
|
|
96
|
+
export function extractBlobEndpoints(session) {
|
|
97
|
+
const uploadUrl = session["uploadUrl"];
|
|
98
|
+
const downloadUrl = session["downloadUrl"];
|
|
99
|
+
if (typeof uploadUrl !== "string" || uploadUrl.length === 0) {
|
|
100
|
+
throw new Error("JMAP session missing uploadUrl.");
|
|
101
|
+
}
|
|
102
|
+
if (typeof downloadUrl !== "string" || downloadUrl.length === 0) {
|
|
103
|
+
throw new Error("JMAP session missing downloadUrl.");
|
|
104
|
+
}
|
|
105
|
+
return { uploadUrl, downloadUrl };
|
|
106
|
+
}
|
|
96
107
|
export async function fetchJmapWellKnown(apiUrl, capabilityJwt) {
|
|
97
108
|
const base = apiUrl.replace(/\/+$/, "");
|
|
98
109
|
const res = await fetch(`${base}/.well-known/jmap`, {
|
|
@@ -120,6 +131,11 @@ export async function runJmapRequest(input) {
|
|
|
120
131
|
ACCOUNT_ID: () => input.session.getPrimaryMailAccountId(),
|
|
121
132
|
INBOX: async () => input.session.currentInboxId ??
|
|
122
133
|
(await readCredentials(input.session.files.credentialsFile)).inboxId,
|
|
134
|
+
UPLOAD_URL: async () => input.session.currentUploadUrl ??
|
|
135
|
+
(await readCredentials(input.session.files.credentialsFile)).uploadUrl,
|
|
136
|
+
DOWNLOAD_URL: async () => input.session.currentDownloadUrl ??
|
|
137
|
+
(await readCredentials(input.session.files.credentialsFile))
|
|
138
|
+
.downloadUrl,
|
|
123
139
|
},
|
|
124
140
|
});
|
|
125
141
|
const envelope = parseJmapEnvelope(raw, input.defaultUsing, input.sourceLabel);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-credentials-store.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/session/agent-credentials-store.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-credentials-store.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/session/agent-credentials-store.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAOjE;AAMD,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAmCxE;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAOlC;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;AAED,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAO7B;AAED,0EAA0E;AAC1E,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,UAAU,GAChB,OAAO,CAAC,IAAI,CAAC,CAcf"}
|
|
@@ -28,16 +28,21 @@ export declare class AgentSession {
|
|
|
28
28
|
private sessionJWT;
|
|
29
29
|
private capabilityJWT;
|
|
30
30
|
private cachedMailAccountId;
|
|
31
|
+
private cachedUploadUrl;
|
|
32
|
+
private cachedDownloadUrl;
|
|
31
33
|
constructor(cfg: AgentSessionConfig);
|
|
32
34
|
static create(cfg: AgentSessionConfig): Promise<AgentSession>;
|
|
33
35
|
get hasApiKey(): boolean;
|
|
34
36
|
get currentInboxId(): string | undefined;
|
|
37
|
+
get currentUploadUrl(): string | undefined;
|
|
38
|
+
get currentDownloadUrl(): string | undefined;
|
|
35
39
|
private loadFromDisk;
|
|
36
40
|
/**
|
|
37
41
|
* Primary JMAP mail accountId from GET /.well-known/jmap (cached).
|
|
38
42
|
*/
|
|
39
43
|
getPrimaryMailAccountId(): Promise<string>;
|
|
40
44
|
invalidateJmapSessionCache(): void;
|
|
45
|
+
private refreshJmapSessionData;
|
|
41
46
|
/**
|
|
42
47
|
* Register or return existing inbox when username matches (idempotent).
|
|
43
48
|
* Different username replaces on-disk credentials and creates a new inbox.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/session/agent-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,UAAU,EAMhB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/session/agent-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,UAAU,EAMhB,MAAM,8BAA8B,CAAC;AActC,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAMD,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKtD;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAqB;IACpC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAE3B,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,aAAa,CAAqB;IAC1C,OAAO,CAAC,mBAAmB,CAAqB;IAChD,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,iBAAiB,CAAqB;gBAElC,GAAG,EAAE,kBAAkB;WAUtB,MAAM,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAMnE,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,IAAI,cAAc,IAAI,MAAM,GAAG,SAAS,CAEvC;IAED,IAAI,gBAAgB,IAAI,MAAM,GAAG,SAAS,CAEzC;IAED,IAAI,kBAAkB,IAAI,MAAM,GAAG,SAAS,CAE3C;YAEa,YAAY;IAY1B;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAehD,0BAA0B,IAAI,IAAI;YAMpB,sBAAsB;IASpC;;;OAGG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IA8EnD,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC;YA4B7B,aAAa;IA2B3B,OAAO,IAAI,IAAI;CAGhB;AAED,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,CAAC;IAClB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAED,0EAA0E;AAC1E,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,2BAA2B,GACjC,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CA6B9B"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Stateful PoW + capability JWT + optional cached JMAP session (accountId).
|
|
2
2
|
import { tryReadCredentials, tryReadJwtFile, unlinkCredentialArtifacts, writeCredentials, writeJwtFile, } from "./agent-credentials-store.js";
|
|
3
3
|
import { CAPABILITY_SAFETY_MARGIN_MS, decodeJwtPayload, isJwtExpired, SESSION_SAFETY_MARGIN_MS, } from "../auth/agent-jwt.js";
|
|
4
|
-
import { extractPrimaryMailAccountId, fetchJmapWellKnown, } from "../jmap/agent-jmap.js";
|
|
4
|
+
import { extractBlobEndpoints, extractPrimaryMailAccountId, fetchJmapWellKnown, } from "../jmap/agent-jmap.js";
|
|
5
5
|
import { fetchCapability, performPoWAndSession } from "../auth/agent-auth-http.js";
|
|
6
6
|
function normalizeUsername(u) {
|
|
7
7
|
return u.trim().toLowerCase();
|
|
@@ -24,6 +24,8 @@ export class AgentSession {
|
|
|
24
24
|
sessionJWT;
|
|
25
25
|
capabilityJWT;
|
|
26
26
|
cachedMailAccountId;
|
|
27
|
+
cachedUploadUrl;
|
|
28
|
+
cachedDownloadUrl;
|
|
27
29
|
constructor(cfg) {
|
|
28
30
|
this.authUrl = cfg.authUrl.replace(/\/+$/, "");
|
|
29
31
|
this.apiUrl = cfg.apiUrl.replace(/\/+$/, "");
|
|
@@ -44,6 +46,12 @@ export class AgentSession {
|
|
|
44
46
|
get currentInboxId() {
|
|
45
47
|
return this.inboxId;
|
|
46
48
|
}
|
|
49
|
+
get currentUploadUrl() {
|
|
50
|
+
return this.cachedUploadUrl;
|
|
51
|
+
}
|
|
52
|
+
get currentDownloadUrl() {
|
|
53
|
+
return this.cachedDownloadUrl;
|
|
54
|
+
}
|
|
47
55
|
async loadFromDisk() {
|
|
48
56
|
this.sessionJWT = await tryReadJwtFile(this.files.sessionFile);
|
|
49
57
|
this.capabilityJWT = await tryReadJwtFile(this.files.capabilityFile);
|
|
@@ -51,22 +59,37 @@ export class AgentSession {
|
|
|
51
59
|
if (disk) {
|
|
52
60
|
this.apiKey = this.apiKey ?? disk.apiKey;
|
|
53
61
|
this.inboxId = this.inboxId ?? disk.inboxId;
|
|
62
|
+
this.cachedUploadUrl = disk.uploadUrl;
|
|
63
|
+
this.cachedDownloadUrl = disk.downloadUrl;
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
/**
|
|
57
67
|
* Primary JMAP mail accountId from GET /.well-known/jmap (cached).
|
|
58
68
|
*/
|
|
59
69
|
async getPrimaryMailAccountId() {
|
|
60
|
-
if (this.cachedMailAccountId
|
|
70
|
+
if (this.cachedMailAccountId &&
|
|
71
|
+
this.cachedUploadUrl &&
|
|
72
|
+
this.cachedDownloadUrl) {
|
|
61
73
|
return this.cachedMailAccountId;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
}
|
|
75
|
+
await this.refreshJmapSessionData();
|
|
76
|
+
if (!this.cachedMailAccountId) {
|
|
77
|
+
throw new Error("JMAP session missing primary mail account id.");
|
|
78
|
+
}
|
|
79
|
+
return this.cachedMailAccountId;
|
|
67
80
|
}
|
|
68
81
|
invalidateJmapSessionCache() {
|
|
69
82
|
this.cachedMailAccountId = undefined;
|
|
83
|
+
this.cachedUploadUrl = undefined;
|
|
84
|
+
this.cachedDownloadUrl = undefined;
|
|
85
|
+
}
|
|
86
|
+
async refreshJmapSessionData() {
|
|
87
|
+
const cap = await this.getCapabilityToken();
|
|
88
|
+
const session = await fetchJmapWellKnown(this.apiUrl, cap);
|
|
89
|
+
this.cachedMailAccountId = extractPrimaryMailAccountId(session);
|
|
90
|
+
const blobs = extractBlobEndpoints(session);
|
|
91
|
+
this.cachedUploadUrl = blobs.uploadUrl;
|
|
92
|
+
this.cachedDownloadUrl = blobs.downloadUrl;
|
|
70
93
|
}
|
|
71
94
|
/**
|
|
72
95
|
* Register or return existing inbox when username matches (idempotent).
|
|
@@ -115,15 +138,22 @@ export class AgentSession {
|
|
|
115
138
|
}
|
|
116
139
|
this.inboxId = claims.inboxId;
|
|
117
140
|
this.cachedMailAccountId = undefined;
|
|
141
|
+
this.cachedUploadUrl = undefined;
|
|
142
|
+
this.cachedDownloadUrl = undefined;
|
|
143
|
+
const accountId = await this.getPrimaryMailAccountId();
|
|
144
|
+
if (!this.cachedUploadUrl || !this.cachedDownloadUrl) {
|
|
145
|
+
throw new Error("JMAP session did not provide upload/download URLs.");
|
|
146
|
+
}
|
|
118
147
|
const creds = {
|
|
119
148
|
apiKey: this.apiKey,
|
|
120
149
|
inboxId: this.inboxId,
|
|
121
150
|
authUrl: this.authUrl,
|
|
122
151
|
apiUrl: this.apiUrl,
|
|
123
152
|
scryptSalt: this.scryptSalt,
|
|
153
|
+
uploadUrl: this.cachedUploadUrl,
|
|
154
|
+
downloadUrl: this.cachedDownloadUrl,
|
|
124
155
|
};
|
|
125
156
|
await writeCredentials(this.files.credentialsFile, creds);
|
|
126
|
-
const accountId = await this.getPrimaryMailAccountId();
|
|
127
157
|
return {
|
|
128
158
|
inbox: this.inboxId,
|
|
129
159
|
accountId,
|
|
@@ -171,6 +201,8 @@ export class AgentSession {
|
|
|
171
201
|
this.sessionJWT = result.sessionJWT;
|
|
172
202
|
this.capabilityJWT = undefined;
|
|
173
203
|
this.cachedMailAccountId = undefined;
|
|
204
|
+
this.cachedUploadUrl = undefined;
|
|
205
|
+
this.cachedDownloadUrl = undefined;
|
|
174
206
|
await writeJwtFile(this.files.sessionFile, this.sessionJWT);
|
|
175
207
|
}
|
|
176
208
|
destroy() {
|
|
@@ -193,12 +225,16 @@ export async function persistLoginWithApiKey(input) {
|
|
|
193
225
|
if (typeof inboxId !== "string" || inboxId.length === 0) {
|
|
194
226
|
throw new Error("Capability JWT did not contain an inboxId claim.");
|
|
195
227
|
}
|
|
228
|
+
const jmapSession = await fetchJmapWellKnown(apiUrl, capabilityJWT);
|
|
229
|
+
const blobs = extractBlobEndpoints(jmapSession);
|
|
196
230
|
await writeCredentials(input.files.credentialsFile, {
|
|
197
231
|
apiKey: input.apiKey,
|
|
198
232
|
inboxId,
|
|
199
233
|
authUrl,
|
|
200
234
|
apiUrl,
|
|
201
235
|
scryptSalt: input.scryptSalt,
|
|
236
|
+
uploadUrl: blobs.uploadUrl,
|
|
237
|
+
downloadUrl: blobs.downloadUrl,
|
|
202
238
|
});
|
|
203
239
|
await writeJwtFile(input.files.sessionFile, session.sessionJWT);
|
|
204
240
|
await writeJwtFile(input.files.capabilityFile, capabilityJWT);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads README.md from the npm package root (next to package.json).
|
|
3
|
+
* Intended for published @atomicmail/mcp and @atomicmail/agent-skill layouts.
|
|
4
|
+
*/
|
|
5
|
+
export declare function readNpmPackageReadme(): Promise<string>;
|
|
6
|
+
//# sourceMappingURL=read-npm-package-readme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"read-npm-package-readme.d.ts","sourceRoot":"","sources":["../../../src/lib/core/read-npm-package-readme.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CA+C5D"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const MAX_DEPTH = 16;
|
|
5
|
+
const ATOMICMAIL_NPM_NAMES = new Set([
|
|
6
|
+
"@atomicmail/mcp",
|
|
7
|
+
"@atomicmail/agent-skill",
|
|
8
|
+
]);
|
|
9
|
+
function isEnoent(err) {
|
|
10
|
+
if (!(err instanceof Error))
|
|
11
|
+
return false;
|
|
12
|
+
const code = err.code;
|
|
13
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Reads README.md from the npm package root (next to package.json).
|
|
17
|
+
* Intended for published @atomicmail/mcp and @atomicmail/agent-skill layouts.
|
|
18
|
+
*/
|
|
19
|
+
export async function readNpmPackageReadme() {
|
|
20
|
+
const moduleDir = dirname(fileURLToPath(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
|
|
21
|
+
let currentDir = moduleDir;
|
|
22
|
+
for (let i = 0; i < MAX_DEPTH; i++) {
|
|
23
|
+
const pkgPath = resolve(currentDir, "package.json");
|
|
24
|
+
const readmePath = resolve(currentDir, "README.md");
|
|
25
|
+
let pkgRaw;
|
|
26
|
+
try {
|
|
27
|
+
pkgRaw = await readFile(pkgPath, "utf-8");
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (!isEnoent(err))
|
|
31
|
+
throw err;
|
|
32
|
+
const parent = resolve(currentDir, "..");
|
|
33
|
+
if (parent === currentDir)
|
|
34
|
+
break;
|
|
35
|
+
currentDir = parent;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
let name;
|
|
39
|
+
try {
|
|
40
|
+
name = JSON.parse(pkgRaw).name;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
name = undefined;
|
|
44
|
+
}
|
|
45
|
+
if (!name || !ATOMICMAIL_NPM_NAMES.has(name)) {
|
|
46
|
+
const parent = resolve(currentDir, "..");
|
|
47
|
+
if (parent === currentDir)
|
|
48
|
+
break;
|
|
49
|
+
currentDir = parent;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
return await readFile(readmePath, "utf-8");
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
if (!isEnoent(err))
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
const parent = resolve(currentDir, "..");
|
|
60
|
+
if (parent === currentDir)
|
|
61
|
+
break;
|
|
62
|
+
currentDir = parent;
|
|
63
|
+
}
|
|
64
|
+
throw new Error("Could not find Atomic Mail package README.md — use a published npm install " +
|
|
65
|
+
"(@atomicmail/mcp or @atomicmail/agent-skill) for --topic readme.");
|
|
66
|
+
}
|
package/esm/lib/mod.d.ts
CHANGED
package/esm/lib/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/lib/mod.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,yCAAyC,CAAC;AACxD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC"}
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/lib/mod.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mCAAmC,CAAC;AAClD,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iCAAiC,CAAC;AAChD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,kCAAkC,CAAC;AACjD,cAAc,yCAAyC,CAAC;AACxD,cAAc,oCAAoC,CAAC;AACnD,cAAc,4BAA4B,CAAC"}
|
package/esm/lib/mod.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../../src/lib/network/auth-client.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,iEAAiE;AACjE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;gBAEL,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM9D;AAOD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,iBAAiB;IAKtC;;;;OAIG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"auth-client.d.ts","sourceRoot":"","sources":["../../../src/lib/network/auth-client.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,iBAAiB;IAChC,6FAA6F;IAC7F,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,iEAAiE;AACjE,qBAAa,eAAgB,SAAQ,KAAK;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;gBAEL,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAM9D;AAOD,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,iBAAiB;IAKtC;;;;OAIG;IACG,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAyBrD,4DAA4D;IACtD,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAgBjD;;;OAGG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;YAoBvC,cAAc;YAsCd,WAAW;YAyCX,gBAAgB;IAuB9B;;;;;;;OAOG;YACW,QAAQ;CAgBvB"}
|
|
@@ -40,32 +40,26 @@ export class AuthClient {
|
|
|
40
40
|
async signup(username) {
|
|
41
41
|
const { challengeJWT, challenge, difficulty } = await this.fetchChallenge();
|
|
42
42
|
const { powHex, nonce } = await this.solvePoW(challenge, difficulty);
|
|
43
|
-
const data = await this.postSession({
|
|
44
|
-
challengeJWT,
|
|
43
|
+
const { sessionJWT, data } = await this.postSession(challengeJWT, {
|
|
45
44
|
powHex,
|
|
46
45
|
nonce: nonce.toString(),
|
|
47
46
|
username,
|
|
48
47
|
});
|
|
49
|
-
if (typeof data.apiKey !== "string"
|
|
50
|
-
|
|
51
|
-
throw new AuthClientError(200, JSON.stringify(data), "Signup response missing apiKey or sessionJWT.");
|
|
48
|
+
if (typeof data.apiKey !== "string") {
|
|
49
|
+
throw new AuthClientError(200, JSON.stringify(data), "Signup response missing apiKey.");
|
|
52
50
|
}
|
|
53
|
-
return { apiKey: data.apiKey, sessionJWT
|
|
51
|
+
return { apiKey: data.apiKey, sessionJWT };
|
|
54
52
|
}
|
|
55
53
|
/** Exchange an existing API key for a fresh session JWT. */
|
|
56
54
|
async login(apiKey) {
|
|
57
55
|
const { challengeJWT, challenge, difficulty } = await this.fetchChallenge();
|
|
58
56
|
const { powHex, nonce } = await this.solvePoW(challenge, difficulty);
|
|
59
|
-
const
|
|
60
|
-
challengeJWT,
|
|
57
|
+
const { sessionJWT } = await this.postSession(challengeJWT, {
|
|
61
58
|
powHex,
|
|
62
59
|
nonce: nonce.toString(),
|
|
63
60
|
apiKey,
|
|
64
61
|
});
|
|
65
|
-
|
|
66
|
-
throw new AuthClientError(200, JSON.stringify(data), "Login response missing sessionJWT.");
|
|
67
|
-
}
|
|
68
|
-
return { sessionJWT: data.sessionJWT };
|
|
62
|
+
return { sessionJWT };
|
|
69
63
|
}
|
|
70
64
|
/**
|
|
71
65
|
* Exchange a session JWT for a short-lived capability JWT (audience:
|
|
@@ -76,38 +70,57 @@ export class AuthClient {
|
|
|
76
70
|
method: "POST",
|
|
77
71
|
headers: { Authorization: `Bearer ${sessionJWT}` },
|
|
78
72
|
});
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
throw new AuthClientError(res.status,
|
|
73
|
+
const text = await res.text();
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
throw new AuthClientError(res.status, text, `auth-service capability returned ${res.status}: ${text}`);
|
|
82
76
|
}
|
|
83
|
-
|
|
77
|
+
const capabilityJWT = readBearerToken(res.headers.get("Authorization"), "Capability response missing Authorization bearer token.");
|
|
78
|
+
return { capabilityJWT };
|
|
84
79
|
}
|
|
85
80
|
async fetchChallenge() {
|
|
86
81
|
const res = await fetch(`${this.baseUrl}/api/v1/challenge`, {
|
|
87
82
|
method: "POST",
|
|
88
83
|
});
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
throw new AuthClientError(res.status,
|
|
84
|
+
const text = await res.text();
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
throw new AuthClientError(res.status, text, `auth-service challenge returned ${res.status}: ${text}`);
|
|
92
87
|
}
|
|
93
|
-
const
|
|
88
|
+
const challengeJWT = readBearerToken(res.headers.get("Authorization"), "Challenge response missing Authorization bearer token.");
|
|
89
|
+
const payload = decodeJwtPayload(challengeJWT);
|
|
94
90
|
if (typeof payload.jti !== "string" ||
|
|
95
91
|
typeof payload.difficulty !== "number") {
|
|
96
|
-
throw new AuthClientError(res.status,
|
|
92
|
+
throw new AuthClientError(res.status, challengeJWT, "Challenge JWT payload is malformed (missing jti or difficulty).");
|
|
97
93
|
}
|
|
98
94
|
return {
|
|
99
|
-
challengeJWT
|
|
95
|
+
challengeJWT,
|
|
100
96
|
challenge: payload.jti,
|
|
101
97
|
difficulty: payload.difficulty,
|
|
102
98
|
};
|
|
103
99
|
}
|
|
104
|
-
async postSession(body) {
|
|
100
|
+
async postSession(challengeJWT, body) {
|
|
105
101
|
const res = await fetch(`${this.baseUrl}/api/v1/session`, {
|
|
106
102
|
method: "POST",
|
|
107
|
-
headers: {
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
Authorization: `Bearer ${challengeJWT}`,
|
|
106
|
+
},
|
|
108
107
|
body: JSON.stringify(body),
|
|
109
108
|
});
|
|
110
|
-
|
|
109
|
+
const text = await res.text();
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
throw new AuthClientError(res.status, text, `auth-service session returned ${res.status}: ${text}`);
|
|
112
|
+
}
|
|
113
|
+
const sessionJWT = readBearerToken(res.headers.get("Authorization"), "Session response missing Authorization bearer token.");
|
|
114
|
+
let data = {};
|
|
115
|
+
if (text.trim().length > 0) {
|
|
116
|
+
try {
|
|
117
|
+
data = JSON.parse(text);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new AuthClientError(res.status, text, "auth-service session returned non-JSON body.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { sessionJWT, data };
|
|
111
124
|
}
|
|
112
125
|
async parseJsonOrThrow(res, endpoint) {
|
|
113
126
|
const text = await res.text();
|
|
@@ -186,3 +199,13 @@ function decodeJwtPayload(jwt) {
|
|
|
186
199
|
.padEnd(payloadB64Url.length + padLen, "=");
|
|
187
200
|
return JSON.parse(atob(base64));
|
|
188
201
|
}
|
|
202
|
+
function readBearerToken(headerValue, missingError) {
|
|
203
|
+
if (!headerValue) {
|
|
204
|
+
throw new Error(missingError);
|
|
205
|
+
}
|
|
206
|
+
const match = /^\s*Bearer\s+(.+?)\s*$/i.exec(headerValue);
|
|
207
|
+
if (!match || !match[1]) {
|
|
208
|
+
throw new Error("Authorization header must use Bearer scheme.");
|
|
209
|
+
}
|
|
210
|
+
return match[1];
|
|
211
|
+
}
|
package/esm/mcp/main.js
CHANGED
|
@@ -20,7 +20,8 @@ WORKFLOW
|
|
|
20
20
|
1. Call register with a desired username (PoW signup; credentials on disk).
|
|
21
21
|
2. Call jmap_request with JMAP method calls (inline ops JSON or ops_file preset).
|
|
22
22
|
$VAR_NAME tokens: $ACCOUNT_ID / $INBOX from session; pass others in vars.
|
|
23
|
-
3. Call help for full documentation (JMAP cheatsheet, presets, troubleshooting)
|
|
23
|
+
3. Call help for full documentation (JMAP cheatsheet, presets, troubleshooting);
|
|
24
|
+
topic readme returns the npm package README.
|
|
24
25
|
|
|
25
26
|
CREDENTIAL DIRECTORY
|
|
26
27
|
Default ~/.atomicmail/ (override ATOMIC_MAIL_CREDENTIALS_DIR). Same files as
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/help.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/help.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAStE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAmDxD"}
|
package/esm/mcp/tools/help.js
CHANGED
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { getHelp, HELP_TOPIC_LIST } from "../../lib/mod.js";
|
|
2
|
+
import { getHelp, HELP_TOPIC_LIST, normalizeHelpTopic, readNpmPackageReadme, } from "../../lib/mod.js";
|
|
3
3
|
export function registerHelpTool(server) {
|
|
4
4
|
server.registerTool("help", {
|
|
5
5
|
title: "Atomic Mail documentation",
|
|
6
6
|
description: "Return in-depth documentation: JMAP cheatsheet, presets, auth flow, " +
|
|
7
|
-
"troubleshooting
|
|
8
|
-
|
|
7
|
+
"troubleshooting; or the published package README when topic is readme. " +
|
|
8
|
+
"Optional topic. Topics: " +
|
|
9
|
+
HELP_TOPIC_LIST.join(", ") +
|
|
10
|
+
", readme.",
|
|
9
11
|
inputSchema: z.object({
|
|
10
12
|
topic: z
|
|
11
13
|
.string()
|
|
12
14
|
.optional()
|
|
13
|
-
.describe(`Topic (e.g. ${HELP_TOPIC_LIST.slice(0, 4).join(", ")},
|
|
15
|
+
.describe(`Topic (e.g. ${HELP_TOPIC_LIST.slice(0, 4).join(", ")}, ..., readme). Use readme for the npm package README.md. Omit for overview.`),
|
|
14
16
|
}),
|
|
15
17
|
annotations: {
|
|
16
18
|
readOnlyHint: true,
|
|
17
19
|
idempotentHint: true,
|
|
18
20
|
},
|
|
19
|
-
}, ({ topic }) =>
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
}, async ({ topic }) => {
|
|
22
|
+
try {
|
|
23
|
+
if (topic !== undefined && normalizeHelpTopic(topic) === "readme") {
|
|
24
|
+
const text = await readNpmPackageReadme();
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text }],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: getHelp(topic) }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: error instanceof Error ? error.message : String(error),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
22
45
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jmap.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/jmap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEtE,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,kBAAkB,CAAC;AAE1B,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,GACpB,IAAI,
|
|
1
|
+
{"version":3,"file":"jmap.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/jmap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sCAAsC,CAAC;AAEtE,OAAO,EACL,KAAK,YAAY,EAIlB,MAAM,kBAAkB,CAAC;AAE1B,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,OAAO,EAAE,YAAY,GACpB,IAAI,CAoIN"}
|
package/esm/mcp/tools/jmap.js
CHANGED
|
@@ -7,8 +7,9 @@ export function registerJmapTool(server, session) {
|
|
|
7
7
|
"Provide exactly one of: `ops` (JSON string — methodCalls array or full " +
|
|
8
8
|
"envelope) or `ops_file` (preset path; relative paths resolve against " +
|
|
9
9
|
"the credential directory). Tokens `$VAR_NAME` (uppercase `$FOO_BAR`) in " +
|
|
10
|
-
"either input are replaced: `$ACCOUNT_ID
|
|
11
|
-
"session; pass other names
|
|
10
|
+
"either input are replaced: `$ACCOUNT_ID`, `$INBOX`, `$UPLOAD_URL`, and " +
|
|
11
|
+
"`$DOWNLOAD_URL` come from the JMAP session/credentials; pass other names " +
|
|
12
|
+
"via `vars`.",
|
|
12
13
|
inputSchema: z.object({
|
|
13
14
|
using: z
|
|
14
15
|
.array(z.string())
|
|
@@ -29,7 +30,7 @@ export function registerJmapTool(server, session) {
|
|
|
29
30
|
.optional()
|
|
30
31
|
.describe("Map of placeholder names (no `$`) to string values, e.g. " +
|
|
31
32
|
'{ "TO": "a@b.com", "SUBJECT": "Hi" } for `$TO` and `$SUBJECT` in ' +
|
|
32
|
-
"ops or ops_file. Overrides session values for `ACCOUNT_ID`
|
|
33
|
+
"ops or ops_file. Overrides session values for `ACCOUNT_ID`, `INBOX`, `UPLOAD_URL`, or `DOWNLOAD_URL` if set."),
|
|
33
34
|
}),
|
|
34
35
|
}, async ({ using, ops, ops_file, vars }) => {
|
|
35
36
|
try {
|