@atomicmail/agent-skill 0.1.0 → 0.2.0
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 +56 -0
- package/SKILL.md +67 -187
- package/esm/{skill/scripts/lib/auth.d.ts → lib/src/agent-auth-http.d.ts} +1 -17
- package/esm/lib/src/agent-auth-http.d.ts.map +1 -0
- package/esm/lib/src/agent-auth-http.js +76 -0
- package/esm/{skill/scripts/lib/credentials.d.ts → lib/src/agent-credentials-store.d.ts} +4 -1
- package/esm/lib/src/agent-credentials-store.d.ts.map +1 -0
- package/esm/{skill/scripts/lib/credentials.js → lib/src/agent-credentials-store.js} +28 -8
- package/esm/lib/src/agent-help-content.d.ts +4 -0
- package/esm/lib/src/agent-help-content.d.ts.map +1 -0
- package/esm/lib/src/agent-help-content.js +236 -0
- package/esm/lib/src/agent-jmap.d.ts +49 -0
- package/esm/lib/src/agent-jmap.d.ts.map +1 -0
- package/esm/lib/src/agent-jmap.js +130 -0
- package/esm/lib/src/agent-jwt.d.ts +14 -0
- package/esm/lib/src/agent-jwt.d.ts.map +1 -0
- package/esm/lib/src/agent-jwt.js +29 -0
- package/esm/lib/src/agent-pow.d.ts +5 -0
- package/esm/lib/src/agent-pow.d.ts.map +1 -0
- package/esm/lib/src/agent-pow.js +49 -0
- package/esm/lib/src/agent-session.d.ts +62 -0
- package/esm/lib/src/agent-session.d.ts.map +1 -0
- package/esm/lib/src/agent-session.js +206 -0
- package/esm/lib/src/agent-vars.d.ts +23 -0
- package/esm/lib/src/agent-vars.d.ts.map +1 -0
- package/esm/lib/src/agent-vars.js +65 -0
- package/esm/skill/scripts/cli.d.ts +3 -0
- package/esm/skill/scripts/cli.d.ts.map +1 -0
- package/esm/skill/scripts/cli.js +309 -0
- package/package.json +3 -4
- package/esm/skill/scripts/jmap_request.d.ts +0 -3
- package/esm/skill/scripts/jmap_request.d.ts.map +0 -1
- package/esm/skill/scripts/jmap_request.js +0 -265
- package/esm/skill/scripts/lib/auth.d.ts.map +0 -1
- package/esm/skill/scripts/lib/auth.js +0 -163
- package/esm/skill/scripts/lib/credentials.d.ts.map +0 -1
- package/esm/skill/scripts/signup.d.ts +0 -3
- package/esm/skill/scripts/signup.d.ts.map +0 -1
- package/esm/skill/scripts/signup.js +0 -170
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// Long-form help for MCP `help` tool and AgentSkill `help` command.
|
|
2
|
+
export const HELP_TOPICS = {
|
|
3
|
+
overview: `\
|
|
4
|
+
# Atomic Mail — Overview
|
|
5
|
+
|
|
6
|
+
Atomic Mail is an email service provider (ESP) designed for AI agents. You
|
|
7
|
+
manage mail over JMAP (RFC 8620 + RFC 8621).
|
|
8
|
+
|
|
9
|
+
## Public surface (identical for MCP and AgentSkill)
|
|
10
|
+
|
|
11
|
+
Three operations only:
|
|
12
|
+
|
|
13
|
+
1. **register** — Proof-of-work signup (or idempotent replay when the same
|
|
14
|
+
username matches the inbox already on disk). Persists credentials and
|
|
15
|
+
returns \`{ inbox, accountId }\` (and \`apiKey\` on first signup).
|
|
16
|
+
2. **jmap_request** — Send a JMAP method-call batch. Auth and JWT rotation are
|
|
17
|
+
automatic. Pass inline \`ops\` JSON or an \`ops_file\` preset path (same
|
|
18
|
+
substitution applies to both). Uppercase tokens like \`$ACCOUNT_ID\`,
|
|
19
|
+
\`$INBOX\`, \`$TO\`, \`$SUBJECT\` are replaced before the request is sent.
|
|
20
|
+
\`$ACCOUNT_ID\` / \`$INBOX\` come from the JMAP session and credentials; pass
|
|
21
|
+
any other names via MCP \`vars\` or skill \`--vars\`.
|
|
22
|
+
3. **help** — This documentation (optional \`topic\` / \`--topic\`).
|
|
23
|
+
|
|
24
|
+
## Typical workflow
|
|
25
|
+
|
|
26
|
+
1. \`register\` with a username.
|
|
27
|
+
2. \`jmap_request\` with JMAP method calls (presets may use \`$VAR_NAME\`; pass
|
|
28
|
+
custom values in \`vars\` / \`--vars\`).
|
|
29
|
+
3. If stuck, read error hints and call \`help\`.
|
|
30
|
+
|
|
31
|
+
Available topics: overview, installation, auth, jmap_cheatsheet, tools,
|
|
32
|
+
presets, troubleshooting.`,
|
|
33
|
+
installation: `\
|
|
34
|
+
# Atomic Mail — Installation
|
|
35
|
+
|
|
36
|
+
## MCP (stdio)
|
|
37
|
+
|
|
38
|
+
\`\`\`json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"atomicmail": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["-y", "@atomicmail/mcp"]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
## AgentSkill (shell)
|
|
50
|
+
|
|
51
|
+
\`\`\`bash
|
|
52
|
+
npx --package=@atomicmail/agent-skill atomicmail register --username "myagent"
|
|
53
|
+
npx --package=@atomicmail/agent-skill atomicmail jmap_request \\
|
|
54
|
+
--ops-file send_hello.json
|
|
55
|
+
npx --package=@atomicmail/agent-skill atomicmail help
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
From the repo: \`deno run -A scripts/cli.ts <command> ...\` inside \`skill/\`.
|
|
59
|
+
|
|
60
|
+
## Shared credentials
|
|
61
|
+
|
|
62
|
+
MCP and the skill use the same directory layout (default \`~/.atomicmail/\`):
|
|
63
|
+
|
|
64
|
+
- \`credentials.json\`, \`session.jwt\`, \`capability.jwt\`
|
|
65
|
+
|
|
66
|
+
## Overriding defaults
|
|
67
|
+
|
|
68
|
+
- Endpoints: \`ATOMIC_MAIL_AUTH_URL\`, \`ATOMIC_MAIL_API_URL\`
|
|
69
|
+
- Credentials path: \`ATOMIC_MAIL_CREDENTIALS_DIR\` (MCP), \`--credentials-dir\` (skill)
|
|
70
|
+
- Optional PoW salt: \`ATOMIC_MAIL_SCRYPT_SALT\`
|
|
71
|
+
|
|
72
|
+
## From source (development)
|
|
73
|
+
|
|
74
|
+
From the repo \`mcp/\` or \`skill/\` directory:
|
|
75
|
+
|
|
76
|
+
\`\`\`bash
|
|
77
|
+
deno task start # MCP
|
|
78
|
+
deno task build:npm
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
See each package README for details.`,
|
|
82
|
+
auth: `\
|
|
83
|
+
# Atomic Mail — Auth flow
|
|
84
|
+
|
|
85
|
+
Auth is automatic after \`register\` (or when \`credentials.json\` + API key
|
|
86
|
+
exist).
|
|
87
|
+
|
|
88
|
+
1. **Challenge** — \`POST /api/v1/challenge\`
|
|
89
|
+
2. **Proof-of-work** — scrypt until difficulty satisfied
|
|
90
|
+
3. **Session JWT** — \`POST /api/v1/session\` (4h TTL); signup returns a
|
|
91
|
+
one-time \`apiKey\`
|
|
92
|
+
4. **Capability JWT** — \`POST /api/v1/capability\` (2 min TTL) used as the
|
|
93
|
+
JMAP bearer
|
|
94
|
+
|
|
95
|
+
JWTs are rotated before expiry and written back to disk.
|
|
96
|
+
|
|
97
|
+
## Credential files (mode 0600)
|
|
98
|
+
|
|
99
|
+
\`credentials.json\` — \`{ apiKey, inboxId, authUrl, apiUrl, scryptSalt }\`
|
|
100
|
+
\`session.jwt\` — session token
|
|
101
|
+
\`capability.jwt\` — capability token
|
|
102
|
+
|
|
103
|
+
## Overriding defaults
|
|
104
|
+
|
|
105
|
+
- \`ATOMIC_MAIL_AUTH_URL\` (default: \`https://auth.atomicmail.ai\`)
|
|
106
|
+
- \`ATOMIC_MAIL_API_URL\` (default: \`https://api.atomicmail.ai\`)
|
|
107
|
+
- \`ATOMIC_MAIL_SCRYPT_SALT\` (optional)
|
|
108
|
+
- \`ATOMIC_MAIL_API_KEY\` (optional)
|
|
109
|
+
- \`ATOMIC_MAIL_CREDENTIALS_DIR\` (default: \`~/.atomicmail\`)`,
|
|
110
|
+
jmap_cheatsheet: `\
|
|
111
|
+
# JMAP cheatsheet
|
|
112
|
+
|
|
113
|
+
## Capabilities (\`using\`)
|
|
114
|
+
|
|
115
|
+
- urn:ietf:params:jmap:core
|
|
116
|
+
- urn:ietf:params:jmap:mail
|
|
117
|
+
- urn:ietf:params:jmap:submission
|
|
118
|
+
|
|
119
|
+
## Examples
|
|
120
|
+
|
|
121
|
+
Use \`$ACCOUNT_ID\` and \`$INBOX\` for session fields; use \`$TO\`, \`$SUBJECT\`,
|
|
122
|
+
etc., and supply values via MCP \`vars\` or \`--vars\` (JSON object of strings).
|
|
123
|
+
|
|
124
|
+
### Mailboxes
|
|
125
|
+
\`\`\`json
|
|
126
|
+
["Mailbox/get", {"accountId": "$ACCOUNT_ID"}, "m0"]
|
|
127
|
+
\`\`\`
|
|
128
|
+
|
|
129
|
+
### Query + fetch emails
|
|
130
|
+
\`\`\`json
|
|
131
|
+
["Email/query", {
|
|
132
|
+
"accountId": "$ACCOUNT_ID",
|
|
133
|
+
"filter": {"inMailbox": "$INBOX"},
|
|
134
|
+
"sort": [{"property": "receivedAt", "isAscending": false}],
|
|
135
|
+
"limit": 100
|
|
136
|
+
}, "q0"]
|
|
137
|
+
\`\`\`
|
|
138
|
+
|
|
139
|
+
### Send (add submission to \`using\`)
|
|
140
|
+
|
|
141
|
+
Include \`urn:ietf:params:jmap:submission\` in the envelope \`using\` array
|
|
142
|
+
when calling \`EmailSubmission/set\`.
|
|
143
|
+
|
|
144
|
+
## Tips
|
|
145
|
+
|
|
146
|
+
- Back-references (\`#ids\`, \`#draft\`) chain calls in one batch.
|
|
147
|
+
- Save reusable JSON as preset files and pass \`ops_file\`.`,
|
|
148
|
+
tools: `\
|
|
149
|
+
# Tool / CLI reference
|
|
150
|
+
|
|
151
|
+
## register
|
|
152
|
+
|
|
153
|
+
**MCP input:** \`{ "username": string }\`
|
|
154
|
+
**Skill:** \`register --username NAME\` (or \`--api-key KEY\`).
|
|
155
|
+
|
|
156
|
+
Creates an inbox or returns the same \`{ inbox, accountId }\` when the
|
|
157
|
+
username matches the stored inbox local-part. A **different** username
|
|
158
|
+
replaces credentials in the directory and registers a new inbox.
|
|
159
|
+
|
|
160
|
+
## jmap_request
|
|
161
|
+
|
|
162
|
+
**MCP input:** \`{ "using"?: string[], "ops"?: string, "ops_file"?: string,
|
|
163
|
+
"vars"?: Record<string, string> }\` — keys in \`vars\` are names without \`$\`
|
|
164
|
+
(e.g. \`TO\` for \`$TO\`). Exactly one of \`ops\` or \`ops_file\`.
|
|
165
|
+
|
|
166
|
+
**Skill:** \`jmap_request --ops '...'\` or \`--ops-file path\` plus
|
|
167
|
+
\`--credentials-dir\` (optional), plus optional \`--vars '<json>'\`, \`--using\`,
|
|
168
|
+
\`--dry-run\`.
|
|
169
|
+
|
|
170
|
+
## help
|
|
171
|
+
|
|
172
|
+
**MCP:** \`{ "topic"?: string }\`
|
|
173
|
+
**Skill:** \`help [--topic TOPIC]\`
|
|
174
|
+
|
|
175
|
+
Topics: overview, installation, auth, jmap_cheatsheet, tools, presets,
|
|
176
|
+
troubleshooting.`,
|
|
177
|
+
presets: `\
|
|
178
|
+
# JMAP presets
|
|
179
|
+
|
|
180
|
+
Save a method-call array or a full \`{ "using", "methodCalls" }\` envelope
|
|
181
|
+
as JSON, then pass \`ops_file\` (MCP) or \`--ops-file\` (skill).
|
|
182
|
+
|
|
183
|
+
Relative paths resolve against the credential directory (MCP) or current
|
|
184
|
+
\`--credentials-dir\` (skill).
|
|
185
|
+
|
|
186
|
+
## Placeholders
|
|
187
|
+
|
|
188
|
+
Syntax: \`$VAR_NAME\` where \`VAR_NAME\` matches \`/^[A-Z][A-Z0-9_]*$/\` (so JMAP
|
|
189
|
+
keywords like \`$draft\` stay untouched).
|
|
190
|
+
|
|
191
|
+
- \`$ACCOUNT_ID\` — primary mail account id (from \`GET /.well-known/jmap\` when
|
|
192
|
+
referenced).
|
|
193
|
+
- \`$INBOX\` — inbox email address from credentials.
|
|
194
|
+
- Any other \`$FOO\` — must appear in MCP \`vars\` or skill \`--vars\` as
|
|
195
|
+
\`"FOO": "..."\` (string values only; JSON escaping in the preset body is your
|
|
196
|
+
responsibility).
|
|
197
|
+
|
|
198
|
+
You may override \`ACCOUNT_ID\` / \`INBOX\` via \`vars\` / \`--vars\` if needed.`,
|
|
199
|
+
troubleshooting: `\
|
|
200
|
+
# Troubleshooting
|
|
201
|
+
|
|
202
|
+
## Custom endpoint configuration issues
|
|
203
|
+
|
|
204
|
+
Defaults are production endpoints. Set env vars only for custom deployments.
|
|
205
|
+
|
|
206
|
+
## No API key / register first
|
|
207
|
+
|
|
208
|
+
Run \`register\`, or set \`ATOMIC_MAIL_API_KEY\`, or copy an existing
|
|
209
|
+
\`credentials.json\` into the credential directory.
|
|
210
|
+
|
|
211
|
+
## auth-service /api/v1/session returned 401
|
|
212
|
+
|
|
213
|
+
Invalid \`apiKey\` or wrong \`ATOMIC_MAIL_SCRYPT_SALT\` for this deployment.
|
|
214
|
+
|
|
215
|
+
## Capability JWT missing inboxId
|
|
216
|
+
|
|
217
|
+
Server/version mismatch — verify \`ATOMIC_MAIL_AUTH_URL\`.
|
|
218
|
+
|
|
219
|
+
## Could not read ops file
|
|
220
|
+
|
|
221
|
+
Check the path; use an absolute path if unsure.
|
|
222
|
+
|
|
223
|
+
## Missing values for variables (\`$TO\`, etc.)
|
|
224
|
+
|
|
225
|
+
Pass every custom placeholder in MCP \`vars\` or \`--vars\` as a JSON object of
|
|
226
|
+
strings. Ensure \`register\` completed so \`$ACCOUNT_ID\` / \`$INBOX\` can resolve.`,
|
|
227
|
+
};
|
|
228
|
+
export const HELP_TOPIC_LIST = Object.keys(HELP_TOPICS);
|
|
229
|
+
export function getHelp(topic) {
|
|
230
|
+
if (!topic) {
|
|
231
|
+
return HELP_TOPICS["overview"];
|
|
232
|
+
}
|
|
233
|
+
const key = topic.toLowerCase().replace(/[\s-]/g, "_");
|
|
234
|
+
return (HELP_TOPICS[key] ??
|
|
235
|
+
`Unknown topic "${topic}". Available topics: ${HELP_TOPIC_LIST.join(", ")}`);
|
|
236
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare const DEFAULT_JMAP_USING: readonly ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
|
|
2
|
+
export declare const JMAP_MAIL_URN: "urn:ietf:params:jmap:mail";
|
|
3
|
+
export interface JmapEnvelope {
|
|
4
|
+
using: string[];
|
|
5
|
+
methodCalls: unknown[];
|
|
6
|
+
}
|
|
7
|
+
export declare function parseJmapEnvelope(raw: string, defaultUsing: string[], source: string): JmapEnvelope;
|
|
8
|
+
export declare function resolveOpsFilePath(credentialDir: string, opsFile: string): string;
|
|
9
|
+
export declare function readOpsFile(credentialDir: string, opsFile: string): Promise<string>;
|
|
10
|
+
export declare function extractPrimaryMailAccountId(session: Record<string, unknown>): string;
|
|
11
|
+
export declare function fetchJmapWellKnown(apiUrl: string, capabilityJwt: string): Promise<Record<string, unknown>>;
|
|
12
|
+
/** Minimal surface for JMAP execution (implemented by AgentSession). */
|
|
13
|
+
export interface JmapSessionPort {
|
|
14
|
+
readonly apiUrl: string;
|
|
15
|
+
readonly files: {
|
|
16
|
+
credentialsFile: string;
|
|
17
|
+
};
|
|
18
|
+
getPrimaryMailAccountId(): Promise<string>;
|
|
19
|
+
getCapabilityToken(): Promise<string>;
|
|
20
|
+
readonly currentInboxId?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface RunJmapRequestInput {
|
|
23
|
+
session: JmapSessionPort;
|
|
24
|
+
/** Raw JSON: methodCalls array or full envelope */
|
|
25
|
+
opsJson: string;
|
|
26
|
+
/** Default `using` when the envelope omits it */
|
|
27
|
+
defaultUsing: string[];
|
|
28
|
+
/** Label for parse errors */
|
|
29
|
+
sourceLabel: string;
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
/** Values for `$VAR` tokens (keys without `$`). Overrides session vars when present. */
|
|
32
|
+
vars?: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse ops JSON, substitute `$VAR_NAME` tokens (session + caller vars), POST to JMAP.
|
|
36
|
+
*/
|
|
37
|
+
export declare function runJmapRequest(input: RunJmapRequestInput): Promise<{
|
|
38
|
+
ok: boolean;
|
|
39
|
+
status: number;
|
|
40
|
+
bodyText: string;
|
|
41
|
+
}>;
|
|
42
|
+
export declare function postJmap(apiUrl: string, capabilityJwt: string, envelope: JmapEnvelope): Promise<{
|
|
43
|
+
ok: boolean;
|
|
44
|
+
status: number;
|
|
45
|
+
bodyText: string;
|
|
46
|
+
}>;
|
|
47
|
+
/** Attach _next hints to a successful JMAP JSON object when parseable. */
|
|
48
|
+
export declare function attachJmapNextHints(bodyText: string): string;
|
|
49
|
+
//# sourceMappingURL=agent-jmap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-jmap.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-jmap.ts"],"names":[],"mappings":"AAQA,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,CAGjB;AAED,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;CAClC;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,CA4C5D;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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// JMAP envelope parsing, preset paths, $VAR substitution, and HTTP helpers.
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
4
|
+
import { readCredentials } from "./agent-credentials-store.js";
|
|
5
|
+
import { substituteVars } from "./agent-vars.js";
|
|
6
|
+
export const DEFAULT_JMAP_USING = [
|
|
7
|
+
"urn:ietf:params:jmap:core",
|
|
8
|
+
"urn:ietf:params:jmap:mail",
|
|
9
|
+
];
|
|
10
|
+
export const JMAP_MAIL_URN = "urn:ietf:params:jmap:mail";
|
|
11
|
+
export function parseJmapEnvelope(raw, defaultUsing, source) {
|
|
12
|
+
let value;
|
|
13
|
+
try {
|
|
14
|
+
value = JSON.parse(raw);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
throw new Error(`${source} is not valid JSON: ${err.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return { using: [...defaultUsing], methodCalls: value };
|
|
21
|
+
}
|
|
22
|
+
if (value !== null &&
|
|
23
|
+
typeof value === "object" &&
|
|
24
|
+
Array.isArray(value.methodCalls)) {
|
|
25
|
+
const obj = value;
|
|
26
|
+
const using = Array.isArray(obj.using)
|
|
27
|
+
? obj.using.filter((u) => typeof u === "string")
|
|
28
|
+
: [...defaultUsing];
|
|
29
|
+
return { using, methodCalls: obj.methodCalls };
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`${source} must be a methodCalls array, e.g. ` +
|
|
32
|
+
'[["Mailbox/get",{...},"m0"]], or an object with a methodCalls array.');
|
|
33
|
+
}
|
|
34
|
+
export function resolveOpsFilePath(credentialDir, opsFile) {
|
|
35
|
+
return isAbsolute(opsFile) ? opsFile : resolvePath(credentialDir, opsFile);
|
|
36
|
+
}
|
|
37
|
+
export async function readOpsFile(credentialDir, opsFile) {
|
|
38
|
+
const filePath = resolveOpsFilePath(credentialDir, opsFile);
|
|
39
|
+
return await readFile(filePath, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
export function extractPrimaryMailAccountId(session) {
|
|
42
|
+
const primary = session["primaryAccounts"];
|
|
43
|
+
if (!primary || typeof primary !== "object") {
|
|
44
|
+
throw new Error("JMAP session missing primaryAccounts.");
|
|
45
|
+
}
|
|
46
|
+
const id = primary[JMAP_MAIL_URN];
|
|
47
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
48
|
+
throw new Error(`JMAP session missing primaryAccounts['${JMAP_MAIL_URN}'].`);
|
|
49
|
+
}
|
|
50
|
+
return id;
|
|
51
|
+
}
|
|
52
|
+
export async function fetchJmapWellKnown(apiUrl, capabilityJwt) {
|
|
53
|
+
const base = apiUrl.replace(/\/+$/, "");
|
|
54
|
+
const res = await fetch(`${base}/.well-known/jmap`, {
|
|
55
|
+
headers: { Authorization: `Bearer ${capabilityJwt}` },
|
|
56
|
+
});
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
throw new Error(`JMAP session fetch failed (HTTP ${res.status}): ${text}`);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(text);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new Error("JMAP session response is not valid JSON.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Parse ops JSON, substitute `$VAR_NAME` tokens (session + caller vars), POST to JMAP.
|
|
70
|
+
*/
|
|
71
|
+
export async function runJmapRequest(input) {
|
|
72
|
+
const { text: raw } = await substituteVars({
|
|
73
|
+
raw: input.opsJson,
|
|
74
|
+
vars: input.vars,
|
|
75
|
+
autoResolvers: {
|
|
76
|
+
ACCOUNT_ID: () => input.session.getPrimaryMailAccountId(),
|
|
77
|
+
INBOX: async () => input.session.currentInboxId ??
|
|
78
|
+
(await readCredentials(input.session.files.credentialsFile)).inboxId,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const envelope = parseJmapEnvelope(raw, input.defaultUsing, input.sourceLabel);
|
|
82
|
+
if (input.dryRun) {
|
|
83
|
+
return {
|
|
84
|
+
ok: true,
|
|
85
|
+
status: 200,
|
|
86
|
+
bodyText: JSON.stringify({
|
|
87
|
+
dryRun: true,
|
|
88
|
+
url: `${input.session.apiUrl.replace(/\/+$/, "")}/jmap/`,
|
|
89
|
+
envelope,
|
|
90
|
+
}, null, 2),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const capabilityJwt = await input.session.getCapabilityToken();
|
|
94
|
+
const { ok, status, bodyText } = await postJmap(input.session.apiUrl, capabilityJwt, envelope);
|
|
95
|
+
if (!ok) {
|
|
96
|
+
return { ok, status, bodyText };
|
|
97
|
+
}
|
|
98
|
+
return { ok, status, bodyText: attachJmapNextHints(bodyText) };
|
|
99
|
+
}
|
|
100
|
+
export async function postJmap(apiUrl, capabilityJwt, envelope) {
|
|
101
|
+
const base = apiUrl.replace(/\/+$/, "");
|
|
102
|
+
const res = await fetch(`${base}/jmap/`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
Authorization: `Bearer ${capabilityJwt}`,
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(envelope),
|
|
109
|
+
});
|
|
110
|
+
const bodyText = await res.text();
|
|
111
|
+
return { ok: res.ok, status: res.status, bodyText };
|
|
112
|
+
}
|
|
113
|
+
const JMAP_NEXT_HINTS = [
|
|
114
|
+
"Use jmap_request with Mailbox/get or Email/query to work with mail data.",
|
|
115
|
+
"Use presets with $VAR placeholders — $ACCOUNT_ID and $INBOX come from the session; pass others via vars / --vars.",
|
|
116
|
+
"Call help for the JMAP cheatsheet and troubleshooting.",
|
|
117
|
+
];
|
|
118
|
+
/** Attach _next hints to a successful JMAP JSON object when parseable. */
|
|
119
|
+
export function attachJmapNextHints(bodyText) {
|
|
120
|
+
try {
|
|
121
|
+
const obj = JSON.parse(bodyText);
|
|
122
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
123
|
+
return JSON.stringify({ ...obj, _next: [...JMAP_NEXT_HINTS] }, null, 2);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// not JSON — return raw
|
|
128
|
+
}
|
|
129
|
+
return bodyText;
|
|
130
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const SESSION_TTL_MS: number;
|
|
2
|
+
export declare const CAPABILITY_TTL_MS: number;
|
|
3
|
+
export declare const SESSION_SAFETY_MARGIN_MS = 60000;
|
|
4
|
+
export declare const CAPABILITY_SAFETY_MARGIN_MS = 20000;
|
|
5
|
+
export interface JwtPayload {
|
|
6
|
+
exp?: number;
|
|
7
|
+
iat?: number;
|
|
8
|
+
jti?: string;
|
|
9
|
+
inboxId?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
export declare function decodeJwtPayload<T = JwtPayload>(jwt: string): T;
|
|
13
|
+
export declare function isJwtExpired(jwt: string, marginMs: number): boolean;
|
|
14
|
+
//# sourceMappingURL=agent-jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-jwt.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-jwt.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,cAAc,QAAqB,CAAC;AACjD,eAAO,MAAM,iBAAiB,QAAgB,CAAC;AAE/C,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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
export const SESSION_SAFETY_MARGIN_MS = 60_000;
|
|
5
|
+
export const CAPABILITY_SAFETY_MARGIN_MS = 20_000;
|
|
6
|
+
export function decodeJwtPayload(jwt) {
|
|
7
|
+
const parts = jwt.split(".");
|
|
8
|
+
if (parts.length < 2) {
|
|
9
|
+
throw new Error("Malformed JWT: expected at least 2 dot-separated segments.");
|
|
10
|
+
}
|
|
11
|
+
const payloadB64Url = parts[1];
|
|
12
|
+
const padLen = (4 - (payloadB64Url.length % 4)) % 4;
|
|
13
|
+
const base64 = payloadB64Url
|
|
14
|
+
.replace(/-/g, "+")
|
|
15
|
+
.replace(/_/g, "/")
|
|
16
|
+
.padEnd(payloadB64Url.length + padLen, "=");
|
|
17
|
+
return JSON.parse(atob(base64));
|
|
18
|
+
}
|
|
19
|
+
export function isJwtExpired(jwt, marginMs) {
|
|
20
|
+
try {
|
|
21
|
+
const { exp } = decodeJwtPayload(jwt);
|
|
22
|
+
if (typeof exp !== "number")
|
|
23
|
+
return true;
|
|
24
|
+
return Date.now() >= exp * 1000 - marginMs;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-pow.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-pow.ts"],"names":[],"mappings":"AAuCA,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACnC,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAU5C"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// PoW scrypt — mirrors services/auth-service/src/crypto.ts.
|
|
2
|
+
import { scrypt } from "node:crypto";
|
|
3
|
+
const SCRYPT_PARAMS = { N: 16384, r: 8, p: 1 };
|
|
4
|
+
const POW_HASH_BYTES = 64;
|
|
5
|
+
function bytesToHex(bytes) {
|
|
6
|
+
let hex = "";
|
|
7
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
8
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
9
|
+
}
|
|
10
|
+
return hex;
|
|
11
|
+
}
|
|
12
|
+
function hasLeadingZeroBits(hash, bits) {
|
|
13
|
+
if (bits > hash.length * 8)
|
|
14
|
+
return false;
|
|
15
|
+
const fullBytes = Math.floor(bits / 8);
|
|
16
|
+
const remainingBits = bits % 8;
|
|
17
|
+
for (let i = 0; i < fullBytes; i++) {
|
|
18
|
+
if (hash[i] !== 0)
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
if (remainingBits > 0) {
|
|
22
|
+
const mask = (0xff << (8 - remainingBits)) & 0xff;
|
|
23
|
+
if ((hash[fullBytes] & mask) !== 0)
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
function scryptHash(data, salt) {
|
|
29
|
+
const bytes = new TextEncoder().encode(data);
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
scrypt(bytes, salt, POW_HASH_BYTES, SCRYPT_PARAMS, (err, derived) => {
|
|
32
|
+
if (err)
|
|
33
|
+
return reject(err);
|
|
34
|
+
resolve(new Uint8Array(derived));
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export async function solvePow(challenge, difficulty, salt, onProgress) {
|
|
39
|
+
let nonce = 0n;
|
|
40
|
+
while (true) {
|
|
41
|
+
const digest = await scryptHash(`${challenge}:${nonce}`, salt);
|
|
42
|
+
if (hasLeadingZeroBits(digest, difficulty)) {
|
|
43
|
+
return { powHex: bytesToHex(digest), nonce: nonce.toString() };
|
|
44
|
+
}
|
|
45
|
+
nonce++;
|
|
46
|
+
if (onProgress && nonce % 64n === 0n)
|
|
47
|
+
onProgress(nonce);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SkillFiles } from "./agent-credentials-store.js";
|
|
2
|
+
export interface AgentSessionConfig {
|
|
3
|
+
authUrl: string;
|
|
4
|
+
apiUrl: string;
|
|
5
|
+
scryptSalt: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
inboxId?: string;
|
|
8
|
+
credentialDir: string;
|
|
9
|
+
files: SkillFiles;
|
|
10
|
+
}
|
|
11
|
+
export interface RegisterResult {
|
|
12
|
+
inbox: string;
|
|
13
|
+
accountId: string;
|
|
14
|
+
/** Present only on first-time signup (not idempotent replay). */
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
idempotent?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Local-part of an inbox email, or the whole string if no @. */
|
|
19
|
+
export declare function inboxLocalPart(inboxId: string): string;
|
|
20
|
+
export declare class AgentSession {
|
|
21
|
+
private readonly authUrl;
|
|
22
|
+
readonly apiUrl: string;
|
|
23
|
+
private readonly scryptSalt;
|
|
24
|
+
private apiKey;
|
|
25
|
+
private inboxId;
|
|
26
|
+
readonly credentialDir: string;
|
|
27
|
+
readonly files: SkillFiles;
|
|
28
|
+
private sessionJWT;
|
|
29
|
+
private capabilityJWT;
|
|
30
|
+
private cachedMailAccountId;
|
|
31
|
+
constructor(cfg: AgentSessionConfig);
|
|
32
|
+
static create(cfg: AgentSessionConfig): Promise<AgentSession>;
|
|
33
|
+
get hasApiKey(): boolean;
|
|
34
|
+
get currentInboxId(): string | undefined;
|
|
35
|
+
private loadFromDisk;
|
|
36
|
+
/**
|
|
37
|
+
* Primary JMAP mail accountId from GET /.well-known/jmap (cached).
|
|
38
|
+
*/
|
|
39
|
+
getPrimaryMailAccountId(): Promise<string>;
|
|
40
|
+
invalidateJmapSessionCache(): void;
|
|
41
|
+
/**
|
|
42
|
+
* Register or return existing inbox when username matches (idempotent).
|
|
43
|
+
* Different username replaces on-disk credentials and creates a new inbox.
|
|
44
|
+
*/
|
|
45
|
+
register(username: string): Promise<RegisterResult>;
|
|
46
|
+
getCapabilityToken(): Promise<string>;
|
|
47
|
+
private ensureSession;
|
|
48
|
+
destroy(): void;
|
|
49
|
+
}
|
|
50
|
+
export interface PersistLoginWithApiKeyInput {
|
|
51
|
+
authUrl: string;
|
|
52
|
+
apiUrl: string;
|
|
53
|
+
scryptSalt: string;
|
|
54
|
+
apiKey: string;
|
|
55
|
+
files: SkillFiles;
|
|
56
|
+
onPowProgress?: (nonce: bigint) => void;
|
|
57
|
+
}
|
|
58
|
+
/** PoW login with an existing API key; writes credentials + JWT files. */
|
|
59
|
+
export declare function persistLoginWithApiKey(input: PersistLoginWithApiKeyInput): Promise<{
|
|
60
|
+
inboxId: string;
|
|
61
|
+
}>;
|
|
62
|
+
//# sourceMappingURL=agent-session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-session.d.ts","sourceRoot":"","sources":["../../../src/lib/src/agent-session.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,UAAU,EAMhB,MAAM,8BAA8B,CAAC;AAatC,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;gBAEpC,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;YAEa,YAAY;IAU1B;;OAEG;IACG,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC;IAShD,0BAA0B,IAAI,IAAI;IAIlC;;;OAGG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAuEnD,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC;YA4B7B,aAAa;IAyB3B,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,CAyB9B"}
|