@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.
Files changed (39) hide show
  1. package/README.md +56 -0
  2. package/SKILL.md +67 -187
  3. package/esm/{skill/scripts/lib/auth.d.ts → lib/src/agent-auth-http.d.ts} +1 -17
  4. package/esm/lib/src/agent-auth-http.d.ts.map +1 -0
  5. package/esm/lib/src/agent-auth-http.js +76 -0
  6. package/esm/{skill/scripts/lib/credentials.d.ts → lib/src/agent-credentials-store.d.ts} +4 -1
  7. package/esm/lib/src/agent-credentials-store.d.ts.map +1 -0
  8. package/esm/{skill/scripts/lib/credentials.js → lib/src/agent-credentials-store.js} +28 -8
  9. package/esm/lib/src/agent-help-content.d.ts +4 -0
  10. package/esm/lib/src/agent-help-content.d.ts.map +1 -0
  11. package/esm/lib/src/agent-help-content.js +236 -0
  12. package/esm/lib/src/agent-jmap.d.ts +49 -0
  13. package/esm/lib/src/agent-jmap.d.ts.map +1 -0
  14. package/esm/lib/src/agent-jmap.js +130 -0
  15. package/esm/lib/src/agent-jwt.d.ts +14 -0
  16. package/esm/lib/src/agent-jwt.d.ts.map +1 -0
  17. package/esm/lib/src/agent-jwt.js +29 -0
  18. package/esm/lib/src/agent-pow.d.ts +5 -0
  19. package/esm/lib/src/agent-pow.d.ts.map +1 -0
  20. package/esm/lib/src/agent-pow.js +49 -0
  21. package/esm/lib/src/agent-session.d.ts +62 -0
  22. package/esm/lib/src/agent-session.d.ts.map +1 -0
  23. package/esm/lib/src/agent-session.js +206 -0
  24. package/esm/lib/src/agent-vars.d.ts +23 -0
  25. package/esm/lib/src/agent-vars.d.ts.map +1 -0
  26. package/esm/lib/src/agent-vars.js +65 -0
  27. package/esm/skill/scripts/cli.d.ts +3 -0
  28. package/esm/skill/scripts/cli.d.ts.map +1 -0
  29. package/esm/skill/scripts/cli.js +309 -0
  30. package/package.json +3 -4
  31. package/esm/skill/scripts/jmap_request.d.ts +0 -3
  32. package/esm/skill/scripts/jmap_request.d.ts.map +0 -1
  33. package/esm/skill/scripts/jmap_request.js +0 -265
  34. package/esm/skill/scripts/lib/auth.d.ts.map +0 -1
  35. package/esm/skill/scripts/lib/auth.js +0 -163
  36. package/esm/skill/scripts/lib/credentials.d.ts.map +0 -1
  37. package/esm/skill/scripts/signup.d.ts +0 -3
  38. package/esm/skill/scripts/signup.d.ts.map +0 -1
  39. 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,5 @@
1
+ export declare function solvePow(challenge: string, difficulty: number, salt: string, onProgress?: (nonce: bigint) => void): Promise<{
2
+ powHex: string;
3
+ nonce: string;
4
+ }>;
5
+ //# sourceMappingURL=agent-pow.d.ts.map
@@ -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"}