@atomicmail/langchain 0.3.14

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/esm/_dnt.polyfills.d.ts +101 -0
  4. package/esm/_dnt.polyfills.d.ts.map +1 -0
  5. package/esm/_dnt.polyfills.js +127 -0
  6. package/esm/langchain/mod.d.ts +23 -0
  7. package/esm/langchain/mod.d.ts.map +1 -0
  8. package/esm/langchain/mod.js +157 -0
  9. package/esm/lib/agent/auth/agent-auth-http.d.ts +26 -0
  10. package/esm/lib/agent/auth/agent-auth-http.d.ts.map +1 -0
  11. package/esm/lib/agent/auth/agent-auth-http.js +85 -0
  12. package/esm/lib/agent/auth/agent-jwt.d.ts +12 -0
  13. package/esm/lib/agent/auth/agent-jwt.d.ts.map +1 -0
  14. package/esm/lib/agent/auth/agent-jwt.js +27 -0
  15. package/esm/lib/agent/auth/agent-pow.d.ts +5 -0
  16. package/esm/lib/agent/auth/agent-pow.d.ts.map +1 -0
  17. package/esm/lib/agent/auth/agent-pow.js +49 -0
  18. package/esm/lib/agent/jmap/agent-help-content.d.ts +2 -0
  19. package/esm/lib/agent/jmap/agent-help-content.d.ts.map +1 -0
  20. package/esm/lib/agent/jmap/agent-help-content.js +2 -0
  21. package/esm/lib/agent/jmap/agent-jmap-blob-limits.d.ts +27 -0
  22. package/esm/lib/agent/jmap/agent-jmap-blob-limits.d.ts.map +1 -0
  23. package/esm/lib/agent/jmap/agent-jmap-blob-limits.js +166 -0
  24. package/esm/lib/agent/jmap/agent-jmap-blob-upload.d.ts +24 -0
  25. package/esm/lib/agent/jmap/agent-jmap-blob-upload.d.ts.map +1 -0
  26. package/esm/lib/agent/jmap/agent-jmap-blob-upload.js +104 -0
  27. package/esm/lib/agent/jmap/agent-jmap-email-charset.d.ts +8 -0
  28. package/esm/lib/agent/jmap/agent-jmap-email-charset.d.ts.map +1 -0
  29. package/esm/lib/agent/jmap/agent-jmap-email-charset.js +61 -0
  30. package/esm/lib/agent/jmap/agent-jmap-verify.d.ts +9 -0
  31. package/esm/lib/agent/jmap/agent-jmap-verify.d.ts.map +1 -0
  32. package/esm/lib/agent/jmap/agent-jmap-verify.js +50 -0
  33. package/esm/lib/agent/jmap/agent-jmap.d.ts +89 -0
  34. package/esm/lib/agent/jmap/agent-jmap.d.ts.map +1 -0
  35. package/esm/lib/agent/jmap/agent-jmap.js +373 -0
  36. package/esm/lib/agent/jmap/agent-vars.d.ts +30 -0
  37. package/esm/lib/agent/jmap/agent-vars.d.ts.map +1 -0
  38. package/esm/lib/agent/jmap/agent-vars.js +96 -0
  39. package/esm/lib/agent/jmap/help-content/auth.d.ts +2 -0
  40. package/esm/lib/agent/jmap/help-content/auth.d.ts.map +1 -0
  41. package/esm/lib/agent/jmap/help-content/auth.js +33 -0
  42. package/esm/lib/agent/jmap/help-content/cron.d.ts +6 -0
  43. package/esm/lib/agent/jmap/help-content/cron.d.ts.map +1 -0
  44. package/esm/lib/agent/jmap/help-content/cron.js +159 -0
  45. package/esm/lib/agent/jmap/help-content/index.d.ts +6 -0
  46. package/esm/lib/agent/jmap/help-content/index.d.ts.map +1 -0
  47. package/esm/lib/agent/jmap/help-content/index.js +61 -0
  48. package/esm/lib/agent/jmap/help-content/installation.d.ts +2 -0
  49. package/esm/lib/agent/jmap/help-content/installation.d.ts.map +1 -0
  50. package/esm/lib/agent/jmap/help-content/installation.js +47 -0
  51. package/esm/lib/agent/jmap/help-content/jmap-cheatsheet.d.ts +2 -0
  52. package/esm/lib/agent/jmap/help-content/jmap-cheatsheet.d.ts.map +1 -0
  53. package/esm/lib/agent/jmap/help-content/jmap-cheatsheet.js +230 -0
  54. package/esm/lib/agent/jmap/help-content/multi-account.d.ts +2 -0
  55. package/esm/lib/agent/jmap/help-content/multi-account.d.ts.map +1 -0
  56. package/esm/lib/agent/jmap/help-content/multi-account.js +49 -0
  57. package/esm/lib/agent/jmap/help-content/overview.d.ts +2 -0
  58. package/esm/lib/agent/jmap/help-content/overview.d.ts.map +1 -0
  59. package/esm/lib/agent/jmap/help-content/overview.js +49 -0
  60. package/esm/lib/agent/jmap/help-content/presets.d.ts +2 -0
  61. package/esm/lib/agent/jmap/help-content/presets.d.ts.map +1 -0
  62. package/esm/lib/agent/jmap/help-content/presets.js +51 -0
  63. package/esm/lib/agent/jmap/help-content/tools.d.ts +2 -0
  64. package/esm/lib/agent/jmap/help-content/tools.d.ts.map +1 -0
  65. package/esm/lib/agent/jmap/help-content/tools.js +49 -0
  66. package/esm/lib/agent/jmap/help-content/troubleshooting.d.ts +2 -0
  67. package/esm/lib/agent/jmap/help-content/troubleshooting.d.ts.map +1 -0
  68. package/esm/lib/agent/jmap/help-content/troubleshooting.js +65 -0
  69. package/esm/lib/agent/session/agent-credentials-store.d.ts +45 -0
  70. package/esm/lib/agent/session/agent-credentials-store.d.ts.map +1 -0
  71. package/esm/lib/agent/session/agent-credentials-store.js +121 -0
  72. package/esm/lib/agent/session/agent-resolve-config.d.ts +29 -0
  73. package/esm/lib/agent/session/agent-resolve-config.d.ts.map +1 -0
  74. package/esm/lib/agent/session/agent-resolve-config.js +71 -0
  75. package/esm/lib/agent/session/agent-session-for-dir.d.ts +8 -0
  76. package/esm/lib/agent/session/agent-session-for-dir.d.ts.map +1 -0
  77. package/esm/lib/agent/session/agent-session-for-dir.js +33 -0
  78. package/esm/lib/agent/session/agent-session.d.ts +89 -0
  79. package/esm/lib/agent/session/agent-session.d.ts.map +1 -0
  80. package/esm/lib/agent/session/agent-session.js +320 -0
  81. package/esm/lib/agent/session/inbox-id-to-mailbox-email.d.ts +6 -0
  82. package/esm/lib/agent/session/inbox-id-to-mailbox-email.d.ts.map +1 -0
  83. package/esm/lib/agent/session/inbox-id-to-mailbox-email.js +24 -0
  84. package/esm/lib/core/consts.d.ts +17 -0
  85. package/esm/lib/core/consts.d.ts.map +1 -0
  86. package/esm/lib/core/consts.js +28 -0
  87. package/esm/lib/core/messages.d.ts +6 -0
  88. package/esm/lib/core/messages.d.ts.map +1 -0
  89. package/esm/lib/core/messages.js +19 -0
  90. package/esm/lib/core/read-npm-package-readme.d.ts +6 -0
  91. package/esm/lib/core/read-npm-package-readme.d.ts.map +1 -0
  92. package/esm/lib/core/read-npm-package-readme.js +81 -0
  93. package/esm/lib/core/shared-assets.d.ts +6 -0
  94. package/esm/lib/core/shared-assets.d.ts.map +1 -0
  95. package/esm/lib/core/shared-assets.js +46 -0
  96. package/esm/lib/core/types.d.ts +2 -0
  97. package/esm/lib/core/types.d.ts.map +1 -0
  98. package/esm/lib/core/types.js +2 -0
  99. package/esm/lib/core/utils.d.ts +12 -0
  100. package/esm/lib/core/utils.d.ts.map +1 -0
  101. package/esm/lib/core/utils.js +29 -0
  102. package/esm/lib/mod.d.ts +20 -0
  103. package/esm/lib/mod.d.ts.map +1 -0
  104. package/esm/lib/mod.js +19 -0
  105. package/esm/lib/network/auth-client.d.ts +57 -0
  106. package/esm/lib/network/auth-client.d.ts.map +1 -0
  107. package/esm/lib/network/auth-client.js +210 -0
  108. package/esm/package.json +3 -0
  109. package/package.json +51 -0
  110. package/presets/list_inbox.json +46 -0
  111. package/presets/reply.json +97 -0
  112. package/presets/send_mail.json +70 -0
  113. package/presets/send_mail_attachment.json +92 -0
  114. package/presets/send_mail_blob_attachment.json +74 -0
  115. package/shared/consts.json +11 -0
  116. package/shared/fixtures/pow_vectors.json +32 -0
  117. package/shared/help/fragments/inbox_cron_agent_prompt.md +1 -0
  118. package/shared/help/fragments/post_register_cron_reminder.md +5 -0
  119. package/shared/help/readme_stub.md +3 -0
  120. package/shared/help/topics/auth.md +8 -0
  121. package/shared/help/topics/cron.md +217 -0
  122. package/shared/help/topics/installation.md +35 -0
  123. package/shared/help/topics/jmap_cheatsheet.md +19 -0
  124. package/shared/help/topics/multi_account.md +9 -0
  125. package/shared/help/topics/overview.md +27 -0
  126. package/shared/help/topics/presets.md +12 -0
  127. package/shared/help/topics/tools.md +16 -0
  128. package/shared/help/topics/troubleshooting.md +6 -0
  129. package/shared/manifest.json +31 -0
  130. package/shared/messages/errors.json +68 -0
  131. package/shared/messages/hints.json +8 -0
  132. package/shared/presets/list_inbox.json +46 -0
  133. package/shared/presets/reply.json +97 -0
  134. package/shared/presets/send_mail.json +70 -0
  135. package/shared/presets/send_mail_attachment.json +92 -0
  136. package/shared/presets/send_mail_blob_attachment.json +74 -0
  137. package/shared/skill/SKILL.template.md +202 -0
  138. package/shared/skill/manifest.json +89 -0
