@rubytech/create-maxy-code 0.1.458 → 0.1.460

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 (218) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js +298 -3
  3. package/payload/platform/lib/storage-broker/dist/__tests__/cf-exec.test.js.map +1 -1
  4. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts +2 -0
  5. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.d.ts.map +1 -0
  6. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js +79 -0
  7. package/payload/platform/lib/storage-broker/dist/__tests__/object-limits.test.js.map +1 -0
  8. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts +2 -0
  9. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.d.ts.map +1 -0
  10. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js +41 -0
  11. package/payload/platform/lib/storage-broker/dist/__tests__/resource-limits.test.js.map +1 -0
  12. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts +13 -1
  13. package/payload/platform/lib/storage-broker/dist/cf-exec.d.ts.map +1 -1
  14. package/payload/platform/lib/storage-broker/dist/cf-exec.js +165 -0
  15. package/payload/platform/lib/storage-broker/dist/cf-exec.js.map +1 -1
  16. package/payload/platform/lib/storage-broker/dist/house-credential.d.ts.map +1 -1
  17. package/payload/platform/lib/storage-broker/dist/house-credential.js +4 -2
  18. package/payload/platform/lib/storage-broker/dist/house-credential.js.map +1 -1
  19. package/payload/platform/lib/storage-broker/dist/index.d.ts +2 -0
  20. package/payload/platform/lib/storage-broker/dist/index.d.ts.map +1 -1
  21. package/payload/platform/lib/storage-broker/dist/index.js +2 -0
  22. package/payload/platform/lib/storage-broker/dist/index.js.map +1 -1
  23. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts +80 -0
  24. package/payload/platform/lib/storage-broker/dist/object-limits.d.ts.map +1 -0
  25. package/payload/platform/lib/storage-broker/dist/object-limits.js +92 -0
  26. package/payload/platform/lib/storage-broker/dist/object-limits.js.map +1 -0
  27. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts +102 -0
  28. package/payload/platform/lib/storage-broker/dist/resource-limits.d.ts.map +1 -0
  29. package/payload/platform/lib/storage-broker/dist/resource-limits.js +105 -0
  30. package/payload/platform/lib/storage-broker/dist/resource-limits.js.map +1 -0
  31. package/payload/platform/lib/storage-broker/src/__tests__/cf-exec.test.ts +334 -5
  32. package/payload/platform/lib/storage-broker/src/__tests__/object-limits.test.ts +106 -0
  33. package/payload/platform/lib/storage-broker/src/__tests__/resource-limits.test.ts +49 -0
  34. package/payload/platform/lib/storage-broker/src/cf-exec.ts +221 -6
  35. package/payload/platform/lib/storage-broker/src/house-credential.ts +4 -2
  36. package/payload/platform/lib/storage-broker/src/index.ts +2 -0
  37. package/payload/platform/lib/storage-broker/src/object-limits.ts +95 -0
  38. package/payload/platform/lib/storage-broker/src/resource-limits.ts +105 -0
  39. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -6
  40. package/payload/platform/plugins/cloudflare/PLUGIN.md +3 -1
  41. package/payload/platform/plugins/cloudflare/bin/cf-token.sh +4 -3
  42. package/payload/platform/plugins/cloudflare/mcp/__tests__/auth-route.test.ts +206 -0
  43. package/payload/platform/plugins/cloudflare/mcp/__tests__/authorize.test.ts +201 -0
  44. package/payload/platform/plugins/cloudflare/mcp/__tests__/file-routes.test.ts +178 -0
  45. package/payload/platform/plugins/cloudflare/mcp/__tests__/harness.test.ts +32 -0
  46. package/payload/platform/plugins/cloudflare/mcp/__tests__/passcode.test.ts +74 -0
  47. package/payload/platform/plugins/cloudflare/mcp/__tests__/ratelimit.test.ts +77 -0
  48. package/payload/platform/plugins/cloudflare/mcp/__tests__/skill-contract.test.ts +46 -0
  49. package/payload/platform/plugins/cloudflare/mcp/__tests__/template-config.test.ts +55 -0
  50. package/payload/platform/plugins/cloudflare/mcp/__tests__/upload-route.test.ts +255 -0
  51. package/payload/platform/plugins/cloudflare/mcp/package.json +5 -2
  52. package/payload/platform/plugins/cloudflare/references/api.md +1 -1
  53. package/payload/platform/plugins/cloudflare/references/r2-object-storage.md +46 -0
  54. package/payload/platform/plugins/cloudflare/skills/data-portal/SKILL.md +123 -0
  55. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/authorize.ts +32 -0
  56. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/log.ts +12 -0
  57. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/passcode.ts +73 -0
  58. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/ratelimit.ts +55 -0
  59. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/session.ts +85 -0
  60. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/_lib/types.ts +44 -0
  61. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/auth.ts +113 -0
  62. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/delete.ts +89 -0
  63. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/download.ts +70 -0
  64. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/files.ts +41 -0
  65. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/upload.ts +151 -0
  66. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/index.html +42 -0
  67. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.css +113 -0
  68. package/payload/platform/plugins/cloudflare/skills/data-portal/template/public/portal.js +155 -0
  69. package/payload/platform/plugins/cloudflare/skills/data-portal/template/schema.sql +64 -0
  70. package/payload/platform/plugins/cloudflare/skills/data-portal/template/wrangler.toml +21 -0
  71. package/payload/platform/plugins/cloudflare/skills/site-deploy/SKILL.md +8 -0
  72. package/payload/platform/plugins/docs/references/outlook-guide.md +5 -5
  73. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +1 -0
  74. package/payload/platform/plugins/filesystem/PLUGIN.md +0 -1
  75. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts +14 -10
  76. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.d.ts.map +1 -1
  77. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js +14 -10
  78. package/payload/platform/plugins/filesystem/mcp/dist/lib/scope.js.map +1 -1
  79. package/payload/platform/plugins/outlook/.claude-plugin/plugin.json +1 -1
  80. package/payload/platform/plugins/outlook/PLUGIN.md +3 -5
  81. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts +2 -0
  82. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.d.ts.map +1 -0
  83. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js +138 -0
  84. package/payload/platform/plugins/outlook/mcp/dist/__tests__/attach.test.js.map +1 -0
  85. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts +2 -0
  86. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.d.ts.map +1 -0
  87. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js +75 -0
  88. package/payload/platform/plugins/outlook/mcp/dist/__tests__/detach.test.js.map +1 -0
  89. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts +2 -0
  90. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.d.ts.map +1 -0
  91. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js +121 -0
  92. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-attachments.test.js.map +1 -0
  93. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts +2 -0
  94. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.d.ts.map +1 -0
  95. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js +102 -0
  96. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-attachments.test.js.map +1 -0
  97. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts +2 -0
  98. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.d.ts.map +1 -0
  99. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js +187 -0
  100. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit-removal.test.js.map +1 -0
  101. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js +1 -1
  102. package/payload/platform/plugins/outlook/mcp/dist/__tests__/draft-edit.test.js.map +1 -1
  103. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts +2 -0
  104. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.d.ts.map +1 -0
  105. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js +209 -0
  106. package/payload/platform/plugins/outlook/mcp/dist/__tests__/mail-send-attachments.test.js.map +1 -0
  107. package/payload/platform/plugins/outlook/mcp/dist/index.js +12 -8
  108. package/payload/platform/plugins/outlook/mcp/dist/index.js.map +1 -1
  109. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts +37 -0
  110. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.d.ts.map +1 -0
  111. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js +79 -0
  112. package/payload/platform/plugins/outlook/mcp/dist/lib/attach.js.map +1 -0
  113. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts +43 -0
  114. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.d.ts.map +1 -0
  115. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js +65 -0
  116. package/payload/platform/plugins/outlook/mcp/dist/lib/attachment-list.js.map +1 -0
  117. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts +65 -0
  118. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.d.ts.map +1 -0
  119. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js +99 -0
  120. package/payload/platform/plugins/outlook/mcp/dist/lib/detach.js.map +1 -0
  121. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts +8 -0
  122. package/payload/platform/plugins/outlook/mcp/dist/lib/message.d.ts.map +1 -1
  123. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js +4 -0
  124. package/payload/platform/plugins/outlook/mcp/dist/lib/message.js.map +1 -1
  125. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts +36 -8
  126. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.d.ts.map +1 -1
  127. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js +40 -13
  128. package/payload/platform/plugins/outlook/mcp/dist/lib/outbound-attachments.js.map +1 -1
  129. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts +9 -5
  130. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.d.ts.map +1 -1
  131. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js +12 -8
  132. package/payload/platform/plugins/outlook/mcp/dist/lib/upload-session.js.map +1 -1
  133. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts +49 -4
  134. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.d.ts.map +1 -1
  135. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js +82 -13
  136. package/payload/platform/plugins/outlook/mcp/dist/tools/draft-edit.js.map +1 -1
  137. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts +8 -2
  138. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.d.ts.map +1 -1
  139. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js +13 -4
  140. package/payload/platform/plugins/outlook/mcp/dist/tools/draft.js.map +1 -1
  141. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts +15 -16
  142. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.d.ts.map +1 -1
  143. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js +8 -41
  144. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-attachment.js.map +1 -1
  145. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.d.ts.map +1 -1
  146. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js +3 -20
  147. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-reply.js.map +1 -1
  148. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts +27 -3
  149. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.d.ts.map +1 -1
  150. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js +53 -8
  151. package/payload/platform/plugins/outlook/mcp/dist/tools/mail-send.js.map +1 -1
  152. package/payload/platform/plugins/outlook/references/auth.md +1 -1
  153. package/payload/platform/plugins/outlook/references/graph-surfaces.md +33 -6
  154. package/payload/platform/plugins/outlook/skills/outlook/SKILL.md +22 -5
  155. package/payload/platform/plugins/storage-broker/PLUGIN.md +41 -2
  156. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts +2 -0
  157. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.d.ts.map +1 -0
  158. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js +85 -0
  159. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/confine-path.test.js.map +1 -0
  160. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts +2 -0
  161. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.d.ts.map +1 -0
  162. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js +77 -0
  163. package/payload/platform/plugins/storage-broker/mcp/dist/__tests__/put-source.test.js.map +1 -0
  164. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts +12 -0
  165. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.d.ts.map +1 -0
  166. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js +107 -0
  167. package/payload/platform/plugins/storage-broker/mcp/dist/confine-path.js.map +1 -0
  168. package/payload/platform/plugins/storage-broker/mcp/dist/index.js +86 -0
  169. package/payload/platform/plugins/storage-broker/mcp/dist/index.js.map +1 -1
  170. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts +11 -0
  171. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.d.ts.map +1 -0
  172. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js +35 -0
  173. package/payload/platform/plugins/storage-broker/mcp/dist/put-source.js.map +1 -0
  174. package/payload/platform/plugins/voice-mirror/PLUGIN.md +19 -7
  175. package/payload/platform/plugins/voice-mirror/mcp/dist/index.d.ts.map +1 -1
  176. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js +35 -16
  177. package/payload/platform/plugins/voice-mirror/mcp/dist/index.js.map +1 -1
  178. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts +31 -0
  179. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.d.ts.map +1 -0
  180. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js +87 -0
  181. package/payload/platform/plugins/voice-mirror/mcp/dist/lib/admin-user.js.map +1 -0
  182. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  183. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js +13 -3
  184. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  185. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
  186. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js +2 -1
  187. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
  188. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  189. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js +2 -1
  190. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  191. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts +2 -2
  192. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  193. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js +12 -3
  194. package/payload/platform/plugins/voice-mirror/mcp/dist/tools/voice-tag-content.js.map +1 -1
  195. package/payload/platform/plugins/voice-mirror/mcp/scripts/smoke.mjs +334 -0
  196. package/payload/platform/plugins/voice-mirror/skills/voice-mirror/SKILL.md +13 -0
  197. package/payload/platform/scripts/logs-read-jsonl.test.sh +118 -0
  198. package/payload/platform/scripts/logs-read.sh +63 -9
  199. package/payload/platform/scripts/voice-author-key-audit.sh +155 -0
  200. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  201. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +4 -0
  202. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  203. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts +17 -0
  204. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.d.ts.map +1 -0
  205. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js +88 -0
  206. package/payload/platform/services/claude-session-manager/dist/mcp-descriptor-resolve.js.map +1 -0
  207. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +0 -1
  208. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  209. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +34 -23
  210. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  211. package/payload/server/{chunk-64FGYKNZ.js → chunk-JXWFVE5X.js} +148 -0
  212. package/payload/server/{chunk-F4D35LKL.js → chunk-POBXIHOF.js} +34 -0
  213. package/payload/server/maxy-edge.js +1 -1
  214. package/payload/server/server.js +412 -37
  215. package/payload/server/{src-S7C4P6TT.js → src-4F37OHLK.js} +19 -1
  216. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.d.ts +0 -2
  217. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js +0 -215
  218. package/payload/platform/plugins/outlook/mcp/dist/__tests__/pkce-flow.test.js.map +0 -1
@@ -19,7 +19,7 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
19
19
  | `outlook-mail-list` | Mail newest-first. Default top=25, folder=Inbox. `folder` takes a well-known name or alias (case/space-insensitive): Inbox, Sent Items/sent, Drafts, Deleted Items/trash, Junk Email/junk/spam, Archive, Outbox — or a raw folder id. Custom folder names are not resolved. `since`/`before` (ISO date-times) narrow by receipt date. The 250-per-page figure is not a ceiling: the reply carries `nextCursor` when older mail remains — pass it back as `cursor` to page further back. Filter by sender/subject via `outlook-mail-search`. |
20
20
  | `outlook-mail-search` | Microsoft Graph `$search` (KQL) over the mailbox. Preview-only, like `outlook-mail-list`, and pages the same way via `nextCursor`/`cursor`. Put sender/subject/recipient/date filters in the query: `from:`, `to:`, `subject:`, `received>=`. Graph forbids `$search`+`$filter`, so a strict date window is on `outlook-mail-list`. |
21
21
  | `outlook-mail-fetch-body` | Complete body of one message by id (`GET /me/messages/{id}?$select=body`). HTML decoded to text, no preview cap. The full-read path — list/search return Microsoft's ~255-char preview only. Returns the envelope plus the whole body. |
22
- | `outlook-mail-attachment` | List or download a message's inbound attachments. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
22
+ | `outlook-mail-attachment` | List or download any message's attachments — received or draft. With `messageId` alone, LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of file/item/reference) via `GET /me/messages/{id}/attachments` and `$select` (no bytes). A draft is a message in the same collection, so passing a draftId lists what that draft carries; the returned `name`/`id` is what `outlook-draft-edit`'s `removeAttachments` takes. With `messageId`+`attachmentId`, DOWNLOADS one file attachment: its bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — item/reference kinds, empty payloads, and anything over 25 MB are refused with nothing written. `outlook-mail-list` / `outlook-mail-search` / `outlook-mail-fetch-body` report `hasAttachments` so the agent knows when to call this. Needs `Mail.Read`. |
23
23
  | `outlook-mail-reply` | Reply in-thread (`createReply` / `createReplyAll` + send). Threads natively via conversationId. `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file; files over Graph's ~3 MB inline limit use a chunked upload session). Body is plain text, prepended above the quote. Needs `Mail.ReadWrite` + `Mail.Send`. |
24
24
  | `outlook-mail-delete` | Move messages to Deleted Items (`POST /me/messages/{id}/move`). Recoverable — never hard-deletes. An id already gone is counted not-moved. Needs `Mail.ReadWrite`. |
25
25
  | `outlook-mail-otp-extract` | Poll the inbox for a one-time code from a sender (domain). Reads full bodies to extract it; optional subject regex and timeout (default 60 s). |
@@ -33,9 +33,9 @@ The `outlook` plugin gives the admin agent access to Microsoft 365 / Outlook.com
33
33
  | `outlook-contacts-list` | Top contacts. Default top=50. |
34
34
  | `outlook-mailbox-info` | Health probe for one mailbox — auth state, refresh-window, folder count. Name the mailbox with `mailbox` when several are attached. |
35
35
  | `outlook-mailbox-list` | Every connected mailbox with its email, scopes, and token health. Local only — no network — so it also flags a mailbox whose stored credentials cannot be read. |
36
- | `outlook-mail-send` | Send a message (`POST /me/sendMail`). to/cc/bcc arrays, subject, body, isHtml. Success asserted on Graph 202. Needs `Mail.Send`. |
37
- | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model. Returns the draft id. Needs `Mail.ReadWrite`. |
38
- | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`). Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml). Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
36
+ | `outlook-mail-send` | Send a message. No attachments → `POST /me/sendMail`; with attachments → draft, attach, send (`/me/sendMail` carries attachments inline and cannot fit a large file). to/cc/bcc arrays, subject, body, isHtml, attachments (account-dir paths, 25 MB/file). Success asserted on Graph 202; no message id on either route. Needs `Mail.Send` (+ `Mail.ReadWrite` to attach). |
37
+ | `outlook-draft` | Create a draft in Drafts (`POST /me/messages`). Same body model, including attachments — files are attached before the id is returned, so the draft is ready to send. Returns the draft id. Needs `Mail.ReadWrite`. |
38
+ | `outlook-draft-edit` | Update an existing draft in place (`PATCH /me/messages/{draftId}`) and/or add and remove attachments. Graph drafts are mutable — pass draftId plus the fields to change (to/cc/bcc/subject/body/isHtml/attachments/removeAttachments). `attachments` adds; `removeAttachments` takes files off by name or attachment id; both together replace a file in one call. Sent messages cannot be edited. Needs `Mail.ReadWrite`. |
39
39
  | `outlook-draft-send` | Send an existing draft by id (`POST /me/messages/{id}/send`). Graph consumes the draft, returns 202. Needs `Mail.Send`. |