@@ -0,0 +1,27 @@
1
+ // JWT helpers for capability/session expiry checks.
2
+ export const SESSION_SAFETY_MARGIN_MS = 60_000;
3
+ export const CAPABILITY_SAFETY_MARGIN_MS = 20_000;
4
+ export function decodeJwtPayload(jwt) {
5
+ const parts = jwt.split(".");
6
+ if (parts.length < 2) {
7
+ throw new Error("Malformed JWT: expected at least 2 dot-separated segments.");
8
+ }
9
+ const payloadB64Url = parts[1];
10
+ const padLen = (4 - (payloadB64Url.length % 4)) % 4;
11
+ const base64 = payloadB64Url
12
+ .replace(/-/g, "+")
13
+ .replace(/_/g, "/")
14
+ .padEnd(payloadB64Url.length + padLen, "=");
15
+ return JSON.parse(atob(base64));
16
+ }
17
+ export function isJwtExpired(jwt, marginMs) {
18
+ try {
19
+ const { exp } = decodeJwtPayload(jwt);
20
+ if (typeof exp !== "number")
21
+ return true;
22
+ return Date.now() >= exp * 1000 - marginMs;
23
+ }
24
+ catch {
25
+ return true;
26
+ }
27
+ }
@@ -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/agent/auth/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,2 @@
1
+ export * from "./help-content/index.js";
2
+ //# sourceMappingURL=agent-help-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-help-content.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-help-content.ts"],"names":[],"mappings":"AAEA,cAAc,yBAAyB,CAAC"}
@@ -0,0 +1,2 @@
1
+ // Long-form help for MCP `help` tool and AgentSkill `help` command.
2
+ export * from "./help-content/index.js";
@@ -0,0 +1,27 @@
1
+ export interface JmapBlobUploadLimits {
2
+ maxSizeBlobSet: number | null;
3
+ /** When omitted, skip maxDataSources enforcement (not advertised). */
4
+ maxDataSources?: number;
5
+ }
6
+ export interface JmapEnvelopeLike {
7
+ using: string[];
8
+ methodCalls: unknown[];
9
+ }
10
+ export declare function utf8ByteLength(s: string): number;
11
+ /** Decoded byte length; throws if the string is not valid standard base64. */
12
+ export declare function decodedBase64ByteLength(b64: string): number;
13
+ /**
14
+ * Returns total octets for one UploadObject `data` array, or `null` if a literal
15
+ * (non-`#`) blobId slice is present or a `#ref` is not yet in `knownSizes`.
16
+ */
17
+ export declare function tryComputeUploadDataOctets(dataUnknown: unknown, knownSizes: ReadonlyMap<string, number>): number | null;
18
+ /**
19
+ * Walks `methodCalls` in order and enforces limits for each `Blob/upload` whose
20
+ * `accountId` has an entry in `limitsByAccount`.
21
+ */
22
+ export declare function assertBlobUploadEnvelopeWithinLimits(envelope: JmapEnvelopeLike, limitsByAccount: ReadonlyMap<string, JmapBlobUploadLimits | null>): void;
23
+ export declare function assertAttachmentBytesWithinBlobLimit(items: readonly {
24
+ label: string;
25
+ byteLength: number;
26
+ }[], limits: JmapBlobUploadLimits | null): void;
27
+ //# sourceMappingURL=agent-jmap-blob-limits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-jmap-blob-limits.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-jmap-blob-limits.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,oBAAoB;IACnC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,OAAO,EAAE,CAAC;CACxB;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,8EAA8E;AAC9E,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAgB3D;AAqBD;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,OAAO,EACpB,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC,MAAM,GAAG,IAAI,CAiDf;AA6DD;;;GAGG;AACH,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,gBAAgB,EAC1B,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,oBAAoB,GAAG,IAAI,CAAC,GAChE,IAAI,CAuBN;AAED,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,SAAS;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,EAAE,EACvD,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAClC,IAAI,CAYN"}
@@ -0,0 +1,166 @@
1
+ // RFC 9404 §3.1 client-side checks before Blob/upload POST (maxSizeBlobSet, maxDataSources).
2
+ const utf8Encoder = new TextEncoder();
3
+ export function utf8ByteLength(s) {
4
+ return utf8Encoder.encode(s).length;
5
+ }
6
+ /** Decoded byte length; throws if the string is not valid standard base64. */
7
+ export function decodedBase64ByteLength(b64) {
8
+ const t = b64.replace(/\s+/g, "");
9
+ if (t.length === 0)
10
+ return 0;
11
+ if (t.length % 4 === 1) {
12
+ throw new Error("Invalid base64 in Blob/upload data:asBase64 (RFC 9404 §4.1): bad length.");
13
+ }
14
+ try {
15
+ const bin = atob(t.replace(/-/g, "+").replace(/_/g, "/"));
16
+ return bin.length;
17
+ }
18
+ catch {
19
+ throw new Error("Invalid base64 in Blob/upload data:asBase64 (RFC 9404 §4.1).");
20
+ }
21
+ }
22
+ function sliceOctetCount(sourceLen, offset, length) {
23
+ let off = 0;
24
+ if (typeof offset === "number" && Number.isInteger(offset) && offset >= 0) {
25
+ off = offset;
26
+ }
27
+ const rest = Math.max(0, sourceLen - off);
28
+ if (length === null || length === undefined) {
29
+ return rest;
30
+ }
31
+ if (typeof length === "number" && Number.isInteger(length) && length >= 0) {
32
+ return Math.min(rest, length);
33
+ }
34
+ return rest;
35
+ }
36
+ /**
37
+ * Returns total octets for one UploadObject `data` array, or `null` if a literal
38
+ * (non-`#`) blobId slice is present or a `#ref` is not yet in `knownSizes`.
39
+ */
40
+ export function tryComputeUploadDataOctets(dataUnknown, knownSizes) {
41
+ if (!Array.isArray(dataUnknown))
42
+ return 0;
43
+ let total = 0;
44
+ for (const part of dataUnknown) {
45
+ if (!part || typeof part !== "object")
46
+ continue;
47
+ const o = part;
48
+ const tText = o["data:asText"];
49
+ const tB64 = o["data:asBase64"];
50
+ const tBlob = o["blobId"];
51
+ const hasText = tText !== undefined && tText !== null;
52
+ const hasB64 = tB64 !== undefined && tB64 !== null;
53
+ const hasBlob = tBlob !== undefined && tBlob !== null;
54
+ const n = [hasText, hasB64, hasBlob].filter(Boolean).length;
55
+ if (n === 0)
56
+ continue;
57
+ if (n !== 1) {
58
+ throw new Error("Each Blob/upload DataSourceObject must use exactly one of " +
59
+ "data:asText, data:asBase64, or blobId (RFC 9404 §4.1).");
60
+ }
61
+ if (hasText) {
62
+ if (typeof tText !== "string") {
63
+ throw new Error("Blob/upload data:asText must be a string.");
64
+ }
65
+ total += utf8ByteLength(tText);
66
+ continue;
67
+ }
68
+ if (hasB64) {
69
+ if (typeof tB64 !== "string") {
70
+ throw new Error("Blob/upload data:asBase64 must be a string.");
71
+ }
72
+ total += decodedBase64ByteLength(tB64);
73
+ continue;
74
+ }
75
+ if (typeof tBlob !== "string" || tBlob.length === 0) {
76
+ throw new Error("Blob/upload blobId must be a non-empty string.");
77
+ }
78
+ if (!tBlob.startsWith("#")) {
79
+ return null;
80
+ }
81
+ const refId = tBlob.slice(1);
82
+ const sourceLen = knownSizes.get(refId);
83
+ if (sourceLen === undefined) {
84
+ return null;
85
+ }
86
+ total += sliceOctetCount(sourceLen, o["offset"], o["length"]);
87
+ }
88
+ return total;
89
+ }
90
+ function resolveCreateSizesForOneBlobUpload(create, limits, priorSizes) {
91
+ const merged = new Map(priorSizes);
92
+ const pending = new Set(Object.keys(create));
93
+ while (pending.size > 0) {
94
+ let progressed = false;
95
+ for (const id of [...pending]) {
96
+ const upload = create[id];
97
+ if (!upload || typeof upload !== "object") {
98
+ pending.delete(id);
99
+ progressed = true;
100
+ continue;
101
+ }
102
+ const data = upload["data"];
103
+ const dsCount = Array.isArray(data) ? data.length : 0;
104
+ if (limits.maxDataSources !== undefined &&
105
+ dsCount > limits.maxDataSources) {
106
+ throw new Error(`Blob/upload create "${id}" uses ${dsCount} DataSourceObject entries; ` +
107
+ `account maxDataSources is ${limits.maxDataSources} (RFC 9404 §3.1).`);
108
+ }
109
+ const computed = tryComputeUploadDataOctets(data, merged);
110
+ if (computed === null) {
111
+ continue;
112
+ }
113
+ if (limits.maxSizeBlobSet !== null &&
114
+ computed > limits.maxSizeBlobSet) {
115
+ throw new Error(`Blob/upload create "${id}" would be ${computed} octets; account ` +
116
+ `maxSizeBlobSet is ${limits.maxSizeBlobSet} (RFC 9404 §3.1). ` +
117
+ "Use a smaller payload, split data, or POST the file to the session " +
118
+ "uploadUrl (RFC 8620) / MCP attachments for large binaries.");
119
+ }
120
+ merged.set(id, computed);
121
+ pending.delete(id);
122
+ progressed = true;
123
+ }
124
+ if (!progressed) {
125
+ break;
126
+ }
127
+ }
128
+ return merged;
129
+ }
130
+ /**
131
+ * Walks `methodCalls` in order and enforces limits for each `Blob/upload` whose
132
+ * `accountId` has an entry in `limitsByAccount`.
133
+ */
134
+ export function assertBlobUploadEnvelopeWithinLimits(envelope, limitsByAccount) {
135
+ let globalSizes = new Map();
136
+ for (const call of envelope.methodCalls) {
137
+ if (!Array.isArray(call) || call[0] !== "Blob/upload")
138
+ continue;
139
+ const arg = call[1];
140
+ if (!arg || typeof arg !== "object")
141
+ continue;
142
+ const rec = arg;
143
+ const accountId = rec["accountId"];
144
+ if (typeof accountId !== "string" || accountId.length === 0)
145
+ continue;
146
+ const limits = limitsByAccount.get(accountId);
147
+ if (!limits)
148
+ continue;
149
+ const create = rec["create"];
150
+ if (!create || typeof create !== "object")
151
+ continue;
152
+ globalSizes = resolveCreateSizesForOneBlobUpload(create, limits, globalSizes);
153
+ }
154
+ }
155
+ export function assertAttachmentBytesWithinBlobLimit(items, limits) {
156
+ if (!limits || limits.maxSizeBlobSet === null)
157
+ return;
158
+ const max = limits.maxSizeBlobSet;
159
+ for (const it of items) {
160
+ if (it.byteLength > max) {
161
+ throw new Error(`${it.label} is ${it.byteLength} octets but account maxSizeBlobSet is ` +
162
+ `${max} (RFC 9404 §3.1). Use a smaller file or refresh the session if ` +
163
+ "limits changed.");
164
+ }
165
+ }
166
+ }
@@ -0,0 +1,24 @@
1
+ import type { JmapSessionPort } from "./agent-jmap.js";
2
+ /** One local file to upload before `$ATTACHMENT_*` substitution in ops JSON. */
3
+ export interface JmapAttachmentInput {
4
+ path: string;
5
+ filename?: string;
6
+ contentType?: string;
7
+ }
8
+ export declare function guessMimeTypeFromFilename(name: string): string;
9
+ export declare function expandUploadUrl(template: string, accountId: string): string;
10
+ export declare function postBinaryBlobUpload(uploadUrlExpanded: string, capabilityJwt: string, bytes: Uint8Array, contentType: string): Promise<{
11
+ blobId: string;
12
+ size: number;
13
+ }>;
14
+ /**
15
+ * Reads each file, POSTs bytes to the JMAP session `uploadUrl`, and returns
16
+ * placeholder values for use in standard JMAP ops (same batch as
17
+ * `Email/set` / `EmailSubmission/set`).
18
+ *
19
+ * Injected keys (strings): `ATTACHMENT_0_BLOB_ID`, `ATTACHMENT_0_NAME`,
20
+ * `ATTACHMENT_0_TYPE`, `ATTACHMENT_0_SIZE`, … zero-based index per file, plus
21
+ * `ATTACHMENT_COUNT`.
22
+ */
23
+ export declare function buildVarsFromAttachmentFiles(session: JmapSessionPort, attachments: JmapAttachmentInput[], pathBase?: string): Promise<Record<string, string>>;
24
+ //# sourceMappingURL=agent-jmap-blob-upload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-jmap-blob-upload.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-jmap-blob-upload.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAoBD,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK9D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAI3E;AAED,wBAAsB,oBAAoB,CACxC,iBAAiB,EAAE,MAAM,EACzB,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAwB3C;AAED;;;;;;;;GAQG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,eAAe,EACxB,WAAW,EAAE,mBAAmB,EAAE,EAClC,QAAQ,GAAE,MAAc,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAuDjC"}
@@ -0,0 +1,104 @@
1
+ // RFC 8620 binary blob upload (session uploadUrl) for jmap_request attachment paths.
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename, isAbsolute, resolve } from "node:path";
4
+ import { cwd } from "node:process";
5
+ import { readCredentials } from "../session/agent-credentials-store.js";
6
+ import { assertAttachmentBytesWithinBlobLimit } from "./agent-jmap-blob-limits.js";
7
+ const EXT_TO_MIME = {
8
+ ".txt": "text/plain",
9
+ ".html": "text/html",
10
+ ".htm": "text/html",
11
+ ".css": "text/css",
12
+ ".js": "text/javascript",
13
+ ".json": "application/json",
14
+ ".pdf": "application/pdf",
15
+ ".png": "image/png",
16
+ ".jpg": "image/jpeg",
17
+ ".jpeg": "image/jpeg",
18
+ ".gif": "image/gif",
19
+ ".webp": "image/webp",
20
+ ".zip": "application/zip",
21
+ ".gz": "application/gzip",
22
+ ".xml": "application/xml",
23
+ };
24
+ export function guessMimeTypeFromFilename(name) {
25
+ const lower = name.toLowerCase();
26
+ const dot = lower.lastIndexOf(".");
27
+ if (dot === -1)
28
+ return "application/octet-stream";
29
+ return EXT_TO_MIME[lower.slice(dot)] ?? "application/octet-stream";
30
+ }
31
+ export function expandUploadUrl(template, accountId) {
32
+ return template
33
+ .replaceAll("%7BaccountId%7D", accountId)
34
+ .replaceAll("{accountId}", accountId);
35
+ }
36
+ export async function postBinaryBlobUpload(uploadUrlExpanded, capabilityJwt, bytes, contentType) {
37
+ const body = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
38
+ const res = await fetch(uploadUrlExpanded, {
39
+ method: "POST",
40
+ headers: {
41
+ Authorization: `Bearer ${capabilityJwt}`,
42
+ "Content-Type": contentType,
43
+ },
44
+ body,
45
+ });
46
+ const text = await res.text();
47
+ if (!res.ok) {
48
+ throw new Error(`RFC 8620 binary upload failed (HTTP ${res.status}) for ${uploadUrlExpanded}: ${text}`);
49
+ }
50
+ const j = JSON.parse(text);
51
+ if (typeof j.blobId !== "string" || j.blobId.length === 0) {
52
+ throw new Error(`Upload response missing blobId: ${text}`);
53
+ }
54
+ return { blobId: j.blobId, size: typeof j.size === "number" ? j.size : 0 };
55
+ }
56
+ /**
57
+ * Reads each file, POSTs bytes to the JMAP session `uploadUrl`, and returns
58
+ * placeholder values for use in standard JMAP ops (same batch as
59
+ * `Email/set` / `EmailSubmission/set`).
60
+ *
61
+ * Injected keys (strings): `ATTACHMENT_0_BLOB_ID`, `ATTACHMENT_0_NAME`,
62
+ * `ATTACHMENT_0_TYPE`, `ATTACHMENT_0_SIZE`, … zero-based index per file, plus
63
+ * `ATTACHMENT_COUNT`.
64
+ */
65
+ export async function buildVarsFromAttachmentFiles(session, attachments, pathBase = cwd()) {
66
+ if (attachments.length === 0)
67
+ return {};
68
+ const accountId = await session.getPrimaryMailAccountId();
69
+ const limits = await session.getBlobUploadLimitsForAccount(accountId);
70
+ const capabilityJwt = await session.getCapabilityToken();
71
+ const uploadTemplate = session.currentUploadUrl ??
72
+ (session.files
73
+ ? (await readCredentials(session.files.credentialsFile)).uploadUrl
74
+ : undefined);
75
+ if (!uploadTemplate) {
76
+ throw new Error("JMAP session missing uploadUrl.");
77
+ }
78
+ const uploadUrlExpanded = expandUploadUrl(uploadTemplate, accountId);
79
+ const prepared = [];
80
+ for (let i = 0; i < attachments.length; i++) {
81
+ const a = attachments[i];
82
+ const abs = isAbsolute(a.path) ? a.path : resolve(pathBase, a.path);
83
+ const buf = await readFile(abs);
84
+ const bytes = buf;
85
+ const filename = a.filename ?? basename(abs);
86
+ const contentType = a.contentType ?? guessMimeTypeFromFilename(filename);
87
+ prepared.push({ bytes, filename, contentType });
88
+ }
89
+ assertAttachmentBytesWithinBlobLimit(prepared.map((p) => ({
90
+ label: p.filename,
91
+ byteLength: p.bytes.byteLength,
92
+ })), limits);
93
+ const vars = {};
94
+ for (let i = 0; i < prepared.length; i++) {
95
+ const { bytes, filename, contentType } = prepared[i];
96
+ const { blobId, size } = await postBinaryBlobUpload(uploadUrlExpanded, capabilityJwt, bytes, contentType);
97
+ vars[`ATTACHMENT_${i}_BLOB_ID`] = blobId;
98
+ vars[`ATTACHMENT_${i}_NAME`] = filename;
99
+ vars[`ATTACHMENT_${i}_TYPE`] = contentType;
100
+ vars[`ATTACHMENT_${i}_SIZE`] = String(size);
101
+ }
102
+ vars["ATTACHMENT_COUNT"] = String(attachments.length);
103
+ return vars;
104
+ }
@@ -0,0 +1,8 @@
1
+ import type { JmapEnvelope } from "./agent-jmap.js";
2
+ /**
3
+ * Ensures blob-backed `text/*` body parts in `Email/set` `create` include
4
+ * `charset` when omitted, for strict JMAP servers. Skips parts with `partId`
5
+ * (RFC 8621 forbids charset there).
6
+ */
7
+ export declare function ensureTextCharsetOnEmailSetBlobParts(envelope: JmapEnvelope): void;
8
+ //# sourceMappingURL=agent-jmap-email-charset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-jmap-email-charset.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-jmap-email-charset.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA2CpD;;;;GAIG;AACH,wBAAgB,oCAAoC,CAClD,QAAQ,EAAE,YAAY,GACrB,IAAI,CAMN"}
@@ -0,0 +1,61 @@
1
+ // RFC 8621 §4.4: charset MUST be omitted when partId is given; blob-backed text/*
2
+ // parts should include charset (type strips Content-Type parameters).
3
+ const DEFAULT_TEXT_CHARSET = "utf-8";
4
+ function baseMediaType(type) {
5
+ const semi = type.indexOf(";");
6
+ return (semi === -1 ? type : type.slice(0, semi)).trim().toLowerCase();
7
+ }
8
+ function isTextStarType(type) {
9
+ return baseMediaType(type).startsWith("text/");
10
+ }
11
+ /** RFC 8621 EmailBodyPart: never add charset when partId is set. */
12
+ function ensureCharsetOnBodyPart(part) {
13
+ if (!part || typeof part !== "object" || Array.isArray(part))
14
+ return;
15
+ const o = part;
16
+ if (typeof o.partId === "string" && o.partId.length > 0)
17
+ return;
18
+ if (typeof o.blobId !== "string" || o.blobId.length === 0)
19
+ return;
20
+ const t = o.type;
21
+ if (typeof t !== "string" || !isTextStarType(t))
22
+ return;
23
+ if (o.charset != null && o.charset !== "")
24
+ return;
25
+ o.charset = DEFAULT_TEXT_CHARSET;
26
+ }
27
+ function normalizeBodyPartArray(arr) {
28
+ if (!Array.isArray(arr))
29
+ return;
30
+ for (const item of arr)
31
+ ensureCharsetOnBodyPart(item);
32
+ }
33
+ function normalizeEmailSetArg(arg) {
34
+ if (!arg || typeof arg !== "object" || Array.isArray(arg))
35
+ return;
36
+ const create = arg["create"];
37
+ if (!create || typeof create !== "object" || Array.isArray(create))
38
+ return;
39
+ for (const email of Object.values(create)) {
40
+ if (!email || typeof email !== "object" || Array.isArray(email))
41
+ continue;
42
+ const e = email;
43
+ normalizeBodyPartArray(e.attachments);
44
+ normalizeBodyPartArray(e.textBody);
45
+ normalizeBodyPartArray(e.htmlBody);
46
+ }
47
+ }
48
+ /**
49
+ * Ensures blob-backed `text/*` body parts in `Email/set` `create` include
50
+ * `charset` when omitted, for strict JMAP servers. Skips parts with `partId`
51
+ * (RFC 8621 forbids charset there).
52
+ */
53
+ export function ensureTextCharsetOnEmailSetBlobParts(envelope) {
54
+ for (const call of envelope.methodCalls) {
55
+ if (!Array.isArray(call) || call.length < 2)
56
+ continue;
57
+ if (call[0] !== "Email/set")
58
+ continue;
59
+ normalizeEmailSetArg(call[1]);
60
+ }
61
+ }
@@ -0,0 +1,9 @@
1
+ /** Parse JMAP responses for integration tests and dev CLIs. */
2
+ /** Throws if `EmailSubmission/set` did not create a submission. */
3
+ export declare function assertJmapSubmissionCreated(bodyText: string): void;
4
+ /**
5
+ * Throws if the first `Blob/upload` response reports `size: 0` for a non-empty
6
+ * payload (common server misconfiguration).
7
+ */
8
+ export declare function assertBlobUploadSizesNonZero(bodyText: string, expectBytes: number): void;
9
+ //# sourceMappingURL=agent-jmap-verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-jmap-verify.d.ts","sourceRoot":"","sources":["../../../../src/lib/agent/jmap/agent-jmap-verify.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAE/D,mEAAmE;AACnE,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAoBlE;AAED;;;GAGG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,IAAI,CAwBN"}
@@ -0,0 +1,50 @@
1
+ /** Parse JMAP responses for integration tests and dev CLIs. */
2
+ /** Throws if `EmailSubmission/set` did not create a submission. */
3
+ export function assertJmapSubmissionCreated(bodyText) {
4
+ let parsed;
5
+ try {
6
+ parsed = JSON.parse(bodyText);
7
+ }
8
+ catch {
9
+ throw new Error("JMAP response was not JSON; cannot verify submission.");
10
+ }
11
+ const responses = parsed.methodResponses ?? [];
12
+ for (const r of responses) {
13
+ if (!Array.isArray(r) || r[0] !== "EmailSubmission/set")
14
+ continue;
15
+ const payload = r[1];
16
+ if (payload?.created && Object.keys(payload.created).length > 0) {
17
+ return;
18
+ }
19
+ }
20
+ throw new Error("EmailSubmission/set did not create a submission. Response:\n" + bodyText);
21
+ }
22
+ /**
23
+ * Throws if the first `Blob/upload` response reports `size: 0` for a non-empty
24
+ * payload (common server misconfiguration).
25
+ */
26
+ export function assertBlobUploadSizesNonZero(bodyText, expectBytes) {
27
+ if (expectBytes <= 0)
28
+ return;
29
+ let parsed;
30
+ try {
31
+ parsed = JSON.parse(bodyText);
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ const first = parsed.methodResponses?.[0];
37
+ if (!Array.isArray(first) || first[0] !== "Blob/upload")
38
+ return;
39
+ const payload = first[1];
40
+ const created = payload?.created;
41
+ if (!created || typeof created !== "object")
42
+ return;
43
+ for (const v of Object.values(created)) {
44
+ const s = v?.size;
45
+ if (typeof s === "number" && s === 0) {
46
+ throw new Error("Blob/upload returned size 0 — this host is not persisting blob bytes. " +
47
+ "Fix the server, or use RFC 8620 binary upload when POST to uploadUrl works.");
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,89 @@
1
+ import { type JmapBlobUploadLimits } from "./agent-jmap-blob-limits.js";
2
+ import { type JmapAttachmentInput } from "./agent-jmap-blob-upload.js";
3
+ export type { JmapAttachmentInput } from "./agent-jmap-blob-upload.js";
4
+ export type { JmapBlobUploadLimits } from "./agent-jmap-blob-limits.js";
5
+ export declare const DEFAULT_JMAP_USING: readonly ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"];
6
+ export declare const BUNDLED_OPS_PRESET_NAMES: readonly ["list_inbox.json", "reply.json", "send_mail.json", "send_mail_attachment.json", "send_mail_blob_attachment.json"];
7
+ export declare const JMAP_MAIL_URN: "urn:ietf:params:jmap:mail";
8
+ /** RFC 9404 blob extension URN (Blob/upload, Blob/get, Blob/lookup). */
9
+ export declare const JMAP_BLOB_URN: "urn:ietf:params:jmap:blob";
10
+ export interface JmapEnvelope {
11
+ using: string[];
12
+ methodCalls: unknown[];
13
+ }
14
+ export declare function parseJmapEnvelope(raw: string, defaultUsing: string[], source: string): JmapEnvelope;
15
+ export declare function resolveOpsFilePath(credentialDir: string, opsFile: string): string;
16
+ export declare function readOpsFile(credentialDir: string, opsFile: string): Promise<string>;
17
+ export declare function extractPrimaryMailAccountId(session: Record<string, unknown>): string;
18
+ export interface JmapBlobEndpoints {
19
+ uploadUrl: string;
20
+ downloadUrl: string;
21
+ }
22
+ export declare function extractBlobEndpoints(session: Record<string, unknown>): JmapBlobEndpoints;
23
+ /** RFC 8620 §2 / §3.1: POST target for JMAP API calls from the Session object. */
24
+ export declare function extractJmapApiUrl(session: Record<string, unknown>): string;
25
+ /**
26
+ * RFC 9404 §3.1 blob limits for one account from GET /.well-known/jmap JSON.
27
+ * Returns null when the account does not advertise `urn:ietf:params:jmap:blob`.
28
+ */
29
+ export declare function extractBlobUploadLimits(session: Record<string, unknown>, accountId: string): JmapBlobUploadLimits | null;
30
+ export declare function fetchJmapWellKnown(apiUrl: string, capabilityJwt: string): Promise<Record<string, unknown>>;
31
+ /** Minimal surface for JMAP execution (implemented by AgentSession). */
32
+ export interface JmapSessionPort {
33
+ /** Base used for `GET /.well-known/jmap` (configured `ATOMIC_MAIL_API_URL` / credentials). */
34
+ readonly apiUrl: string;
35
+ readonly files?: {
36
+ credentialsFile: string;
37
+ };
38
+ /** RFC 8620 Session `apiUrl` — full URL for `POST` JMAP batches. */
39
+ getJmapPostUrl(): Promise<string>;
40
+ getPrimaryMailAccountId(): Promise<string>;
41
+ getCapabilityToken(): Promise<string>;
42
+ readonly currentInboxId?: string;
43
+ readonly currentUploadUrl?: string;
44
+ readonly currentDownloadUrl?: string;
45
+ /** RFC 9404 §3.1 limits from cached session; null if blob capability not advertised for the account. */
46
+ getBlobUploadLimitsForAccount(accountId: string): Promise<JmapBlobUploadLimits | null>;
47
+ }
48
+ export interface RunJmapRequestInput {
49
+ session: JmapSessionPort;
50
+ /** Raw JSON: methodCalls array or full envelope */
51
+ opsJson: string;
52
+ /** Default `using` when the envelope omits it */
53
+ defaultUsing: string[];
54
+ /** Label for parse errors */
55
+ sourceLabel: string;
56
+ dryRun?: boolean;
57
+ /**
58
+ * Local files uploaded via RFC 8620 (`POST` to session `uploadUrl`) before
59
+ * `$VAR` substitution. Injects `ATTACHMENT_0_BLOB_ID`, `ATTACHMENT_0_NAME`,
60
+ * `ATTACHMENT_0_TYPE`, `ATTACHMENT_0_SIZE`, … and `ATTACHMENT_COUNT`.
61
+ */
62
+ attachments?: JmapAttachmentInput[];
63
+ /** Base path for relative `attachments[].path` (default: process cwd). */
64
+ attachmentPathBase?: string;
65
+ /** Values for `$VAR` tokens (keys without `$`). Overrides injected attachment vars. */
66
+ vars?: Record<string, string>;
67
+ }
68
+ /**
69
+ * Parse ops JSON, substitute `$VAR_NAME` tokens (session + caller vars), POST to JMAP.
70
+ */
71
+ export declare function runJmapRequest(input: RunJmapRequestInput): Promise<{
72
+ ok: boolean;
73
+ status: number;
74
+ bodyText: string;
75
+ }>;
76
+ /**
77
+ * Resolves the JMAP `Mailbox` id for the account inbox (`role: "inbox"`).
78
+ * Used for `$INBOX_MAILBOX_ID` substitution (distinct from `$INBOX`, which is
79
+ * the mailbox *email address* — see `inboxIdToMailboxEmail` for normalization).
80
+ */
81
+ export declare function fetchInboxMailboxId(port: JmapSessionPort): Promise<string>;
82
+ export declare function postJmap(jmapPostUrl: string, capabilityJwt: string, envelope: JmapEnvelope): Promise<{
83
+ ok: boolean;
84
+ status: number;
85
+ bodyText: string;
86
+ }>;
87
+ /** Attach _next hints to a successful JMAP JSON object when parseable. */
88
+ export declare function attachJmapNextHints(bodyText: string): string;
89
+ //# sourceMappingURL=agent-jmap.d.ts.map