40
40
 
41
41
  ## Observability
@@ -110,4 +110,4 @@ Latency triage: `mail-list count=0 elapsedMs<200` consistent → permissions iss
110
110
 
111
111
  ## Out of scope
112
112
 
113
- Mail move to arbitrary folders / flag, hard (permanent) delete, ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
113
+ Mail move to arbitrary folders / flag, hard (permanent) delete, removing an attachment from a **sent** message (Graph forbids it), ingesting reviewed mail into the business graph (the `email-ingest` analogue), contacts write, recurring-series occurrence editing, the `scheduling` Neo4j calendar and any auto-sync with it, OneDrive / Files, push notifications, on-premises Exchange, M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`), public-agent exposure, multi-tenant federation. Mail read, full-body read, inbound attachment list/download, reply, delete (to Deleted Items), and compose — all four outbound paths carrying attachments, with a draft's attachments editable in place (add, remove, replace) — are supported (`outlook-mail-fetch-body`, `outlook-mail-attachment`, `outlook-mail-reply`, `outlook-mail-delete`, `outlook-mail-otp-extract`, `outlook-mail-send`, `outlook-draft`, `outlook-draft-edit`, `outlook-draft-send`), and the calendar is read plus control. See `platform/plugins/outlook/PLUGIN.md` for the full out-of-scope list.
@@ -66,6 +66,7 @@ Voice mirror is on by default for every drafting skill. To opt out for one, set
66
66
  - **Copy public figures** — voice mirror only learns from your own writing.
67
67
  - **Clone audio** — text only, no speech synthesis.
68
68
  - **Guess** — historical content stays `unknown` until you mark it. {{productName}} never auto-classifies your writing. (Automatic ingestion applies only to your live PTY sessions where authorship is certain.)
69
+ - **Invent an identity** — a voice belongs to a real admin user on the account. Attribution is refused outright if it names anyone else, and nothing is written. A person's name is not an identity, so tagging someone's writing on a shared account needs their actual admin id, not their name.
69
70
 
70
71
  ## Status
71
72
 
@@ -31,7 +31,6 @@ mcp:
31
31
  LOG_DIR: ${LOG_DIR}
32
32
  PLATFORM_ROOT: ${PLATFORM_ROOT}
33
33
  ACCOUNT_ID: ${ACCOUNT_ID}
34
- HOUSE_ADMIN_SCOPE: ${HOUSE_ADMIN_SCOPE}
35
34
  SESSION_ID: ${SESSION_ID}
36
35
  AGENT_SLUG: ${AGENT_SLUG}
37
36
  mcp-manifest: auto
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * Read HOUSE_ADMIN_SCOPE for the plugin, fail-closed. HOUSE_ADMIN_SCOPE is the
3
3
  * whole boundary: it is present in the spawn env ONLY for a house role:admin
4
- * session (Task 1440). This plugin's PLUGIN.md also declares the key with a
5
- * `${HOUSE_ADMIN_SCOPE}` placeholder so the value flows on loaders that pass
6
- * only declared env keys but an UNSET placeholder is substituted
7
- * differently across spawn loaders: some yield empty, some leave the literal
8
- * `${HOUSE_ADMIN_SCOPE}` string (which is truthy). Treating a literal
9
- * placeholder or an empty value as "no scope" makes a non-house session
10
- * deterministically fail-closed regardless of loader behaviour — the read
11
- * never trusts a value it did not receive as a real scope. (This is why the
12
- * plugin does not call the shared `readHouseAdminScope`, whose truthy test
13
- * would accept the literal placeholder.)
4
+ * session (Task 1440), and it reaches this server by INHERITANCE from that
5
+ * env never from a declared env block. Task 1700 deleted this plugin's
6
+ * `HOUSE_ADMIN_SCOPE: ${HOUSE_ADMIN_SCOPE}` declaration: the key is not in the
7
+ * platform placeholder map, so declaring it could only ever yield the literal
8
+ * `${HOUSE_ADMIN_SCOPE}` string (which is truthy), and the `mcp-manifest: auto`
9
+ * generator discards the block anyway. Do not restore the declaration.
10
+ *
11
+ * The literal-placeholder guard below stays as defence in depth. Since Task
12
+ * 1700 the platform drops an unresolvable key rather than emitting its literal,
13
+ * so no loader should now deliver one — but treating a literal placeholder or
14
+ * an empty value as "no scope" keeps a non-house session deterministically
15
+ * fail-closed regardless of loader behaviour: the read never trusts a value it
16
+ * did not receive as a real scope. (This is why the plugin does not call the
17
+ * shared `readHouseAdminScope`, whose truthy test would accept the literal.)
14
18
  */
15
19
  export declare function readPluginHouseScope(env: NodeJS.ProcessEnv): string | null;
16
20
  export interface Ctx {
@@ -1 +1 @@
1
- {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/lib/scope.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,GAAG,IAAI,CAK1E;AAED,MAAM,WAAW,GAAG;IAClB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClC;IACE,MAAM,EACF,sBAAsB,GACtB,8BAA8B,GAC9B,sCAAsC,CAAC;CAC5C,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,GAAG,EACR,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,IAAI,EAAE,MAAM,GACX,iBAAiB,CA2BnB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAenE"}
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../../src/lib/scope.ts"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,GAAG,IAAI,CAK1E;AAED,MAAM,WAAW,GAAG;IAClB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,6EAA6E;IAC7E,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAClC;IACE,MAAM,EACF,sBAAsB,GACtB,8BAA8B,GAC9B,sCAAsC,CAAC;CAC5C,CAAC;AAEN;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,GAAG,EACR,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,IAAI,EAAE,MAAM,GACX,iBAAiB,CA2BnB;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAenE"}
@@ -20,16 +20,20 @@ const short = (id) => id.slice(0, 8);
20
20
  /**
21
21
  * Read HOUSE_ADMIN_SCOPE for the plugin, fail-closed. HOUSE_ADMIN_SCOPE is the
22
22
  * whole boundary: it is present in the spawn env ONLY for a house role:admin
23
- * session (Task 1440). This plugin's PLUGIN.md also declares the key with a
24
- * `${HOUSE_ADMIN_SCOPE}` placeholder so the value flows on loaders that pass
25
- * only declared env keys but an UNSET placeholder is substituted
26
- * differently across spawn loaders: some yield empty, some leave the literal
27
- * `${HOUSE_ADMIN_SCOPE}` string (which is truthy). Treating a literal
28
- * placeholder or an empty value as "no scope" makes a non-house session
29
- * deterministically fail-closed regardless of loader behaviour — the read
30
- * never trusts a value it did not receive as a real scope. (This is why the
31
- * plugin does not call the shared `readHouseAdminScope`, whose truthy test
32
- * would accept the literal placeholder.)
23
+ * session (Task 1440), and it reaches this server by INHERITANCE from that
24
+ * env never from a declared env block. Task 1700 deleted this plugin's
25
+ * `HOUSE_ADMIN_SCOPE: ${HOUSE_ADMIN_SCOPE}` declaration: the key is not in the
26
+ * platform placeholder map, so declaring it could only ever yield the literal
27
+ * `${HOUSE_ADMIN_SCOPE}` string (which is truthy), and the `mcp-manifest: auto`
28
+ * generator discards the block anyway. Do not restore the declaration.
29
+ *
30
+ * The literal-placeholder guard below stays as defence in depth. Since Task
31
+ * 1700 the platform drops an unresolvable key rather than emitting its literal,
32
+ * so no loader should now deliver one — but treating a literal placeholder or
33
+ * an empty value as "no scope" keeps a non-house session deterministically
34
+ * fail-closed regardless of loader behaviour: the read never trusts a value it
35
+ * did not receive as a real scope. (This is why the plugin does not call the
36
+ * shared `readHouseAdminScope`, whose truthy test would accept the literal.)
33
37
  */
34
38
  export function readPluginHouseScope(env) {
35
39
  const raw = env.HOUSE_ADMIN_SCOPE;
@@ -1 +1 @@
1
- {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/lib/scope.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../../src/lib/scope.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "outlook",
3
- "description": "Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list a message's inbound attachments, or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.",
3
+ "description": "Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list any message's attachments (received or draft), or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send, all with attachments; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: outlook
3
- description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list a message's inbound attachments, or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.
3
+ description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list any message's attachments (received or draft), or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send, all with attachments; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.
4
4
  tools:
5
5
  - name: outlook-account-register
6
6
  publicAllowlist: false
@@ -85,8 +85,6 @@ mcp:
85
85
  PLATFORM_ROOT: ${PLATFORM_ROOT}
86
86
  ACCOUNT_ID: ${ACCOUNT_ID}
87
87
  SESSION_ID: ${SESSION_ID}
88
- OUTLOOK_CLIENT_ID: ${OUTLOOK_CLIENT_ID}
89
- OUTLOOK_TENANT_ID: ${OUTLOOK_TENANT_ID}
90
88
  mcp-manifest: auto
91
89
  ---
92
90
 
@@ -99,10 +97,10 @@ Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (
99
97
  - **Register:** `outlook-account-register` — starts the OAuth device-code flow against the brand's Entra app and returns a user code + verification URL immediately. The operator opens `microsoft.com/devicelogin` on any device, enters the code, and consents. The always-running server then completes registration on its own: a self-contained dispatcher (`dist/scripts/complete-registration.js`), armed on a 30 s interval, polls every live pending code to a terminal outcome, so a mailbox connects once the operator consents with no agent action. `outlook-account-register-poll` remains the manual path and the fallback, one check per call. Registering a second mailbox on the same account adds it alongside the first rather than replacing it; each mailbox holds its own encrypted tokens (AES-256-CBC) under a shared account key. One device-code flow runs at a time per account; starting another while one is live returns an `in-progress` signal. Observability: `devicecode-autopoll` per poll cycle and an independent `devicecode-stranded` audit for any pending code left un-harvested past its lifetime.
100
98
  - **Multiple mailboxes:** one account can hold several mailboxes. Every operational tool takes an optional `mailbox` argument (email or graphUserId). With one mailbox, omit it. With two or more, name it — omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows them all.
101
99
  - **Mail (read):** `outlook-mail-list` returns messages newest-first with an optional `since`/`before` date window, and `outlook-mail-search` runs a Graph `$search` (KQL) query where `from:`/`to:`/`subject:`/`received>=` filters live — both return Microsoft's short `bodyPreview` (~255 chars) for cheap triage and a `nextCursor` for paging beyond the 250-per-page window (the cursor is an opaque `@odata.nextLink`; pass it back as `cursor`). Graph forbids `$search`+`$filter`, so the strict date window is on the list tool and sender/subject filtering is on the search tool. `outlook-mail-fetch-body` reads the COMPLETE body of one message by id (HTML decoded to text, no preview cap) — the full-read path. All three read tools report `hasAttachments` per message, so the agent knows to reach for the attachment tool.
102
- - **Mail (attachments):** `outlook-mail-attachment` lists or downloads a message's inbound attachments. Called with `messageId` alone it LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of `file`/`item`/`reference`) via `GET /me/messages/{id}/attachments` with `$select` (no bytes). Called with `messageId`+`attachmentId` it DOWNLOADS one file attachment: the bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved absolute path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — `item` (embedded message/event) and `reference` (cloud-drive link) kinds are refused, as are empty payloads and anything over the 25 MB cap; in each case nothing is written. Requires `Mail.Read`.
100
+ - **Mail (attachments):** `outlook-mail-attachment` lists or downloads any message's attachments — received or draft. Called with `messageId` alone it LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of `file`/`item`/`reference`) via `GET /me/messages/{id}/attachments` with `$select` (no bytes); a draft is a message in the same collection, so passing a draftId lists what that draft carries, and the returned `name`/`id` is the handle `outlook-draft-edit`'s `removeAttachments` takes. Called with `messageId`+`attachmentId` it DOWNLOADS one file attachment: the bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved absolute path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — `item` (embedded message/event) and `reference` (cloud-drive link) kinds are refused, as are empty payloads and anything over the 25 MB cap; in each case nothing is written. Requires `Mail.Read`.
103
101
  - **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
104
102
  - **Mail (OTP):** `outlook-mail-otp-extract` polls the inbox for a one-time verification code from a named sender, reading full bodies to extract it — used during service authentication.
105
- - **Mail (send / draft):** `outlook-mail-send` sends via `POST /me/sendMail` (success asserted on Graph 202); `outlook-draft` creates a draft and returns its id; `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable); `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
103
+ - **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id; `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
106
104
  - **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
107
105
  - **Contacts (read-only):** `outlook-contacts-list` returns contacts.
108
106
  - **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every attached mailbox with its email, scopes, and token health, reading local stores only, never the network. It reports an `addressable` count and a per-mailbox `status` (`ok` / `blank-email` / `refresh-expired` / `unreadable`) so an authenticated, addressable mailbox is distinguishable from one that is merely present: a bare `count` of 1 is not "connected" unless that mailbox is `status:ok`. Use these to answer "did mailbox X auth?" without grepping logs.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=attach.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/attach.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,138 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { TokenStore } from "../auth/token-store.js";
7
+ import { attachFilesToDraft, resolveAttachmentsOrRefuse } from "../lib/attach.js";
8
+ import { AttachmentRefusedError, OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES, OUTLOOK_ATTACHMENT_MAX_BYTES, resolveOutboundAttachments, } from "../lib/outbound-attachments.js";
9
+ function configWithToken(accountId) {
10
+ const dir = mkdtempSync(join(tmpdir(), "outlook-attach-"));
11
+ const tokenStore = new TokenStore(accountId, dir);
12
+ tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
13
+ return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
14
+ }
15
+ const UPLOAD_URL = "https://upload.example.com/session/attach";
16
+ function mockRouted() {
17
+ const calls = [];
18
+ const original = globalThis.fetch;
19
+ globalThis.fetch = (async (input, init) => {
20
+ const req = input instanceof Request ? input : null;
21
+ const url = req ? req.url : typeof input === "string" ? input : input.toString();
22
+ const method = req?.method ?? init?.method ?? "GET";
23
+ const isGraph = url.includes("graph.microsoft.com");
24
+ const isUpload = url.startsWith(UPLOAD_URL);
25
+ if (!isGraph && !isUpload)
26
+ return original(input, init);
27
+ let body = null;
28
+ if (req)
29
+ body = await req.clone().text().catch(() => null);
30
+ else if (typeof init?.body === "string")
31
+ body = init.body;
32
+ const contentRange = req?.headers.get("content-range") ?? new Headers(init?.headers).get("content-range");
33
+ calls.push({ url: contentRange ? `${url}#${contentRange}` : url, method, body });
34
+ if (isUpload) {
35
+ const m = contentRange?.match(/bytes (\d+)-(\d+)\/(\d+)/);
36
+ const final = m ? Number(m[2]) + 1 === Number(m[3]) : true;
37
+ return new Response(JSON.stringify({ id: "ATT1" }), {
38
+ status: final ? 201 : 200, headers: { "content-type": "application/json" },
39
+ });
40
+ }
41
+ if (url.includes("/createUploadSession")) {
42
+ return new Response(JSON.stringify({ uploadUrl: UPLOAD_URL }), {
43
+ status: 201, headers: { "content-type": "application/json" },
44
+ });
45
+ }
46
+ return new Response(JSON.stringify({ id: "ATT1" }), { status: 201, headers: { "content-type": "application/json" } });
47
+ });
48
+ return { restore: () => { globalThis.fetch = original; }, calls };
49
+ }
50
+ test("attachFilesToDraft: a small file takes one inline attachments POST", async () => {
51
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a1-"));
52
+ process.env.ACCOUNT_DIR = dir;
53
+ const small = join(dir, "note.txt");
54
+ writeFileSync(small, "tiny");
55
+ const mock = mockRouted();
56
+ try {
57
+ const files = resolveOutboundAttachments("acct-a1", [small]);
58
+ await attachFilesToDraft(configWithToken("acct-a1"), "DRAFTX", files, "draft");
59
+ assert.ok(!mock.calls.some((c) => /createUploadSession/.test(c.url)), "no upload session for a small file");
60
+ const posts = mock.calls.filter((c) => c.method === "POST" && /\/DRAFTX\/attachments$/.test(c.url));
61
+ assert.equal(posts.length, 1, "exactly one inline attachments POST");
62
+ assert.match(posts[0].body ?? "", /#microsoft\.graph\.fileAttachment/);
63
+ }
64
+ finally {
65
+ mock.restore();
66
+ delete process.env.ACCOUNT_DIR;
67
+ }
68
+ });
69
+ test("attachFilesToDraft: a file over the inline limit streams through an upload session", async () => {
70
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a2-"));
71
+ process.env.ACCOUNT_DIR = dir;
72
+ const big = join(dir, "big.pdf");
73
+ writeFileSync(big, Buffer.alloc(OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES + 1024 * 1024));
74
+ const mock = mockRouted();
75
+ try {
76
+ const files = resolveOutboundAttachments("acct-a2", [big]);
77
+ await attachFilesToDraft(configWithToken("acct-a2"), "DRAFTY", files, "mail-send");
78
+ assert.ok(mock.calls.some((c) => /\/createUploadSession$/.test(c.url)), "createUploadSession invoked");
79
+ assert.ok(mock.calls.some((c) => c.method === "PUT" && c.url.startsWith(UPLOAD_URL)), "chunk PUT issued");
80
+ }
81
+ finally {
82
+ mock.restore();
83
+ delete process.env.ACCOUNT_DIR;
84
+ }
85
+ });
86
+ test("resolveOutboundAttachments: an oversize file throws AttachmentRefusedError reason=oversize", () => {
87
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a3-"));
88
+ process.env.ACCOUNT_DIR = dir;
89
+ const huge = join(dir, "huge.bin");
90
+ writeFileSync(huge, Buffer.alloc(OUTLOOK_ATTACHMENT_MAX_BYTES + 1));
91
+ try {
92
+ assert.throws(() => resolveOutboundAttachments("acct-a3", [huge]), (err) => {
93
+ assert.ok(err instanceof AttachmentRefusedError, "typed refusal");
94
+ assert.equal(err.reason, "oversize");
95
+ assert.equal(err.file, "huge.bin");
96
+ assert.equal(err.cap, OUTLOOK_ATTACHMENT_MAX_BYTES);
97
+ assert.match(err.message, /25 MB/);
98
+ return true;
99
+ });
100
+ }
101
+ finally {
102
+ delete process.env.ACCOUNT_DIR;
103
+ }
104
+ });
105
+ test("resolveOutboundAttachments: a path outside the account dir throws reason=bad-path", () => {
106
+ process.env.ACCOUNT_DIR = mkdtempSync(join(tmpdir(), "outlook-acct-a4-"));
107
+ const outside = join(mkdtempSync(join(tmpdir(), "elsewhere-a4-")), "x.txt");
108
+ writeFileSync(outside, "x");
109
+ try {
110
+ assert.throws(() => resolveOutboundAttachments("acct-a4", [outside]), (err) => {
111
+ assert.ok(err instanceof AttachmentRefusedError, "typed refusal");
112
+ assert.equal(err.reason, "bad-path");
113
+ assert.match(err.message, /outside the account directory/);
114
+ return true;
115
+ });
116
+ }
117
+ finally {
118
+ delete process.env.ACCOUNT_DIR;
119
+ }
120
+ });
121
+ test("resolveAttachmentsOrRefuse: rethrows the refusal and returns files on the happy path", () => {
122
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-a5-"));
123
+ process.env.ACCOUNT_DIR = dir;
124
+ const ok = join(dir, "ok.txt");
125
+ writeFileSync(ok, "fine");
126
+ try {
127
+ assert.deepEqual(resolveAttachmentsOrRefuse("acct-a5", undefined, "draft"), []);
128
+ const files = resolveAttachmentsOrRefuse("acct-a5", [ok], "draft");
129
+ assert.equal(files.length, 1);
130
+ assert.equal(files[0].name, "ok.txt");
131
+ assert.equal(files[0].mode, "inline");
132
+ assert.throws(() => resolveAttachmentsOrRefuse("acct-a5", [join(dir, "nope.txt")], "draft"), /Attachment not found/);
133
+ }
134
+ finally {
135
+ delete process.env.ACCOUNT_DIR;
136
+ }
137
+ });
138
+ //# sourceMappingURL=attach.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attach.test.js","sourceRoot":"","sources":["../../src/__tests__/attach.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=detach.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detach.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/detach.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { AttachmentRefusedError } from "../lib/outbound-attachments.js";
4
+ import { resolveRemovalsOrRefuse } from "../lib/detach.js";
5
+ function item(id, name) {
6
+ return { id, name, contentType: "application/pdf", size: 10, isInline: false, kind: "file" };
7
+ }
8
+ test("resolveRemovalsOrRefuse: resolves a target by exact name", () => {
9
+ const items = [item("A1", "report.pdf"), item("A2", "notes.txt")];
10
+ const out = resolveRemovalsOrRefuse(items, ["report.pdf"], "draft-edit");
11
+ assert.deepEqual(out, [{ attachmentId: "A1", name: "report.pdf" }]);
12
+ });
13
+ test("resolveRemovalsOrRefuse: resolves a target by attachment id", () => {
14
+ const items = [item("A1", "report.pdf"), item("A2", "notes.txt")];
15
+ const out = resolveRemovalsOrRefuse(items, ["A2"], "draft-edit");
16
+ assert.deepEqual(out, [{ attachmentId: "A2", name: "notes.txt" }]);
17
+ });
18
+ test("resolveRemovalsOrRefuse: an id match wins over a name match", () => {
19
+ // A file literally named "A2" alongside an attachment whose id is "A2".
20
+ const items = [item("A1", "A2"), item("A2", "notes.txt")];
21
+ const out = resolveRemovalsOrRefuse(items, ["A2"], "draft-edit");
22
+ assert.deepEqual(out, [{ attachmentId: "A2", name: "notes.txt" }], "the precise handle wins");
23
+ });
24
+ test("resolveRemovalsOrRefuse: a name not on the draft refuses not-on-draft and names what IS on it", () => {
25
+ const items = [item("A1", "report.pdf"), item("A2", "notes.txt")];
26
+ assert.throws(() => resolveRemovalsOrRefuse(items, ["absent.pdf"], "draft-edit"), (err) => {
27
+ assert.ok(err instanceof AttachmentRefusedError);
28
+ assert.equal(err.reason, "not-on-draft");
29
+ assert.equal(err.file, "absent.pdf");
30
+ assert.match(err.message, /report\.pdf/);
31
+ assert.match(err.message, /notes\.txt/);
32
+ return true;
33
+ });
34
+ });
35
+ test("resolveRemovalsOrRefuse: an empty draft refuses not-on-draft without inventing contents", () => {
36
+ assert.throws(() => resolveRemovalsOrRefuse([], ["absent.pdf"], "draft-edit"), (err) => {
37
+ assert.ok(err instanceof AttachmentRefusedError);
38
+ assert.equal(err.reason, "not-on-draft");
39
+ assert.match(err.message, /no attachments/);
40
+ return true;
41
+ });
42
+ });
43
+ test("resolveRemovalsOrRefuse: a name matching two attachments refuses ambiguous-name with the ids", () => {
44
+ const items = [item("A1", "report.pdf"), item("A2", "report.pdf")];
45
+ assert.throws(() => resolveRemovalsOrRefuse(items, ["report.pdf"], "draft-edit"), (err) => {
46
+ assert.ok(err instanceof AttachmentRefusedError);
47
+ assert.equal(err.reason, "ambiguous-name");
48
+ assert.equal(err.file, "report.pdf");
49
+ assert.match(err.message, /A1/);
50
+ assert.match(err.message, /A2/);
51
+ return true;
52
+ });
53
+ });
54
+ test("resolveRemovalsOrRefuse: resolves every target in one call", () => {
55
+ const items = [item("A1", "report.pdf"), item("A2", "notes.txt")];
56
+ const out = resolveRemovalsOrRefuse(items, ["report.pdf", "A2"], "draft-edit");
57
+ assert.deepEqual(out, [
58
+ { attachmentId: "A1", name: "report.pdf" },
59
+ { attachmentId: "A2", name: "notes.txt" },
60
+ ]);
61
+ });
62
+ test("resolveRemovalsOrRefuse: two targets naming the same attachment collapse to one removal", () => {
63
+ // An agent holding both handles for one file from a listing. Without the
64
+ // dedup the second DELETE 404s and the call reports failure for work that
65
+ // wholly succeeded.
66
+ const items = [item("A1", "report.pdf"), item("A2", "notes.txt")];
67
+ const out = resolveRemovalsOrRefuse(items, ["A1", "report.pdf"], "draft-edit");
68
+ assert.deepEqual(out, [{ attachmentId: "A1", name: "report.pdf" }]);
69
+ });
70
+ test("resolveRemovalsOrRefuse: the same target twice collapses to one removal", () => {
71
+ const items = [item("A1", "report.pdf")];
72
+ const out = resolveRemovalsOrRefuse(items, ["report.pdf", "report.pdf"], "draft-edit");
73
+ assert.deepEqual(out, [{ attachmentId: "A1", name: "report.pdf" }]);
74
+ });
75
+ //# sourceMappingURL=detach.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detach.test.js","sourceRoot":"","sources":["../../src/__tests__/detach.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=draft-attachments.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"draft-attachments.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/draft-attachments.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,121 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { TokenStore } from "../auth/token-store.js";
7
+ import { runDraft } from "../tools/draft.js";
8
+ import { OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES, OUTLOOK_ATTACHMENT_MAX_BYTES } from "../lib/outbound-attachments.js";
9
+ function configWithToken(accountId) {
10
+ const dir = mkdtempSync(join(tmpdir(), "outlook-draft-"));
11
+ const tokenStore = new TokenStore(accountId, dir);
12
+ tokenStore.store("access-token", "refresh-token", 3600, { scopes: ["Mail.ReadWrite", "Mail.Send"] });
13
+ return { clientId: "test-client", tenantId: "common", accountId, tokenStore };
14
+ }
15
+ const UPLOAD_URL = "https://upload.example.com/session/draft";
16
+ function mockRouted() {
17
+ const calls = [];
18
+ const original = globalThis.fetch;
19
+ globalThis.fetch = (async (input, init) => {
20
+ const req = input instanceof Request ? input : null;
21
+ const url = req ? req.url : typeof input === "string" ? input : input.toString();
22
+ const method = req?.method ?? init?.method ?? "GET";
23
+ const isGraph = url.includes("graph.microsoft.com");
24
+ const isUpload = url.startsWith(UPLOAD_URL);
25
+ if (!isGraph && !isUpload)
26
+ return original(input, init);
27
+ let body = null;
28
+ if (req)
29
+ body = await req.clone().text().catch(() => null);
30
+ else if (typeof init?.body === "string")
31
+ body = init.body;
32
+ const contentRange = req?.headers.get("content-range") ?? new Headers(init?.headers).get("content-range");
33
+ calls.push({ url: contentRange ? `${url}#${contentRange}` : url, method, body });
34
+ if (isUpload)
35
+ return new Response(JSON.stringify({ id: "ATT1" }), { status: 201, headers: { "content-type": "application/json" } });
36
+ if (url.includes("/createUploadSession")) {
37
+ return new Response(JSON.stringify({ uploadUrl: UPLOAD_URL }), { status: 201, headers: { "content-type": "application/json" } });
38
+ }
39
+ return new Response(JSON.stringify({ id: "D1" }), { status: 201, headers: { "content-type": "application/json" } });
40
+ });
41
+ return { restore: () => { globalThis.fetch = original; }, calls };
42
+ }
43
+ const BASE = { to: ["rob@ecolecltd.co.uk"], subject: "Pricing", body: "See attached" };
44
+ test("outlook-draft: no attachments creates the draft with one POST", async () => {
45
+ const mock = mockRouted();
46
+ try {
47
+ const r = await runDraft(configWithToken("acct-d1"), BASE);
48
+ assert.equal(r.draftId, "D1");
49
+ assert.deepEqual(r.attachments, []);
50
+ assert.equal(mock.calls.length, 1);
51
+ assert.match(mock.calls[0].url, /\/me\/messages$/);
52
+ }
53
+ finally {
54
+ mock.restore();
55
+ }
56
+ });
57
+ test("outlook-draft: a small attachment is attached to the created draft before the id returns", async () => {
58
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d2-"));
59
+ process.env.ACCOUNT_DIR = dir;
60
+ const file = join(dir, "schedule.pdf");
61
+ writeFileSync(file, "pricing");
62
+ const mock = mockRouted();
63
+ try {
64
+ const r = await runDraft(configWithToken("acct-d2"), { ...BASE, attachments: [file] });
65
+ assert.equal(r.draftId, "D1");
66
+ assert.deepEqual(r.attachments, ["schedule.pdf"]);
67
+ assert.match(mock.calls[0].url, /\/me\/messages$/, "draft created first");
68
+ const attachPosts = mock.calls.filter((c) => c.method === "POST" && /\/D1\/attachments$/.test(c.url));
69
+ assert.equal(attachPosts.length, 1, "exactly one inline attachments POST");
70
+ assert.match(attachPosts[0].body ?? "", /#microsoft\.graph\.fileAttachment/);
71
+ }
72
+ finally {
73
+ mock.restore();
74
+ delete process.env.ACCOUNT_DIR;
75
+ }
76
+ });
77
+ test("outlook-draft: an attachment over the inline limit streams via an upload session", async () => {
78
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d3-"));
79
+ process.env.ACCOUNT_DIR = dir;
80
+ const big = join(dir, "big.pdf");
81
+ writeFileSync(big, Buffer.alloc(OUTLOOK_ATTACHMENT_INLINE_MAX_BYTES + 1024 * 1024));
82
+ const mock = mockRouted();
83
+ try {
84
+ await runDraft(configWithToken("acct-d3"), { ...BASE, attachments: [big] });
85
+ assert.ok(mock.calls.some((c) => /\/createUploadSession$/.test(c.url)), "createUploadSession invoked");
86
+ assert.ok(mock.calls.some((c) => c.method === "PUT" && c.url.startsWith(UPLOAD_URL)), "chunk PUT issued");
87
+ }
88
+ finally {
89
+ mock.restore();
90
+ delete process.env.ACCOUNT_DIR;
91
+ }
92
+ });
93
+ test("outlook-draft: an oversize attachment is refused before any Graph call — no orphan draft", async () => {
94
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d4-"));
95
+ process.env.ACCOUNT_DIR = dir;
96
+ const huge = join(dir, "huge.bin");
97
+ writeFileSync(huge, Buffer.alloc(OUTLOOK_ATTACHMENT_MAX_BYTES + 1));
98
+ const mock = mockRouted();
99
+ try {
100
+ await assert.rejects(runDraft(configWithToken("acct-d4"), { ...BASE, attachments: [huge] }), /25 MB/);
101
+ assert.equal(mock.calls.length, 0, "zero Graph calls — nothing to orphan");
102
+ }
103
+ finally {
104
+ mock.restore();
105
+ delete process.env.ACCOUNT_DIR;
106
+ }
107
+ });
108
+ test("outlook-draft: a bad path is refused before any Graph call — no orphan draft", async () => {
109
+ const dir = mkdtempSync(join(tmpdir(), "outlook-acct-d5-"));
110
+ process.env.ACCOUNT_DIR = dir;
111
+ const mock = mockRouted();
112
+ try {
113
+ await assert.rejects(runDraft(configWithToken("acct-d5"), { ...BASE, attachments: [join(dir, "ghost.pdf")] }), /Attachment not found/);
114
+ assert.equal(mock.calls.length, 0, "zero Graph calls — nothing to orphan");
115
+ }
116
+ finally {
117
+ mock.restore();
118
+ delete process.env.ACCOUNT_DIR;
119
+ }
120
+ });
121
+ //# sourceMappingURL=draft-attachments.test.js.map