@jbctechsolutions/mcp-office365 2.5.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/README.md +66 -57
  2. package/dist/approval/hash.d.ts +8 -8
  3. package/dist/approval/token-manager.d.ts +37 -19
  4. package/dist/approval/token-manager.d.ts.map +1 -1
  5. package/dist/approval/token-manager.js +133 -37
  6. package/dist/approval/token-manager.js.map +1 -1
  7. package/dist/approval/types.d.ts +7 -3
  8. package/dist/approval/types.d.ts.map +1 -1
  9. package/dist/cli.d.ts +21 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +100 -2
  12. package/dist/cli.js.map +1 -1
  13. package/dist/database/repository.d.ts +48 -48
  14. package/dist/database/repository.d.ts.map +1 -1
  15. package/dist/database/repository.js.map +1 -1
  16. package/dist/delta/mirror.d.ts +67 -0
  17. package/dist/delta/mirror.d.ts.map +1 -0
  18. package/dist/delta/mirror.js +183 -0
  19. package/dist/delta/mirror.js.map +1 -0
  20. package/dist/graph/auth/account-id.d.ts +19 -0
  21. package/dist/graph/auth/account-id.d.ts.map +1 -0
  22. package/dist/graph/auth/account-id.js +55 -0
  23. package/dist/graph/auth/account-id.js.map +1 -0
  24. package/dist/graph/auth/config.d.ts +1 -1
  25. package/dist/graph/auth/config.d.ts.map +1 -1
  26. package/dist/graph/auth/config.js +13 -1
  27. package/dist/graph/auth/config.js.map +1 -1
  28. package/dist/graph/auth/device-code-flow.d.ts +3 -1
  29. package/dist/graph/auth/device-code-flow.d.ts.map +1 -1
  30. package/dist/graph/auth/device-code-flow.js +42 -14
  31. package/dist/graph/auth/device-code-flow.js.map +1 -1
  32. package/dist/graph/auth/index.d.ts +1 -0
  33. package/dist/graph/auth/index.d.ts.map +1 -1
  34. package/dist/graph/auth/index.js +1 -0
  35. package/dist/graph/auth/index.js.map +1 -1
  36. package/dist/graph/auth/token-cache.js +2 -2
  37. package/dist/graph/auth/token-cache.js.map +1 -1
  38. package/dist/graph/client/graph-client.d.ts +141 -8
  39. package/dist/graph/client/graph-client.d.ts.map +1 -1
  40. package/dist/graph/client/graph-client.js +536 -48
  41. package/dist/graph/client/graph-client.js.map +1 -1
  42. package/dist/graph/client/immutable-id-middleware.d.ts +50 -0
  43. package/dist/graph/client/immutable-id-middleware.d.ts.map +1 -0
  44. package/dist/graph/client/immutable-id-middleware.js +137 -0
  45. package/dist/graph/client/immutable-id-middleware.js.map +1 -0
  46. package/dist/graph/content-readers.d.ts +0 -12
  47. package/dist/graph/content-readers.d.ts.map +1 -1
  48. package/dist/graph/content-readers.js +0 -21
  49. package/dist/graph/content-readers.js.map +1 -1
  50. package/dist/graph/index.d.ts +5 -6
  51. package/dist/graph/index.d.ts.map +1 -1
  52. package/dist/graph/index.js +5 -6
  53. package/dist/graph/index.js.map +1 -1
  54. package/dist/graph/mailbox-adapter.d.ts +15 -15
  55. package/dist/graph/mappers/contact-mapper.d.ts +4 -1
  56. package/dist/graph/mappers/contact-mapper.d.ts.map +1 -1
  57. package/dist/graph/mappers/contact-mapper.js +7 -3
  58. package/dist/graph/mappers/contact-mapper.js.map +1 -1
  59. package/dist/graph/mappers/email-mapper.d.ts.map +1 -1
  60. package/dist/graph/mappers/email-mapper.js +7 -4
  61. package/dist/graph/mappers/email-mapper.js.map +1 -1
  62. package/dist/graph/mappers/event-mapper.d.ts.map +1 -1
  63. package/dist/graph/mappers/event-mapper.js +6 -3
  64. package/dist/graph/mappers/event-mapper.js.map +1 -1
  65. package/dist/graph/mappers/folder-mapper.d.ts +0 -4
  66. package/dist/graph/mappers/folder-mapper.d.ts.map +1 -1
  67. package/dist/graph/mappers/folder-mapper.js +9 -19
  68. package/dist/graph/mappers/folder-mapper.js.map +1 -1
  69. package/dist/graph/mappers/index.d.ts +2 -2
  70. package/dist/graph/mappers/index.d.ts.map +1 -1
  71. package/dist/graph/mappers/index.js +2 -2
  72. package/dist/graph/mappers/index.js.map +1 -1
  73. package/dist/graph/mappers/task-mapper.d.ts +4 -2
  74. package/dist/graph/mappers/task-mapper.d.ts.map +1 -1
  75. package/dist/graph/mappers/task-mapper.js +7 -5
  76. package/dist/graph/mappers/task-mapper.js.map +1 -1
  77. package/dist/graph/mappers/utils.d.ts +0 -10
  78. package/dist/graph/mappers/utils.d.ts.map +1 -1
  79. package/dist/graph/mappers/utils.js +0 -19
  80. package/dist/graph/mappers/utils.js.map +1 -1
  81. package/dist/graph/repository.d.ts +409 -276
  82. package/dist/graph/repository.d.ts.map +1 -1
  83. package/dist/graph/repository.js +772 -1082
  84. package/dist/graph/repository.js.map +1 -1
  85. package/dist/ids/mint.d.ts +31 -0
  86. package/dist/ids/mint.d.ts.map +1 -0
  87. package/dist/ids/mint.js +36 -0
  88. package/dist/ids/mint.js.map +1 -0
  89. package/dist/ids/next-action.d.ts +29 -0
  90. package/dist/ids/next-action.d.ts.map +1 -0
  91. package/dist/ids/next-action.js +58 -0
  92. package/dist/ids/next-action.js.map +1 -0
  93. package/dist/ids/resolver.d.ts +31 -0
  94. package/dist/ids/resolver.d.ts.map +1 -0
  95. package/dist/ids/resolver.js +52 -0
  96. package/dist/ids/resolver.js.map +1 -0
  97. package/dist/ids/schema.d.ts +91 -0
  98. package/dist/ids/schema.d.ts.map +1 -0
  99. package/dist/ids/schema.js +131 -0
  100. package/dist/ids/schema.js.map +1 -0
  101. package/dist/ids/token.d.ts +62 -0
  102. package/dist/ids/token.d.ts.map +1 -0
  103. package/dist/ids/token.js +225 -0
  104. package/dist/ids/token.js.map +1 -0
  105. package/dist/index.d.ts +10 -6
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +255 -5925
  108. package/dist/index.js.map +1 -1
  109. package/dist/parsers/index.d.ts +0 -1
  110. package/dist/parsers/index.d.ts.map +1 -1
  111. package/dist/parsers/index.js +0 -1
  112. package/dist/parsers/index.js.map +1 -1
  113. package/dist/registry/all-tools.d.ts +14 -0
  114. package/dist/registry/all-tools.d.ts.map +1 -0
  115. package/dist/registry/all-tools.js +68 -0
  116. package/dist/registry/all-tools.js.map +1 -0
  117. package/dist/registry/context.d.ts +19 -0
  118. package/dist/registry/context.d.ts.map +1 -0
  119. package/dist/registry/context.js +20 -0
  120. package/dist/registry/context.js.map +1 -0
  121. package/dist/registry/define-tool.d.ts +15 -0
  122. package/dist/registry/define-tool.d.ts.map +1 -0
  123. package/dist/registry/define-tool.js +8 -0
  124. package/dist/registry/define-tool.js.map +1 -0
  125. package/dist/registry/elicit-links.d.ts +35 -0
  126. package/dist/registry/elicit-links.d.ts.map +1 -0
  127. package/dist/registry/elicit-links.js +102 -0
  128. package/dist/registry/elicit-links.js.map +1 -0
  129. package/dist/registry/elicitor.d.ts +24 -0
  130. package/dist/registry/elicitor.d.ts.map +1 -0
  131. package/dist/registry/elicitor.js +51 -0
  132. package/dist/registry/elicitor.js.map +1 -0
  133. package/dist/registry/index.d.ts +13 -0
  134. package/dist/registry/index.d.ts.map +1 -0
  135. package/dist/registry/index.js +8 -0
  136. package/dist/registry/index.js.map +1 -0
  137. package/dist/registry/registry.d.ts +73 -0
  138. package/dist/registry/registry.d.ts.map +1 -0
  139. package/dist/registry/registry.js +221 -0
  140. package/dist/registry/registry.js.map +1 -0
  141. package/dist/registry/types.d.ts +146 -0
  142. package/dist/registry/types.d.ts.map +1 -0
  143. package/dist/registry/types.js +6 -0
  144. package/dist/registry/types.js.map +1 -0
  145. package/dist/search/compiler.d.ts +49 -0
  146. package/dist/search/compiler.d.ts.map +1 -0
  147. package/dist/search/compiler.js +168 -0
  148. package/dist/search/compiler.js.map +1 -0
  149. package/dist/signature.js +2 -2
  150. package/dist/signature.js.map +1 -1
  151. package/dist/state/delta-store.d.ts +85 -0
  152. package/dist/state/delta-store.d.ts.map +1 -0
  153. package/dist/state/delta-store.js +124 -0
  154. package/dist/state/delta-store.js.map +1 -0
  155. package/dist/state/migrate.d.ts +23 -0
  156. package/dist/state/migrate.d.ts.map +1 -0
  157. package/dist/state/migrate.js +81 -0
  158. package/dist/state/migrate.js.map +1 -0
  159. package/dist/state/schema.d.ts +26 -0
  160. package/dist/state/schema.d.ts.map +1 -0
  161. package/dist/state/schema.js +77 -0
  162. package/dist/state/schema.js.map +1 -0
  163. package/dist/state/store.d.ts +155 -0
  164. package/dist/state/store.d.ts.map +1 -0
  165. package/dist/state/store.js +321 -0
  166. package/dist/state/store.js.map +1 -0
  167. package/dist/tools/calendar-graph.d.ts +41 -0
  168. package/dist/tools/calendar-graph.d.ts.map +1 -0
  169. package/dist/tools/calendar-graph.js +299 -0
  170. package/dist/tools/calendar-graph.js.map +1 -0
  171. package/dist/tools/calendar-permissions.d.ts +17 -7
  172. package/dist/tools/calendar-permissions.d.ts.map +1 -1
  173. package/dist/tools/calendar-permissions.js +60 -4
  174. package/dist/tools/calendar-permissions.js.map +1 -1
  175. package/dist/tools/calendar.d.ts +133 -82
  176. package/dist/tools/calendar.d.ts.map +1 -1
  177. package/dist/tools/calendar.js +249 -136
  178. package/dist/tools/calendar.js.map +1 -1
  179. package/dist/tools/categories.d.ts +14 -4
  180. package/dist/tools/categories.d.ts.map +1 -1
  181. package/dist/tools/categories.js +59 -2
  182. package/dist/tools/categories.js.map +1 -1
  183. package/dist/tools/checklist-items.d.ts +19 -9
  184. package/dist/tools/checklist-items.d.ts.map +1 -1
  185. package/dist/tools/checklist-items.js +71 -5
  186. package/dist/tools/checklist-items.js.map +1 -1
  187. package/dist/tools/contact-folders.d.ts +50 -0
  188. package/dist/tools/contact-folders.d.ts.map +1 -0
  189. package/dist/tools/contact-folders.js +136 -0
  190. package/dist/tools/contact-folders.js.map +1 -0
  191. package/dist/tools/contacts-graph.d.ts +45 -0
  192. package/dist/tools/contacts-graph.d.ts.map +1 -0
  193. package/dist/tools/contacts-graph.js +174 -0
  194. package/dist/tools/contacts-graph.js.map +1 -0
  195. package/dist/tools/contacts.d.ts +61 -30
  196. package/dist/tools/contacts.d.ts.map +1 -1
  197. package/dist/tools/contacts.js +150 -93
  198. package/dist/tools/contacts.js.map +1 -1
  199. package/dist/tools/excel.d.ts +20 -10
  200. package/dist/tools/excel.d.ts.map +1 -1
  201. package/dist/tools/excel.js +82 -6
  202. package/dist/tools/excel.js.map +1 -1
  203. package/dist/tools/focused-overrides.d.ts +14 -4
  204. package/dist/tools/focused-overrides.d.ts.map +1 -1
  205. package/dist/tools/focused-overrides.js +59 -2
  206. package/dist/tools/focused-overrides.js.map +1 -1
  207. package/dist/tools/index.d.ts +4 -5
  208. package/dist/tools/index.d.ts.map +1 -1
  209. package/dist/tools/index.js +4 -6
  210. package/dist/tools/index.js.map +1 -1
  211. package/dist/tools/linked-resources.d.ts +17 -7
  212. package/dist/tools/linked-resources.d.ts.map +1 -1
  213. package/dist/tools/linked-resources.js +60 -4
  214. package/dist/tools/linked-resources.js.map +1 -1
  215. package/dist/tools/mail-graph.d.ts +38 -0
  216. package/dist/tools/mail-graph.d.ts.map +1 -0
  217. package/dist/tools/mail-graph.js +187 -0
  218. package/dist/tools/mail-graph.js.map +1 -0
  219. package/dist/tools/mail-rules.d.ts +20 -8
  220. package/dist/tools/mail-rules.d.ts.map +1 -1
  221. package/dist/tools/mail-rules.js +63 -7
  222. package/dist/tools/mail-rules.js.map +1 -1
  223. package/dist/tools/mail-send.d.ts +39 -27
  224. package/dist/tools/mail-send.d.ts.map +1 -1
  225. package/dist/tools/mail-send.js +249 -24
  226. package/dist/tools/mail-send.js.map +1 -1
  227. package/dist/tools/mail.d.ts +91 -68
  228. package/dist/tools/mail.d.ts.map +1 -1
  229. package/dist/tools/mail.js +232 -243
  230. package/dist/tools/mail.js.map +1 -1
  231. package/dist/tools/mailbox-organization.d.ts +51 -34
  232. package/dist/tools/mailbox-organization.d.ts.map +1 -1
  233. package/dist/tools/mailbox-organization.js +318 -35
  234. package/dist/tools/mailbox-organization.js.map +1 -1
  235. package/dist/tools/mailbox-settings.d.ts +59 -0
  236. package/dist/tools/mailbox-settings.d.ts.map +1 -0
  237. package/dist/tools/mailbox-settings.js +136 -0
  238. package/dist/tools/mailbox-settings.js.map +1 -0
  239. package/dist/tools/meetings.d.ts +24 -14
  240. package/dist/tools/meetings.d.ts.map +1 -1
  241. package/dist/tools/meetings.js +82 -6
  242. package/dist/tools/meetings.js.map +1 -1
  243. package/dist/tools/onedrive.d.ts +26 -16
  244. package/dist/tools/onedrive.d.ts.map +1 -1
  245. package/dist/tools/onedrive.js +140 -11
  246. package/dist/tools/onedrive.js.map +1 -1
  247. package/dist/tools/onenote.d.ts +63 -0
  248. package/dist/tools/onenote.d.ts.map +1 -0
  249. package/dist/tools/onenote.js +221 -0
  250. package/dist/tools/onenote.js.map +1 -0
  251. package/dist/tools/people.d.ts +10 -0
  252. package/dist/tools/people.d.ts.map +1 -1
  253. package/dist/tools/people.js +94 -0
  254. package/dist/tools/people.js.map +1 -1
  255. package/dist/tools/planner-visualization.d.ts +15 -5
  256. package/dist/tools/planner-visualization.d.ts.map +1 -1
  257. package/dist/tools/planner-visualization.js +58 -4
  258. package/dist/tools/planner-visualization.js.map +1 -1
  259. package/dist/tools/planner.d.ts +69 -39
  260. package/dist/tools/planner.d.ts.map +1 -1
  261. package/dist/tools/planner.js +231 -23
  262. package/dist/tools/planner.js.map +1 -1
  263. package/dist/tools/scheduling.d.ts +12 -0
  264. package/dist/tools/scheduling.d.ts.map +1 -1
  265. package/dist/tools/scheduling.js +38 -0
  266. package/dist/tools/scheduling.js.map +1 -1
  267. package/dist/tools/shared-mailbox.d.ts +164 -0
  268. package/dist/tools/shared-mailbox.d.ts.map +1 -0
  269. package/dist/tools/shared-mailbox.js +318 -0
  270. package/dist/tools/shared-mailbox.js.map +1 -0
  271. package/dist/tools/sharepoint-lists.d.ts +131 -0
  272. package/dist/tools/sharepoint-lists.d.ts.map +1 -0
  273. package/dist/tools/sharepoint-lists.js +246 -0
  274. package/dist/tools/sharepoint-lists.js.map +1 -0
  275. package/dist/tools/sharepoint.d.ts +24 -14
  276. package/dist/tools/sharepoint.d.ts.map +1 -1
  277. package/dist/tools/sharepoint.js +85 -9
  278. package/dist/tools/sharepoint.js.map +1 -1
  279. package/dist/tools/task-attachments.d.ts +17 -7
  280. package/dist/tools/task-attachments.d.ts.map +1 -1
  281. package/dist/tools/task-attachments.js +60 -4
  282. package/dist/tools/task-attachments.js.map +1 -1
  283. package/dist/tools/task-lists.d.ts +56 -0
  284. package/dist/tools/task-lists.d.ts.map +1 -0
  285. package/dist/tools/task-lists.js +155 -0
  286. package/dist/tools/task-lists.js.map +1 -0
  287. package/dist/tools/tasks-graph.d.ts +49 -0
  288. package/dist/tools/tasks-graph.d.ts.map +1 -0
  289. package/dist/tools/tasks-graph.js +193 -0
  290. package/dist/tools/tasks-graph.js.map +1 -0
  291. package/dist/tools/tasks.d.ts +121 -29
  292. package/dist/tools/tasks.d.ts.map +1 -1
  293. package/dist/tools/tasks.js +136 -84
  294. package/dist/tools/tasks.js.map +1 -1
  295. package/dist/tools/teams.d.ts +57 -46
  296. package/dist/tools/teams.d.ts.map +1 -1
  297. package/dist/tools/teams.js +288 -26
  298. package/dist/tools/teams.js.map +1 -1
  299. package/dist/tools/what-changed.d.ts +48 -0
  300. package/dist/tools/what-changed.d.ts.map +1 -0
  301. package/dist/tools/what-changed.js +149 -0
  302. package/dist/tools/what-changed.js.map +1 -0
  303. package/dist/types/calendar.d.ts +1 -1
  304. package/dist/types/calendar.d.ts.map +1 -1
  305. package/dist/types/calendar.js.map +1 -1
  306. package/dist/types/contacts.d.ts +1 -1
  307. package/dist/types/contacts.d.ts.map +1 -1
  308. package/dist/types/contacts.js.map +1 -1
  309. package/dist/types/mail.d.ts +1 -1
  310. package/dist/types/mail.d.ts.map +1 -1
  311. package/dist/types/mail.js.map +1 -1
  312. package/dist/utils/errors.d.ts +110 -38
  313. package/dist/utils/errors.d.ts.map +1 -1
  314. package/dist/utils/errors.js +239 -56
  315. package/dist/utils/errors.js.map +1 -1
  316. package/dist/visualization/types.d.ts +4 -4
  317. package/package.json +2 -3
  318. package/dist/applescript/account-repository.d.ts +0 -30
  319. package/dist/applescript/account-repository.d.ts.map +0 -1
  320. package/dist/applescript/account-repository.js +0 -38
  321. package/dist/applescript/account-repository.js.map +0 -1
  322. package/dist/applescript/account-scripts.d.ts +0 -21
  323. package/dist/applescript/account-scripts.d.ts.map +0 -1
  324. package/dist/applescript/account-scripts.js +0 -180
  325. package/dist/applescript/account-scripts.js.map +0 -1
  326. package/dist/applescript/calendar-manager.d.ts +0 -44
  327. package/dist/applescript/calendar-manager.d.ts.map +0 -1
  328. package/dist/applescript/calendar-manager.js +0 -92
  329. package/dist/applescript/calendar-manager.js.map +0 -1
  330. package/dist/applescript/calendar-writer.d.ts +0 -36
  331. package/dist/applescript/calendar-writer.d.ts.map +0 -1
  332. package/dist/applescript/calendar-writer.js +0 -94
  333. package/dist/applescript/calendar-writer.js.map +0 -1
  334. package/dist/applescript/content-readers.d.ts +0 -114
  335. package/dist/applescript/content-readers.d.ts.map +0 -1
  336. package/dist/applescript/content-readers.js +0 -328
  337. package/dist/applescript/content-readers.js.map +0 -1
  338. package/dist/applescript/executor.d.ts +0 -60
  339. package/dist/applescript/executor.d.ts.map +0 -1
  340. package/dist/applescript/executor.js +0 -173
  341. package/dist/applescript/executor.js.map +0 -1
  342. package/dist/applescript/index.d.ts +0 -20
  343. package/dist/applescript/index.d.ts.map +0 -1
  344. package/dist/applescript/index.js +0 -29
  345. package/dist/applescript/index.js.map +0 -1
  346. package/dist/applescript/mail-sender.d.ts +0 -38
  347. package/dist/applescript/mail-sender.d.ts.map +0 -1
  348. package/dist/applescript/mail-sender.js +0 -67
  349. package/dist/applescript/mail-sender.js.map +0 -1
  350. package/dist/applescript/parser.d.ts +0 -235
  351. package/dist/applescript/parser.d.ts.map +0 -1
  352. package/dist/applescript/parser.js +0 -496
  353. package/dist/applescript/parser.js.map +0 -1
  354. package/dist/applescript/repository.d.ts +0 -64
  355. package/dist/applescript/repository.d.ts.map +0 -1
  356. package/dist/applescript/repository.js +0 -444
  357. package/dist/applescript/repository.js.map +0 -1
  358. package/dist/applescript/scripts.d.ts +0 -265
  359. package/dist/applescript/scripts.d.ts.map +0 -1
  360. package/dist/applescript/scripts.js +0 -1483
  361. package/dist/applescript/scripts.js.map +0 -1
  362. package/dist/parsers/olk15.d.ts +0 -87
  363. package/dist/parsers/olk15.d.ts.map +0 -1
  364. package/dist/parsers/olk15.js +0 -368
  365. package/dist/parsers/olk15.js.map +0 -1
  366. package/dist/tools/notes.d.ts +0 -74
  367. package/dist/tools/notes.d.ts.map +0 -1
  368. package/dist/tools/notes.js +0 -136
  369. package/dist/tools/notes.js.map +0 -1
@@ -3,10 +3,16 @@
3
3
  * Licensed under the MIT License. See LICENSE file in the project root.
4
4
  */
5
5
  import { GraphClient } from './client/index.js';
6
- import { mapMailFolderToRow, mapCalendarToFolderRow, mapMessageToEmailRow, mapEventToEventRow, mapContactToContactRow, mapTaskToTaskRow, hashStringToNumber, } from './mappers/index.js';
6
+ import { mapMailFolderToRow, mapCalendarToFolderRow, mapMessageToEmailRow, mapEventToEventRow, mapContactToContactRow, mapTaskToTaskRow, } from './mappers/index.js';
7
+ import { currentAccountId } from './auth/index.js';
8
+ import { resolveId } from '../ids/resolver.js';
9
+ import { registerComposite } from '../ids/mint.js';
10
+ import { mintSelfEncoded } from '../ids/token.js';
11
+ import { IdUnknownError, isGraphSdkError } from '../utils/errors.js';
7
12
  import { downloadAttachment, getDownloadDir } from './attachments.js';
8
13
  import * as fs from 'fs';
9
14
  import * as path from 'path';
15
+ import { createHash } from 'node:crypto';
10
16
  /**
11
17
  * Repository implementation using Microsoft Graph API.
12
18
  *
@@ -14,45 +20,179 @@ import * as path from 'path';
14
20
  */
15
21
  export class GraphRepository {
16
22
  client;
23
+ store;
24
+ accountId;
17
25
  deltaLinks = new Map();
18
- idCache = {
19
- folders: new Map(),
20
- messages: new Map(),
21
- conversations: new Map(),
22
- events: new Map(),
23
- contacts: new Map(),
24
- tasks: new Map(),
25
- taskLists: new Map(),
26
- attachments: new Map(),
27
- rules: new Map(),
28
- contactFolders: new Map(),
29
- categories: new Map(),
30
- focusedOverrides: new Map(),
31
- calendarGroups: new Map(),
32
- calendars: new Map(),
33
- calendarPermissions: new Map(),
34
- teams: new Map(),
35
- channels: new Map(),
36
- channelMessages: new Map(),
37
- chats: new Map(),
38
- chatMessages: new Map(),
39
- checklistItems: new Map(),
40
- linkedResources: new Map(),
41
- taskAttachments: new Map(),
42
- plans: new Map(),
43
- plannerBuckets: new Map(),
44
- plannerTasks: new Map(),
45
- plannerTaskDetails: new Map(),
46
- onlineMeetings: new Map(),
47
- recordings: new Map(),
48
- transcripts: new Map(),
49
- driveItems: new Map(),
50
- sites: new Map(),
51
- documentLibraries: new Map(),
52
- libraryDriveItems: new Map(),
53
- };
54
- constructor(deviceCodeCallback) {
26
+ constructor(deviceCodeCallback, store, accountId = currentAccountId) {
55
27
  this.client = new GraphClient(deviceCodeCallback);
28
+ this.store = store;
29
+ this.accountId = accountId;
30
+ }
31
+ /**
32
+ * Resolves any id a tool receives — a durable token, a raw Graph id, or a
33
+ * legacy numeric id — to a live Graph id (U5). Self-encoding tokens decode with
34
+ * no store; alias-backed tokens need {@link store}; a numeric id on Graph is
35
+ * unsupported (D4). Throws a typed error on failure.
36
+ */
37
+ toGraphId(id, expectedEntityType) {
38
+ return resolveId(id, this.accountId(), this.store, expectedEntityType).graphId;
39
+ }
40
+ /**
41
+ * Mints an alias-backed token for a single-Graph-id entity (e.g. team, chat)
42
+ * and records token → graphId in the alias table so it resolves on later calls.
43
+ * Alias-backed (not self-encoding) so the token is short and account-scoped: a
44
+ * cold store yields ID_UNKNOWN rather than leaking a decodable tenant-global id.
45
+ */
46
+ mintAlias(entityType, graphId) {
47
+ if (this.store == null) {
48
+ // Production always has a store (index.ts) and degraded mode still yields a
49
+ // non-null in-memory one, so this is only reachable from a store-less
50
+ // embedding. Fail loudly rather than mint a token nothing can resolve.
51
+ throw new IdUnknownError(`${entityType} (durable state store unavailable)`, 'Alias-backed ids require the durable state store; construct the repository with one.');
52
+ }
53
+ return registerComposite(this.store, {
54
+ entityType,
55
+ parts: { id: graphId },
56
+ graphId,
57
+ accountId: this.accountId(),
58
+ });
59
+ }
60
+ /**
61
+ * Mints an alias-backed token for a composite entity whose Graph URL needs a
62
+ * tuple of ids (e.g. channel {teamId, channelId}). The tuple is the canonical
63
+ * key AND is stored JSON-encoded as the resolved value, so {@link toGraphParts}
64
+ * can recover every field.
65
+ */
66
+ mintAliasComposite(entityType, parts) {
67
+ if (this.store == null) {
68
+ throw new IdUnknownError(`<no store: cannot mint ${entityType}>`);
69
+ }
70
+ return registerComposite(this.store, {
71
+ entityType,
72
+ parts,
73
+ graphId: JSON.stringify(parts),
74
+ accountId: this.accountId(),
75
+ });
76
+ }
77
+ /**
78
+ * Resolves a composite token to its identifying tuple. The alias row stores the
79
+ * tuple JSON-encoded; a raw (non-token) string can't carry a tuple, so anything
80
+ * that doesn't decode to a JSON object carrying every required key is an unusable
81
+ * id (ID_UNKNOWN). Generic over the key set so callers get a precisely-typed
82
+ * result — every requested field is a guaranteed non-empty string.
83
+ */
84
+ toGraphParts(id, entityType, keys) {
85
+ const raw = this.toGraphId(id, entityType);
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(raw);
89
+ }
90
+ catch {
91
+ throw new IdUnknownError(String(id));
92
+ }
93
+ if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed)) {
94
+ throw new IdUnknownError(String(id));
95
+ }
96
+ const obj = parsed;
97
+ const out = {};
98
+ for (const key of keys) {
99
+ const value = obj[key];
100
+ if (typeof value !== 'string' || value.length === 0) {
101
+ throw new IdUnknownError(String(id));
102
+ }
103
+ out[key] = value;
104
+ }
105
+ return out;
106
+ }
107
+ // ===========================================================================
108
+ // Cache Resolvers (auto-fetch parent on cache miss)
109
+ // ===========================================================================
110
+ async resolveTeamId(teamId) {
111
+ // tm_ tokens resolve from the alias store. On a cold miss (never listed this
112
+ // session, or a lost store) re-list — listTeamsAsync deterministically
113
+ // re-mints and re-stores the same token — then resolve again.
114
+ try {
115
+ return this.toGraphId(teamId, 'team');
116
+ }
117
+ catch (e) {
118
+ if (e instanceof IdUnknownError) {
119
+ await this.listTeamsAsync();
120
+ return this.toGraphId(teamId, 'team');
121
+ }
122
+ throw e;
123
+ }
124
+ }
125
+ async resolvePlanId(planId) {
126
+ // pl_ tokens resolve from the alias store. On a cold miss (never listed this
127
+ // session, or a lost store) re-list — listPlansAsync deterministically
128
+ // re-mints and re-stores the same token — then resolve again.
129
+ try {
130
+ return this.toGraphId(planId, 'plan');
131
+ }
132
+ catch (e) {
133
+ if (e instanceof IdUnknownError) {
134
+ await this.listPlansAsync();
135
+ return this.toGraphId(planId, 'plan');
136
+ }
137
+ throw e;
138
+ }
139
+ }
140
+ /** True for an HTTP 412 (Precondition Failed) — the `If-Match` etag we sent no
141
+ * longer matches the entity's current etag. */
142
+ isPreconditionFailed(e) {
143
+ return isGraphSdkError(e) && e.statusCode === 412;
144
+ }
145
+ /** Extracts the OData etag from a fetched entity, defaulting to `''` when absent. */
146
+ extractEtag(entity) {
147
+ return entity['@odata.etag'] ?? '';
148
+ }
149
+ /**
150
+ * Fetches a fresh etag immediately before a write (U5b-5 — the Planner etag is
151
+ * mutable and per-sub-resource, so it can't ride in the durable token or a
152
+ * cache) and retries the write once with a re-fetched etag on a 412, covering
153
+ * the narrow race between the fetch and the write.
154
+ *
155
+ * CONCURRENCY SEMANTIC (deliberate, last-writer-wins): because MCP tool calls
156
+ * are stateless, the etag the caller observed at an earlier `get_*` cannot be
157
+ * carried into a later `update_*`. We therefore re-read the etag at write time,
158
+ * which means a concurrent edit landing between the caller's read and their
159
+ * write is NOT detected — the write overwrites it. This is the intended
160
+ * trade-off (it eliminates the spurious 412s the old cached-etag path produced
161
+ * for a lone editor); Planner writes are not cross-read conflict-protected.
162
+ */
163
+ async withFreshEtag(fetchEtag, write) {
164
+ // An empty If-Match is never a valid write intent — fail loudly rather than
165
+ // send `If-Match: ''` (which would 400/412 confusingly, or on a lenient
166
+ // endpoint become an unconditional overwrite).
167
+ const fetch = async () => {
168
+ const etag = await fetchEtag();
169
+ if (etag.length === 0) {
170
+ throw new Error('Cannot perform a conditional write: the entity returned no @odata.etag.');
171
+ }
172
+ return etag;
173
+ };
174
+ const etag = await fetch();
175
+ try {
176
+ return await write(etag);
177
+ }
178
+ catch (e) {
179
+ if (this.isPreconditionFailed(e)) {
180
+ return await write(await fetch());
181
+ }
182
+ throw e;
183
+ }
184
+ }
185
+ async resolveChatId(chatId) {
186
+ try {
187
+ return this.toGraphId(chatId, 'chat');
188
+ }
189
+ catch (e) {
190
+ if (e instanceof IdUnknownError) {
191
+ await this.listChatsAsync();
192
+ return this.toGraphId(chatId, 'chat');
193
+ }
194
+ throw e;
195
+ }
56
196
  }
57
197
  // ===========================================================================
58
198
  // Folders
@@ -64,31 +204,15 @@ export class GraphRepository {
64
204
  throw new Error('Use listFoldersAsync() for Graph repository');
65
205
  }
66
206
  async listFoldersAsync() {
207
+ // Rows carry self-encoding fd_ tokens (the mapper mints them) — no cache.
67
208
  const folders = await this.client.listMailFolders();
68
- // Update ID cache
69
- for (const folder of folders) {
70
- if (folder.id != null) {
71
- const numericId = hashStringToNumber(folder.id);
72
- this.idCache.folders.set(numericId, folder.id);
73
- }
74
- }
75
209
  return folders.map(mapMailFolderToRow);
76
210
  }
77
211
  getFolder(_id) {
78
212
  throw new Error('Use getFolderAsync() for Graph repository');
79
213
  }
80
214
  async getFolderAsync(id) {
81
- const graphId = this.idCache.folders.get(id);
82
- if (graphId == null) {
83
- // Try to find it by listing all folders
84
- await this.listFoldersAsync();
85
- const refreshedGraphId = this.idCache.folders.get(id);
86
- if (refreshedGraphId == null) {
87
- return undefined;
88
- }
89
- const folder = await this.client.getMailFolder(refreshedGraphId);
90
- return folder != null ? mapMailFolderToRow(folder) : undefined;
91
- }
215
+ const graphId = this.toGraphId(id, 'folder');
92
216
  const folder = await this.client.getMailFolder(graphId);
93
217
  return folder != null ? mapMailFolderToRow(folder) : undefined;
94
218
  }
@@ -99,58 +223,23 @@ export class GraphRepository {
99
223
  throw new Error('Use listEmailsAsync() for Graph repository');
100
224
  }
101
225
  async listEmailsAsync(folderId, limit, offset) {
102
- const graphFolderId = this.idCache.folders.get(folderId);
103
- if (graphFolderId == null) {
104
- // Refresh folder cache
105
- await this.listFoldersAsync();
106
- const refreshedId = this.idCache.folders.get(folderId);
107
- if (refreshedId == null) {
108
- return [];
109
- }
110
- return this.listEmailsWithGraphId(refreshedId, limit, offset);
111
- }
226
+ const graphFolderId = this.toGraphId(folderId, 'folder');
112
227
  return this.listEmailsWithGraphId(graphFolderId, limit, offset);
113
228
  }
114
229
  async listEmailsWithGraphId(folderId, limit, offset) {
230
+ // Rows carry self-encoding em_ tokens (the mapper mints them) — no cache.
115
231
  const messages = await this.client.listMessages(folderId, limit, offset);
116
- // Update ID cache
117
- for (const message of messages) {
118
- if (message.id != null) {
119
- const numericId = hashStringToNumber(message.id);
120
- this.idCache.messages.set(numericId, message.id);
121
- }
122
- if (message.conversationId != null) {
123
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
124
- }
125
- }
126
232
  return messages.map((m) => mapMessageToEmailRow(m, folderId));
127
233
  }
128
234
  listUnreadEmails(_folderId, _limit, _offset) {
129
235
  throw new Error('Use listUnreadEmailsAsync() for Graph repository');
130
236
  }
131
237
  async listUnreadEmailsAsync(folderId, limit, offset) {
132
- const graphFolderId = this.idCache.folders.get(folderId);
133
- if (graphFolderId == null) {
134
- await this.listFoldersAsync();
135
- const refreshedId = this.idCache.folders.get(folderId);
136
- if (refreshedId == null) {
137
- return [];
138
- }
139
- return this.listUnreadEmailsWithGraphId(refreshedId, limit, offset);
140
- }
238
+ const graphFolderId = this.toGraphId(folderId, 'folder');
141
239
  return this.listUnreadEmailsWithGraphId(graphFolderId, limit, offset);
142
240
  }
143
241
  async listUnreadEmailsWithGraphId(folderId, limit, offset) {
144
242
  const messages = await this.client.listUnreadMessages(folderId, limit, offset);
145
- for (const message of messages) {
146
- if (message.id != null) {
147
- const numericId = hashStringToNumber(message.id);
148
- this.idCache.messages.set(numericId, message.id);
149
- }
150
- if (message.conversationId != null) {
151
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
152
- }
153
- }
154
243
  return messages.map((m) => mapMessageToEmailRow(m, folderId));
155
244
  }
156
245
  searchEmails(_query, _limit) {
@@ -158,96 +247,49 @@ export class GraphRepository {
158
247
  }
159
248
  async searchEmailsAsync(query, limit) {
160
249
  const messages = await this.client.searchMessages(query, limit);
161
- for (const message of messages) {
162
- if (message.id != null) {
163
- const numericId = hashStringToNumber(message.id);
164
- this.idCache.messages.set(numericId, message.id);
165
- }
166
- if (message.conversationId != null) {
167
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
168
- }
169
- }
170
250
  return messages.map((m) => mapMessageToEmailRow(m));
171
251
  }
172
252
  searchEmailsInFolder(_folderId, _query, _limit) {
173
253
  throw new Error('Use searchEmailsInFolderAsync() for Graph repository');
174
254
  }
175
255
  async searchEmailsInFolderAsync(folderId, query, limit) {
176
- const graphFolderId = this.idCache.folders.get(folderId);
177
- if (graphFolderId == null) {
178
- await this.listFoldersAsync();
179
- const refreshedId = this.idCache.folders.get(folderId);
180
- if (refreshedId == null) {
181
- return [];
182
- }
183
- return this.searchEmailsInFolderWithGraphId(refreshedId, query, limit);
184
- }
256
+ const graphFolderId = this.toGraphId(folderId, 'folder');
185
257
  return this.searchEmailsInFolderWithGraphId(graphFolderId, query, limit);
186
258
  }
187
259
  async searchEmailsInFolderWithGraphId(folderId, query, limit) {
188
260
  const messages = await this.client.searchMessagesInFolder(folderId, query, limit);
189
- for (const message of messages) {
190
- if (message.id != null) {
191
- const numericId = hashStringToNumber(message.id);
192
- this.idCache.messages.set(numericId, message.id);
193
- }
194
- if (message.conversationId != null) {
195
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
196
- }
197
- }
198
261
  return messages.map((m) => mapMessageToEmailRow(m, folderId));
199
262
  }
200
263
  /**
201
- * Advanced search using raw KQL query syntax.
264
+ * Structured advanced search (U7 / D9). Runs a compiled query on the correct
265
+ * Graph mechanism ($filter / quoted $search / /search/query), then caches IDs
266
+ * and maps to EmailRow[] exactly like the raw-KQL path it replaces.
202
267
  */
203
- async searchEmailsAdvancedAsync(query, limit) {
204
- const messages = await this.client.searchMessagesKql(query, limit);
205
- for (const msg of messages) {
206
- if (msg.id != null) {
207
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
208
- }
209
- if (msg.conversationId != null) {
210
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
211
- }
212
- }
268
+ async searchEmailsStructuredAsync(compiled, limit) {
269
+ const messages = await this.runStructuredSearch(compiled, limit);
213
270
  return messages.map((m) => mapMessageToEmailRow(m));
214
271
  }
215
- /**
216
- * Advanced search in a specific folder using raw KQL query syntax.
217
- */
218
- async searchEmailsAdvancedInFolderAsync(folderId, query, limit) {
219
- const graphFolderId = this.idCache.folders.get(folderId);
220
- if (graphFolderId == null) {
221
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
222
- }
223
- const messages = await this.client.searchMessagesKqlInFolder(graphFolderId, query, limit);
224
- for (const msg of messages) {
225
- if (msg.id != null) {
226
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
227
- }
228
- if (msg.conversationId != null) {
229
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
230
- }
272
+ runStructuredSearch(compiled, limit) {
273
+ switch (compiled.mechanism) {
274
+ case 'filter':
275
+ return this.client.searchMessagesFilter(compiled.filter, limit);
276
+ case 'search':
277
+ return this.client.searchMessagesSearchValue(compiled.search, limit);
278
+ case 'searchQuery':
279
+ return this.client.searchMessagesQuery(compiled.kql, limit);
231
280
  }
232
- return messages.map((m) => mapMessageToEmailRow(m));
233
281
  }
234
282
  async checkNewEmailsAsync(folderId) {
235
- const graphFolderId = this.idCache.folders.get(folderId);
236
- if (graphFolderId == null)
237
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
238
- const existingDeltaLink = this.deltaLinks.get(folderId);
283
+ const graphFolderId = this.toGraphId(folderId, 'folder');
284
+ // Key the delta cursor by the RESOLVED Graph id, not the input string: a
285
+ // folder can be addressed by its fd_ token or its raw Graph id, and both
286
+ // must share one cursor — otherwise the second form triggers a fresh initial
287
+ // sync and re-reports already-seen mail as new.
288
+ const existingDeltaLink = this.deltaLinks.get(graphFolderId);
239
289
  const isInitialSync = existingDeltaLink == null;
240
290
  const { messages, deltaLink } = await this.client.getMessagesDelta(graphFolderId, existingDeltaLink);
241
291
  if (deltaLink) {
242
- this.deltaLinks.set(folderId, deltaLink);
243
- }
244
- for (const msg of messages) {
245
- if (msg.id != null) {
246
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
247
- }
248
- if (msg.conversationId != null) {
249
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
250
- }
292
+ this.deltaLinks.set(graphFolderId, deltaLink);
251
293
  }
252
294
  const activeMessages = messages.filter((m) => m['@removed'] == null);
253
295
  return {
@@ -258,61 +300,8 @@ export class GraphRepository {
258
300
  getEmail(_id) {
259
301
  throw new Error('Use getEmailAsync() for Graph repository');
260
302
  }
261
- /**
262
- * Populates the message ID cache by listing messages from mail folders.
263
- * Used as a fallback when getEmailAsync is called with an ID not yet in cache
264
- * (e.g. after server restart or when list_emails/search_emails wasn't called first).
265
- */
266
- async refreshMessageCacheForGetEmail(targetId) {
267
- let folders;
268
- try {
269
- folders = await this.listFoldersAsync();
270
- }
271
- catch {
272
- return false;
273
- }
274
- if (folders.length === 0)
275
- return false;
276
- const MESSAGE_LIMIT_PER_FOLDER = 100;
277
- const MAX_FOLDERS_TO_SCAN = 15;
278
- for (let i = 0; i < Math.min(folders.length, MAX_FOLDERS_TO_SCAN); i++) {
279
- const folder = folders[i];
280
- const graphFolderId = this.idCache.folders.get(folder.id);
281
- if (graphFolderId == null)
282
- continue;
283
- let messages;
284
- try {
285
- messages = await this.client.listMessages(graphFolderId, MESSAGE_LIMIT_PER_FOLDER, 0);
286
- }
287
- catch {
288
- continue;
289
- }
290
- if (!Array.isArray(messages))
291
- continue;
292
- for (const message of messages) {
293
- if (message.id != null) {
294
- const numericId = hashStringToNumber(message.id);
295
- this.idCache.messages.set(numericId, message.id);
296
- if (numericId === targetId)
297
- return true;
298
- }
299
- if (message.conversationId != null) {
300
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
301
- }
302
- }
303
- }
304
- return this.idCache.messages.has(targetId);
305
- }
306
303
  async getEmailAsync(id) {
307
- let graphId = this.idCache.messages.get(id);
308
- if (graphId == null) {
309
- const found = await this.refreshMessageCacheForGetEmail(id);
310
- if (found)
311
- graphId = this.idCache.messages.get(id) ?? undefined;
312
- }
313
- if (graphId == null) {
314
- return undefined;
315
- }
304
+ const graphId = this.toGraphId(id, 'message');
316
305
  const message = await this.client.getMessage(graphId);
317
306
  return message != null ? mapMessageToEmailRow(message) : undefined;
318
307
  }
@@ -327,16 +316,7 @@ export class GraphRepository {
327
316
  throw new Error('Use getUnreadCountByFolderAsync() for Graph repository');
328
317
  }
329
318
  async getUnreadCountByFolderAsync(folderId) {
330
- const graphId = this.idCache.folders.get(folderId);
331
- if (graphId == null) {
332
- await this.listFoldersAsync();
333
- const refreshedId = this.idCache.folders.get(folderId);
334
- if (refreshedId == null) {
335
- return 0;
336
- }
337
- const folder = await this.client.getMailFolder(refreshedId);
338
- return folder?.unreadItemCount ?? 0;
339
- }
319
+ const graphId = this.toGraphId(folderId, 'folder');
340
320
  const folder = await this.client.getMailFolder(graphId);
341
321
  return folder?.unreadItemCount ?? 0;
342
322
  }
@@ -346,27 +326,19 @@ export class GraphRepository {
346
326
  /**
347
327
  * Lists all messages in a conversation thread.
348
328
  *
349
- * Looks up the message to get its conversationId, resolves the Graph string
350
- * conversationId from cache, then queries for all messages with that ID.
329
+ * Resolves the message id to its Graph id (durable `em_` token or raw Graph
330
+ * id), fetches the message to read its raw Graph conversationId, then queries
331
+ * for all messages with that ID. No cache required.
351
332
  */
352
333
  async listConversationAsync(messageId, limit) {
353
- const email = await this.getEmailAsync(messageId);
354
- if (email == null)
355
- throw new Error(`Message ID ${messageId} not found`);
356
- if (email.conversationId == null)
334
+ const graphId = this.toGraphId(messageId, 'message');
335
+ const message = await this.client.getMessage(graphId);
336
+ if (message == null)
337
+ throw new Error('Message not found');
338
+ const convId = message.conversationId;
339
+ if (convId == null)
357
340
  throw new Error('Message has no conversation ID');
358
- const graphConversationId = this.idCache.conversations.get(email.conversationId);
359
- if (graphConversationId == null)
360
- throw new Error('Conversation ID not found in cache. Try fetching the email first to populate the cache.');
361
- const messages = await this.client.listConversationMessages(graphConversationId, limit);
362
- for (const msg of messages) {
363
- if (msg.id != null) {
364
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
365
- }
366
- if (msg.conversationId != null) {
367
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
368
- }
369
- }
341
+ const messages = await this.client.listConversationMessages(convId, limit);
370
342
  return messages.map((m) => mapMessageToEmailRow(m));
371
343
  }
372
344
  // ===========================================================================
@@ -376,72 +348,39 @@ export class GraphRepository {
376
348
  throw new Error('Use listCalendarsAsync() for Graph repository');
377
349
  }
378
350
  async listCalendarsAsync() {
351
+ // Rows carry self-encoding fd_ tokens (the mapper mints them) — no cache.
379
352
  const calendars = await this.client.listCalendars();
380
- for (const calendar of calendars) {
381
- if (calendar.id != null) {
382
- const numericId = hashStringToNumber(calendar.id);
383
- this.idCache.folders.set(numericId, calendar.id);
384
- this.idCache.calendars.set(numericId, calendar.id);
385
- }
386
- }
387
353
  return calendars.map(mapCalendarToFolderRow);
388
354
  }
389
355
  listEvents(_limit) {
390
356
  throw new Error('Use listEventsAsync() for Graph repository');
391
357
  }
392
358
  async listEventsAsync(limit) {
359
+ // Rows carry self-encoding ev_ tokens (the mapper mints them) — no cache.
393
360
  const events = await this.client.listEvents(limit);
394
- for (const event of events) {
395
- if (event.id != null) {
396
- const numericId = hashStringToNumber(event.id);
397
- this.idCache.events.set(numericId, event.id);
398
- }
399
- }
400
361
  return events.map((e) => mapEventToEventRow(e));
401
362
  }
402
363
  listEventsByFolder(_folderId, _limit) {
403
364
  throw new Error('Use listEventsByFolderAsync() for Graph repository');
404
365
  }
405
366
  async listEventsByFolderAsync(folderId, limit) {
406
- const graphCalendarId = this.idCache.folders.get(folderId);
407
- if (graphCalendarId == null) {
408
- return this.listEventsAsync(limit);
409
- }
367
+ const graphCalendarId = this.toGraphId(folderId, 'folder');
410
368
  const events = await this.client.listEvents(limit, graphCalendarId);
411
- for (const event of events) {
412
- if (event.id != null) {
413
- const numericId = hashStringToNumber(event.id);
414
- this.idCache.events.set(numericId, event.id);
415
- }
416
- }
417
369
  return events.map((e) => mapEventToEventRow(e, graphCalendarId));
418
370
  }
419
371
  searchEvents(_query, _startDate, _endDate, _limit) {
420
372
  throw new Error('Use searchEventsAsync() for Graph repository');
421
373
  }
422
374
  async searchEventsAsync(query, startDate, endDate, limit) {
423
- // Graph doesn't have direct event search, so we filter client-side
375
+ // Graph doesn't have direct event search, so we filter client-side on the
376
+ // Graph events (by subject) before mapping to rows.
424
377
  const start = startDate != null ? new Date(startDate) : undefined;
425
378
  const end = endDate != null ? new Date(endDate) : undefined;
426
379
  const events = await this.client.listEvents(1000, undefined, start, end);
427
- for (const event of events) {
428
- if (event.id != null) {
429
- const numericId = hashStringToNumber(event.id);
430
- this.idCache.events.set(numericId, event.id);
431
- }
432
- }
433
- let rows = events.map((e) => mapEventToEventRow(e));
434
- // Filter by title client-side if query provided
435
- if (query != null) {
436
- const queryLower = query.toLowerCase();
437
- rows = rows.filter((row) => {
438
- // EventRow doesn't have title, so we need to check the original event
439
- const originalEvent = events.find((e) => e.id != null && hashStringToNumber(e.id) === row.id);
440
- const subject = originalEvent?.subject?.toLowerCase() ?? '';
441
- return subject.includes(queryLower);
442
- });
443
- }
444
- return rows.slice(0, limit);
380
+ const matched = query != null
381
+ ? events.filter((e) => (e.subject?.toLowerCase() ?? '').includes(query.toLowerCase()))
382
+ : events;
383
+ return matched.slice(0, limit).map((e) => mapEventToEventRow(e));
445
384
  }
446
385
  listEventsByDateRange(_startDate, _endDate, _limit) {
447
386
  throw new Error('Use listEventsByDateRangeAsync() for Graph repository');
@@ -450,36 +389,23 @@ export class GraphRepository {
450
389
  const start = new Date(startDate * 1000);
451
390
  const end = new Date(endDate * 1000);
452
391
  const events = await this.client.listEvents(limit, undefined, start, end);
453
- for (const event of events) {
454
- if (event.id != null) {
455
- const numericId = hashStringToNumber(event.id);
456
- this.idCache.events.set(numericId, event.id);
457
- }
458
- }
459
392
  return events.map((e) => mapEventToEventRow(e));
460
393
  }
461
394
  getEvent(_id) {
462
395
  throw new Error('Use getEventAsync() for Graph repository');
463
396
  }
464
397
  async getEventAsync(id) {
465
- const graphId = this.idCache.events.get(id);
466
- if (graphId == null) {
467
- return undefined;
468
- }
398
+ const graphId = this.toGraphId(id, 'event');
469
399
  const event = await this.client.getEvent(graphId);
470
400
  return event != null ? mapEventToEventRow(event) : undefined;
471
401
  }
402
+ /** Resolves an event id (durable `ev_` token or raw Graph id) to its Graph id. */
403
+ getEventGraphId(id) {
404
+ return this.toGraphId(id, 'event');
405
+ }
472
406
  async listEventInstancesAsync(eventId, startDate, endDate) {
473
- const graphId = this.idCache.events.get(eventId);
474
- if (graphId == null) {
475
- throw new Error(`Event ID ${eventId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
476
- }
407
+ const graphId = this.toGraphId(eventId, 'event');
477
408
  const instances = await this.client.listEventInstances(graphId, startDate, endDate);
478
- for (const inst of instances) {
479
- if (inst.id != null) {
480
- this.idCache.events.set(hashStringToNumber(inst.id), inst.id);
481
- }
482
- }
483
409
  return instances.map((e) => mapEventToEventRow(e));
484
410
  }
485
411
  // ===========================================================================
@@ -489,13 +415,9 @@ export class GraphRepository {
489
415
  throw new Error('Use listContactsAsync() for Graph repository');
490
416
  }
491
417
  async listContactsAsync(limit, offset) {
418
+ // Rows carry self-encoding `ct_` tokens (the mapper mints them), so there is
419
+ // no numeric-id cache to populate — the token decodes to the Graph id.
492
420
  const contacts = await this.client.listContacts(limit, offset);
493
- for (const contact of contacts) {
494
- if (contact.id != null) {
495
- const numericId = hashStringToNumber(contact.id);
496
- this.idCache.contacts.set(numericId, contact.id);
497
- }
498
- }
499
421
  return contacts.map(mapContactToContactRow);
500
422
  }
501
423
  searchContacts(_query, _limit) {
@@ -503,25 +425,20 @@ export class GraphRepository {
503
425
  }
504
426
  async searchContactsAsync(query, limit) {
505
427
  const contacts = await this.client.searchContacts(query, limit);
506
- for (const contact of contacts) {
507
- if (contact.id != null) {
508
- const numericId = hashStringToNumber(contact.id);
509
- this.idCache.contacts.set(numericId, contact.id);
510
- }
511
- }
512
428
  return contacts.map(mapContactToContactRow);
513
429
  }
514
430
  getContact(_id) {
515
431
  throw new Error('Use getContactAsync() for Graph repository');
516
432
  }
517
433
  async getContactAsync(id) {
518
- const graphId = this.idCache.contacts.get(id);
519
- if (graphId == null) {
520
- return undefined;
521
- }
434
+ const graphId = this.toGraphId(id, 'contact');
522
435
  const contact = await this.client.getContact(graphId);
523
436
  return contact != null ? mapContactToContactRow(contact) : undefined;
524
437
  }
438
+ /** Resolves a contact id (durable `ct_` token or raw Graph id) to its Graph id. */
439
+ getContactGraphId(id) {
440
+ return this.toGraphId(id, 'contact');
441
+ }
525
442
  // ===========================================================================
526
443
  // Contact Folders
527
444
  // ===========================================================================
@@ -529,10 +446,8 @@ export class GraphRepository {
529
446
  const folders = await this.client.listContactFolders();
530
447
  return folders.map((folder) => {
531
448
  const graphId = folder.id;
532
- const numericId = hashStringToNumber(graphId);
533
- this.idCache.contactFolders.set(numericId, graphId);
534
449
  return {
535
- id: numericId,
450
+ id: this.mintAlias('contactFolder', graphId),
536
451
  name: folder.displayName ?? '',
537
452
  parentFolderId: folder.parentFolderId ?? null,
538
453
  };
@@ -540,47 +455,30 @@ export class GraphRepository {
540
455
  }
541
456
  async createContactFolderAsync(name) {
542
457
  const created = await this.client.createContactFolder(name);
543
- const graphId = created.id;
544
- const numericId = hashStringToNumber(graphId);
545
- this.idCache.contactFolders.set(numericId, graphId);
546
- return numericId;
458
+ return this.mintAlias('contactFolder', created.id);
547
459
  }
548
460
  async deleteContactFolderAsync(folderId) {
549
- const graphId = this.idCache.contactFolders.get(folderId);
550
- if (graphId == null)
551
- throw new Error(`Contact folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
461
+ const graphId = this.toGraphId(folderId, 'contactFolder');
552
462
  await this.client.deleteContactFolder(graphId);
553
- this.idCache.contactFolders.delete(folderId);
554
463
  }
555
464
  async listContactsInFolderAsync(folderId, limit = 100) {
556
- const graphId = this.idCache.contactFolders.get(folderId);
557
- if (graphId == null)
558
- throw new Error(`Contact folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
465
+ const graphId = this.toGraphId(folderId, 'contactFolder');
559
466
  const contacts = await this.client.listContactsInFolder(graphId, limit);
560
- return contacts.map((c) => {
561
- if (c.id != null) {
562
- this.idCache.contacts.set(hashStringToNumber(c.id), c.id);
563
- }
564
- return mapContactToContactRow(c);
565
- });
467
+ return contacts.map(mapContactToContactRow);
566
468
  }
567
469
  // ===========================================================================
568
470
  // Contact Photos
569
471
  // ===========================================================================
570
472
  async getContactPhotoAsync(contactId) {
571
- const graphId = this.idCache.contacts.get(contactId);
572
- if (graphId == null)
573
- throw new Error(`Contact ID ${contactId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
473
+ const graphId = this.toGraphId(contactId, 'contact');
574
474
  const photoData = await this.client.getContactPhoto(graphId);
575
475
  const downloadDir = getDownloadDir();
576
- const filePath = path.join(downloadDir, `contact-${contactId}-photo.jpg`);
476
+ const filePath = path.join(downloadDir, `contact-${createHash('sha1').update(graphId).digest('hex').slice(0, 16)}-photo.jpg`);
577
477
  fs.writeFileSync(filePath, Buffer.from(photoData));
578
478
  return { filePath, contentType: 'image/jpeg' };
579
479
  }
580
480
  async setContactPhotoAsync(contactId, filePath) {
581
- const graphId = this.idCache.contacts.get(contactId);
582
- if (graphId == null)
583
- throw new Error(`Contact ID ${contactId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
481
+ const graphId = this.toGraphId(contactId, 'contact');
584
482
  const photoData = fs.readFileSync(filePath);
585
483
  const ext = path.extname(filePath).toLowerCase();
586
484
  const contentType = ext === '.png' ? 'image/png' : 'image/jpeg';
@@ -594,66 +492,65 @@ export class GraphRepository {
594
492
  }
595
493
  async listTasksAsync(limit, offset) {
596
494
  const tasks = await this.client.listAllTasks(limit, offset, true);
597
- for (const task of tasks) {
598
- if (task.id != null && task.taskListId != null) {
599
- const numericId = hashStringToNumber(task.id);
600
- this.idCache.tasks.set(numericId, { taskListId: task.taskListId, taskId: task.id });
601
- const listNumericId = hashStringToNumber(task.taskListId);
602
- this.idCache.taskLists.set(listNumericId, task.taskListId);
603
- }
604
- }
605
- return tasks.map(mapTaskToTaskRow);
495
+ return tasks
496
+ .filter((task) => task.id != null && task.taskListId != null)
497
+ .map((task) => {
498
+ const listTok = this.mintAlias('taskList', task.taskListId);
499
+ const taskTok = this.mintAliasComposite('task', { taskListId: task.taskListId, taskId: task.id });
500
+ return mapTaskToTaskRow({ ...task }, taskTok, listTok);
501
+ });
606
502
  }
607
503
  listIncompleteTasks(_limit, _offset) {
608
504
  throw new Error('Use listIncompleteTasksAsync() for Graph repository');
609
505
  }
610
506
  async listIncompleteTasksAsync(limit, offset) {
611
507
  const tasks = await this.client.listAllTasks(limit, offset, false);
612
- for (const task of tasks) {
613
- if (task.id != null && task.taskListId != null) {
614
- const numericId = hashStringToNumber(task.id);
615
- this.idCache.tasks.set(numericId, { taskListId: task.taskListId, taskId: task.id });
616
- const listNumericId = hashStringToNumber(task.taskListId);
617
- this.idCache.taskLists.set(listNumericId, task.taskListId);
618
- }
619
- }
620
- return tasks.map(mapTaskToTaskRow);
508
+ return tasks
509
+ .filter((task) => task.id != null && task.taskListId != null)
510
+ .map((task) => {
511
+ const listTok = this.mintAlias('taskList', task.taskListId);
512
+ const taskTok = this.mintAliasComposite('task', { taskListId: task.taskListId, taskId: task.id });
513
+ return mapTaskToTaskRow({ ...task }, taskTok, listTok);
514
+ });
621
515
  }
622
516
  searchTasks(_query, _limit) {
623
517
  throw new Error('Use searchTasksAsync() for Graph repository');
624
518
  }
625
519
  async searchTasksAsync(query, limit) {
626
520
  const tasks = await this.client.searchTasks(query, limit);
627
- for (const task of tasks) {
628
- if (task.id != null && task.taskListId != null) {
629
- const numericId = hashStringToNumber(task.id);
630
- this.idCache.tasks.set(numericId, { taskListId: task.taskListId, taskId: task.id });
631
- }
632
- }
633
- return tasks.map(mapTaskToTaskRow);
521
+ // Tasks without a taskListId can't form a {taskListId, taskId} composite
522
+ // token skip them, matching the current cache-miss guard.
523
+ return tasks
524
+ .filter((task) => task.id != null && task.taskListId != null)
525
+ .map((task) => {
526
+ const listTok = this.mintAlias('taskList', task.taskListId);
527
+ const taskTok = this.mintAliasComposite('task', { taskListId: task.taskListId, taskId: task.id });
528
+ return mapTaskToTaskRow({ ...task }, taskTok, listTok);
529
+ });
634
530
  }
635
531
  getTask(_id) {
636
532
  throw new Error('Use getTaskAsync() for Graph repository');
637
533
  }
638
534
  async getTaskAsync(id) {
639
- const taskInfo = this.idCache.tasks.get(id);
640
- if (taskInfo == null) {
535
+ let taskInfo;
536
+ try {
537
+ taskInfo = this.toGraphParts(id, 'task', ['taskListId', 'taskId']);
538
+ }
539
+ catch {
641
540
  return undefined;
642
541
  }
643
542
  const task = await this.client.getTask(taskInfo.taskListId, taskInfo.taskId);
644
543
  if (task == null) {
645
544
  return undefined;
646
545
  }
647
- return mapTaskToTaskRow({ ...task, taskListId: taskInfo.taskListId });
546
+ return mapTaskToTaskRow({ ...task, taskListId: taskInfo.taskListId }, String(id), this.mintAlias('taskList', taskInfo.taskListId));
648
547
  }
649
548
  async listTaskListsAsync() {
650
549
  const lists = await this.client.listTaskLists();
651
550
  return lists.map((list) => {
652
551
  const graphId = list.id;
653
- const numericId = hashStringToNumber(graphId);
654
- this.idCache.taskLists.set(numericId, graphId);
655
552
  return {
656
- id: numericId,
553
+ id: this.mintAlias('taskList', graphId),
657
554
  name: list.displayName ?? '',
658
555
  isDefault: list.wellknownListName === 'defaultList',
659
556
  };
@@ -694,37 +591,33 @@ export class GraphRepository {
694
591
  return this.client;
695
592
  }
696
593
  /**
697
- * Returns the Graph string ID for a cached draft numeric ID (satisfies IMailSendRepository).
594
+ * Resolves a draft id (durable `em_` token or raw Graph id) to its Graph id
595
+ * (satisfies IMailSendRepository). Drafts are messages.
698
596
  */
699
597
  getGraphIdForDraft(draftId) {
700
- return this.idCache.messages.get(draftId);
598
+ return this.toGraphId(draftId, 'message');
701
599
  }
702
600
  /**
703
- * Gets the Graph string ID from a numeric ID.
601
+ * Gets task info (Graph taskListId/taskId) from a durable `td_` token.
704
602
  */
705
- getGraphId(type, numericId) {
706
- switch (type) {
707
- case 'folder':
708
- return this.idCache.folders.get(numericId);
709
- case 'message':
710
- return this.idCache.messages.get(numericId);
711
- case 'event':
712
- return this.idCache.events.get(numericId);
713
- case 'contact':
714
- return this.idCache.contacts.get(numericId);
603
+ getTaskInfo(id) {
604
+ try {
605
+ return this.toGraphParts(id, 'task', ['taskListId', 'taskId']);
606
+ }
607
+ catch {
608
+ return undefined;
715
609
  }
716
610
  }
717
611
  /**
718
- * Gets task info from a numeric ID.
719
- */
720
- getTaskInfo(numericId) {
721
- return this.idCache.tasks.get(numericId);
722
- }
723
- /**
724
- * Gets the Graph string ID for a task list from a numeric ID.
612
+ * Gets the Graph string ID for a task list from a durable `tl_` token.
725
613
  */
726
- getTaskListGraphId(numericId) {
727
- return this.idCache.taskLists.get(numericId);
614
+ getTaskListGraphId(id) {
615
+ try {
616
+ return this.toGraphId(id, 'taskList');
617
+ }
618
+ catch {
619
+ return undefined;
620
+ }
728
621
  }
729
622
  // ===========================================================================
730
623
  // Write Operations (Async)
@@ -771,42 +664,28 @@ export class GraphRepository {
771
664
  }
772
665
  // Async implementations
773
666
  async moveEmailAsync(emailId, destinationFolderId) {
774
- const graphMessageId = this.idCache.messages.get(emailId);
775
- const graphFolderId = this.idCache.folders.get(destinationFolderId);
776
- if (graphMessageId == null)
777
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
778
- if (graphFolderId == null)
779
- throw new Error(`Folder ID ${destinationFolderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
667
+ const graphMessageId = this.toGraphId(emailId, 'message');
668
+ const graphFolderId = this.toGraphId(destinationFolderId, 'folder');
780
669
  await this.client.moveMessage(graphMessageId, graphFolderId);
781
670
  }
782
671
  async deleteEmailAsync(emailId) {
783
- const graphId = this.idCache.messages.get(emailId);
784
- if (graphId == null)
785
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
672
+ const graphId = this.toGraphId(emailId, 'message');
786
673
  await this.client.deleteMessage(graphId);
787
674
  }
788
675
  async archiveEmailAsync(emailId) {
789
- const graphId = this.idCache.messages.get(emailId);
790
- if (graphId == null)
791
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
676
+ const graphId = this.toGraphId(emailId, 'message');
792
677
  await this.client.archiveMessage(graphId);
793
678
  }
794
679
  async junkEmailAsync(emailId) {
795
- const graphId = this.idCache.messages.get(emailId);
796
- if (graphId == null)
797
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
680
+ const graphId = this.toGraphId(emailId, 'message');
798
681
  await this.client.junkMessage(graphId);
799
682
  }
800
683
  async markEmailReadAsync(emailId, isRead) {
801
- const graphId = this.idCache.messages.get(emailId);
802
- if (graphId == null)
803
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
684
+ const graphId = this.toGraphId(emailId, 'message');
804
685
  await this.client.updateMessage(graphId, { isRead });
805
686
  }
806
687
  async setEmailFlagAsync(emailId, flagStatus) {
807
- const graphId = this.idCache.messages.get(emailId);
808
- if (graphId == null)
809
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
688
+ const graphId = this.toGraphId(emailId, 'message');
810
689
  const flagStatusMap = {
811
690
  0: 'notFlagged',
812
691
  1: 'flagged',
@@ -817,55 +696,35 @@ export class GraphRepository {
817
696
  });
818
697
  }
819
698
  async setEmailCategoriesAsync(emailId, categories) {
820
- const graphId = this.idCache.messages.get(emailId);
821
- if (graphId == null)
822
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
699
+ const graphId = this.toGraphId(emailId, 'message');
823
700
  await this.client.updateMessage(graphId, { categories });
824
701
  }
825
702
  async setEmailImportanceAsync(emailId, importance) {
826
- const graphId = this.idCache.messages.get(emailId);
827
- if (graphId == null)
828
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
703
+ const graphId = this.toGraphId(emailId, 'message');
829
704
  await this.client.updateMessage(graphId, { importance });
830
705
  }
831
706
  async createFolderAsync(name, parentFolderId) {
832
707
  const graphParentId = parentFolderId != null
833
- ? this.idCache.folders.get(parentFolderId)
708
+ ? this.toGraphId(parentFolderId, 'folder')
834
709
  : undefined;
835
- const folder = await this.client.createMailFolder(name, graphParentId ?? undefined);
836
- // Update cache with new folder
837
- if (folder.id != null) {
838
- const numericId = hashStringToNumber(folder.id);
839
- this.idCache.folders.set(numericId, folder.id);
840
- }
710
+ const folder = await this.client.createMailFolder(name, graphParentId);
841
711
  return mapMailFolderToRow(folder);
842
712
  }
843
713
  async deleteFolderAsync(folderId) {
844
- const graphId = this.idCache.folders.get(folderId);
845
- if (graphId == null)
846
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
714
+ const graphId = this.toGraphId(folderId, 'folder');
847
715
  await this.client.deleteMailFolder(graphId);
848
- this.idCache.folders.delete(folderId);
849
716
  }
850
717
  async renameFolderAsync(folderId, newName) {
851
- const graphId = this.idCache.folders.get(folderId);
852
- if (graphId == null)
853
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
718
+ const graphId = this.toGraphId(folderId, 'folder');
854
719
  await this.client.renameMailFolder(graphId, newName);
855
720
  }
856
721
  async moveFolderAsync(folderId, destinationParentId) {
857
- const graphFolderId = this.idCache.folders.get(folderId);
858
- const graphParentId = this.idCache.folders.get(destinationParentId);
859
- if (graphFolderId == null)
860
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
861
- if (graphParentId == null)
862
- throw new Error(`Parent folder ID ${destinationParentId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
722
+ const graphFolderId = this.toGraphId(folderId, 'folder');
723
+ const graphParentId = this.toGraphId(destinationParentId, 'folder');
863
724
  await this.client.moveMailFolder(graphFolderId, graphParentId);
864
725
  }
865
726
  async emptyFolderAsync(folderId) {
866
- const graphId = this.idCache.folders.get(folderId);
867
- if (graphId == null)
868
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
727
+ const graphId = this.toGraphId(folderId, 'folder');
869
728
  await this.client.emptyMailFolder(graphId);
870
729
  }
871
730
  // ===========================================================================
@@ -875,7 +734,7 @@ export class GraphRepository {
875
734
  * Creates a new draft message.
876
735
  *
877
736
  * Converts email address strings to Recipient objects, calls the Graph client,
878
- * adds the returned draft to idCache.messages, and returns its numeric ID.
737
+ * and returns a durable `em_` token plus the raw Graph id.
879
738
  */
880
739
  async createDraftAsync(params) {
881
740
  const toRecipients = (params.to ?? []).map(addr => ({
@@ -895,19 +754,15 @@ export class GraphRepository {
895
754
  bccRecipients,
896
755
  });
897
756
  const graphId = draft.id;
898
- const numericId = hashStringToNumber(graphId);
899
- this.idCache.messages.set(numericId, graphId);
900
- return { numericId, graphId };
757
+ return { token: mintSelfEncoded('message', graphId), graphId };
901
758
  }
902
759
  /**
903
760
  * Updates an existing draft message.
904
761
  *
905
- * Looks up the Graph string ID from idCache.messages, then calls the client.
762
+ * Resolves the draft id to its Graph id, then calls the client.
906
763
  */
907
764
  async updateDraftAsync(draftId, updates) {
908
- const graphId = this.idCache.messages.get(draftId);
909
- if (graphId == null)
910
- throw new Error(`Message ID ${draftId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
765
+ const graphId = this.toGraphId(draftId, 'message');
911
766
  await this.client.updateDraft(graphId, updates);
912
767
  }
913
768
  /**
@@ -921,12 +776,10 @@ export class GraphRepository {
921
776
  /**
922
777
  * Sends an existing draft message.
923
778
  *
924
- * Looks up the Graph string ID from idCache.messages, then calls the client.
779
+ * Resolves the id to its Graph id, then calls the client.
925
780
  */
926
781
  async sendDraftAsync(draftId) {
927
- const graphId = this.idCache.messages.get(draftId);
928
- if (graphId == null)
929
- throw new Error(`Message ID ${draftId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
782
+ const graphId = this.toGraphId(draftId, 'message');
930
783
  await this.client.sendDraft(graphId);
931
784
  }
932
785
  /**
@@ -955,24 +808,20 @@ export class GraphRepository {
955
808
  /**
956
809
  * Replies to a message (or replies all).
957
810
  *
958
- * Looks up the Graph string ID from idCache.messages, then calls the client.
811
+ * Resolves the id to its Graph id, then calls the client.
959
812
  */
960
813
  async replyMessageAsync(messageId, comment, replyAll) {
961
- const graphId = this.idCache.messages.get(messageId);
962
- if (graphId == null)
963
- throw new Error(`Message ID ${messageId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
814
+ const graphId = this.toGraphId(messageId, 'message');
964
815
  await this.client.replyMessage(graphId, comment, replyAll);
965
816
  }
966
817
  /**
967
818
  * Forwards a message to specified recipients.
968
819
  *
969
- * Looks up the Graph string ID from idCache.messages, converts recipient
970
- * email strings to Recipient objects, then calls the client.
820
+ * Resolves the id to its Graph id, converts recipient email strings to
821
+ * Recipient objects, then calls the client.
971
822
  */
972
823
  async forwardMessageAsync(messageId, toRecipients, comment) {
973
- const graphId = this.idCache.messages.get(messageId);
974
- if (graphId == null)
975
- throw new Error(`Message ID ${messageId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
824
+ const graphId = this.toGraphId(messageId, 'message');
976
825
  const recipients = toRecipients.map(addr => ({
977
826
  emailAddress: { address: addr },
978
827
  }));
@@ -984,45 +833,33 @@ export class GraphRepository {
984
833
  /**
985
834
  * Creates a reply (or reply-all) draft for a message.
986
835
  *
987
- * Looks up the Graph string ID from idCache.messages, creates the draft
988
- * via the client, caches the new draft ID, and optionally updates the body.
836
+ * Resolves the source message id to its Graph id, creates the draft via the
837
+ * client, and optionally updates the body.
989
838
  *
990
- * @returns The numeric and graph IDs of the new draft.
839
+ * @returns A durable `em_` token and the raw Graph id of the new draft.
991
840
  */
992
841
  async replyAsDraftAsync(messageId, replyAll = false, comment, bodyType = 'text') {
993
- const graphMessageId = this.idCache.messages.get(messageId);
994
- if (graphMessageId == null)
995
- throw new Error(`Message ID ${messageId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
842
+ const graphMessageId = this.toGraphId(messageId, 'message');
843
+ // Pass comment/body through createReply so the quoted thread is preserved
844
+ const body = comment != null ? { contentType: bodyType, content: comment } : undefined;
996
845
  const draft = replyAll
997
- ? await this.client.createReplyAllDraft(graphMessageId)
998
- : await this.client.createReplyDraft(graphMessageId);
846
+ ? await this.client.createReplyAllDraft(graphMessageId, undefined, body)
847
+ : await this.client.createReplyDraft(graphMessageId, undefined, body);
999
848
  const graphId = draft.id;
1000
- const numericId = hashStringToNumber(graphId);
1001
- this.idCache.messages.set(numericId, graphId);
1002
- if (comment != null) {
1003
- await this.client.updateDraft(graphId, {
1004
- body: { contentType: bodyType, content: comment },
1005
- });
1006
- }
1007
- return { numericId, graphId };
849
+ return { token: mintSelfEncoded('message', graphId), graphId };
1008
850
  }
1009
851
  /**
1010
852
  * Creates a forward draft for a message.
1011
853
  *
1012
- * Looks up the Graph string ID from idCache.messages, creates the draft
1013
- * via the client, caches the new draft ID, and optionally updates the
1014
- * recipients and body.
854
+ * Resolves the source message id to its Graph id, creates the draft via the
855
+ * client, and optionally updates the recipients and body.
1015
856
  *
1016
- * @returns The numeric and graph IDs of the new draft.
857
+ * @returns A durable `em_` token and the raw Graph id of the new draft.
1017
858
  */
1018
859
  async forwardAsDraftAsync(messageId, toRecipients, comment, bodyType = 'text') {
1019
- const graphMessageId = this.idCache.messages.get(messageId);
1020
- if (graphMessageId == null)
1021
- throw new Error(`Message ID ${messageId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
860
+ const graphMessageId = this.toGraphId(messageId, 'message');
1022
861
  const draft = await this.client.createForwardDraft(graphMessageId);
1023
862
  const graphId = draft.id;
1024
- const numericId = hashStringToNumber(graphId);
1025
- this.idCache.messages.set(numericId, graphId);
1026
863
  const updates = {};
1027
864
  if (toRecipients != null && toRecipients.length > 0) {
1028
865
  updates.toRecipients = toRecipients.map(addr => ({
@@ -1035,7 +872,7 @@ export class GraphRepository {
1035
872
  if (Object.keys(updates).length > 0) {
1036
873
  await this.client.updateDraft(graphId, updates);
1037
874
  }
1038
- return { numericId, graphId };
875
+ return { token: mintSelfEncoded('message', graphId), graphId };
1039
876
  }
1040
877
  // ---------------------------------------------------------------------------
1041
878
  // Calendar Scheduling
@@ -1077,26 +914,18 @@ export class GraphRepository {
1077
914
  /**
1078
915
  * Lists attachments for a given email.
1079
916
  *
1080
- * Looks up the Graph message ID from idCache.messages, calls
1081
- * client.listAttachments, hashes each attachment ID to a numeric key,
1082
- * and caches it in idCache.attachments with { messageId, attachmentId }.
917
+ * Resolves the id to its Graph id, calls client.listAttachments, and mints
918
+ * an `at_` composite alias token ({ messageId, attachmentId }) per item.
1083
919
  *
1084
920
  * @returns Array of attachment metadata objects.
1085
921
  */
1086
922
  async listAttachmentsAsync(emailId) {
1087
- const graphMessageId = this.idCache.messages.get(emailId);
1088
- if (graphMessageId == null)
1089
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
923
+ const graphMessageId = this.toGraphId(emailId, 'message');
1090
924
  const attachments = await this.client.listAttachments(graphMessageId);
1091
925
  return attachments.map((att) => {
1092
926
  const attId = att.id ?? '';
1093
- const numericId = hashStringToNumber(attId);
1094
- this.idCache.attachments.set(numericId, {
1095
- messageId: graphMessageId,
1096
- attachmentId: attId,
1097
- });
1098
927
  return {
1099
- id: numericId,
928
+ id: this.mintAliasComposite('attachment', { messageId: graphMessageId, attachmentId: attId }),
1100
929
  name: att.name ?? '',
1101
930
  size: att.size ?? 0,
1102
931
  contentType: att.contentType ?? 'application/octet-stream',
@@ -1107,16 +936,14 @@ export class GraphRepository {
1107
936
  /**
1108
937
  * Downloads an attachment for a given email.
1109
938
  *
1110
- * Looks up { messageId, attachmentId } from idCache.attachments,
1111
- * then delegates to the downloadAttachment helper which fetches
1112
- * the content and writes it to disk.
939
+ * Resolves the `at_` composite token to { messageId, attachmentId }, then
940
+ * delegates to the downloadAttachment helper which fetches the content and
941
+ * writes it to disk.
1113
942
  *
1114
943
  * @returns Metadata about the downloaded file including its local path.
1115
944
  */
1116
945
  async downloadAttachmentAsync(attachmentId) {
1117
- const cached = this.idCache.attachments.get(attachmentId);
1118
- if (cached == null)
1119
- throw new Error(`Attachment ID ${attachmentId} not found in cache. Call list_attachments first.`);
946
+ const cached = this.toGraphParts(attachmentId, 'attachment', ['messageId', 'attachmentId']);
1120
947
  return downloadAttachment(this.client, cached.messageId, cached.attachmentId);
1121
948
  }
1122
949
  // ===========================================================================
@@ -1162,13 +989,11 @@ export class GraphRepository {
1162
989
  graphEvent.onlineMeetingProvider = params.online_meeting_provider ?? 'teamsForBusiness';
1163
990
  }
1164
991
  const graphCalendarId = params.calendarId != null
1165
- ? this.idCache.folders.get(params.calendarId)
992
+ ? this.toGraphId(params.calendarId, 'folder')
1166
993
  : undefined;
1167
994
  const created = await this.client.createEvent(graphEvent, graphCalendarId);
1168
995
  const graphId = created.id;
1169
- const numericId = hashStringToNumber(graphId);
1170
- this.idCache.events.set(numericId, graphId);
1171
- return numericId;
996
+ return mintSelfEncoded('event', graphId);
1172
997
  }
1173
998
  /**
1174
999
  * Updates an existing calendar event.
@@ -1177,9 +1002,7 @@ export class GraphRepository {
1177
1002
  * client.updateEvent(). Throws if the event is not cached.
1178
1003
  */
1179
1004
  async updateEventAsync(eventId, updates) {
1180
- const graphId = this.idCache.events.get(eventId);
1181
- if (graphId == null)
1182
- throw new Error(`Event ID ${eventId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1005
+ const graphId = this.toGraphId(eventId, 'event');
1183
1006
  await this.client.updateEvent(graphId, updates);
1184
1007
  }
1185
1008
  /**
@@ -1190,11 +1013,8 @@ export class GraphRepository {
1190
1013
  * Throws if the event is not cached.
1191
1014
  */
1192
1015
  async deleteEventAsync(eventId) {
1193
- const graphId = this.idCache.events.get(eventId);
1194
- if (graphId == null)
1195
- throw new Error(`Event ID ${eventId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1016
+ const graphId = this.toGraphId(eventId, 'event');
1196
1017
  await this.client.deleteEvent(graphId);
1197
- this.idCache.events.delete(eventId);
1198
1018
  }
1199
1019
  /**
1200
1020
  * Responds to a calendar event invitation.
@@ -1203,9 +1023,7 @@ export class GraphRepository {
1203
1023
  * client.respondToEvent(). Throws if the event is not cached.
1204
1024
  */
1205
1025
  async respondToEventAsync(eventId, response, sendResponse, comment) {
1206
- const graphId = this.idCache.events.get(eventId);
1207
- if (graphId == null)
1208
- throw new Error(`Event ID ${eventId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1026
+ const graphId = this.toGraphId(eventId, 'event');
1209
1027
  await this.client.respondToEvent(graphId, response, sendResponse, comment);
1210
1028
  }
1211
1029
  // ===========================================================================
@@ -1250,9 +1068,7 @@ export class GraphRepository {
1250
1068
  }
1251
1069
  const created = await this.client.createContact(graphContact);
1252
1070
  const graphId = created.id;
1253
- const numericId = hashStringToNumber(graphId);
1254
- this.idCache.contacts.set(numericId, graphId);
1255
- return numericId;
1071
+ return mintSelfEncoded('contact', graphId);
1256
1072
  }
1257
1073
  /**
1258
1074
  * Updates an existing contact.
@@ -1261,9 +1077,7 @@ export class GraphRepository {
1261
1077
  * client.updateContact(). Throws if the contact is not cached.
1262
1078
  */
1263
1079
  async updateContactAsync(contactId, updates) {
1264
- const graphId = this.idCache.contacts.get(contactId);
1265
- if (graphId == null)
1266
- throw new Error(`Contact ID ${contactId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1080
+ const graphId = this.toGraphId(contactId, 'contact');
1267
1081
  await this.client.updateContact(graphId, updates);
1268
1082
  }
1269
1083
  /**
@@ -1274,11 +1088,8 @@ export class GraphRepository {
1274
1088
  * Throws if the contact is not cached.
1275
1089
  */
1276
1090
  async deleteContactAsync(contactId) {
1277
- const graphId = this.idCache.contacts.get(contactId);
1278
- if (graphId == null)
1279
- throw new Error(`Contact ID ${contactId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1091
+ const graphId = this.toGraphId(contactId, 'contact');
1280
1092
  await this.client.deleteContact(graphId);
1281
- this.idCache.contacts.delete(contactId);
1282
1093
  }
1283
1094
  // ===========================================================================
1284
1095
  // Task Write Operations (Async)
@@ -1286,14 +1097,12 @@ export class GraphRepository {
1286
1097
  /**
1287
1098
  * Creates a new task in a task list.
1288
1099
  *
1289
- * Looks up the Graph task list ID from idCache.taskLists, builds a
1290
- * Graph API task object from the given params, calls client.createTask(),
1291
- * caches the resulting ID, and returns a numeric ID.
1100
+ * Resolves the `tl_` task list token to a Graph ID, builds a Graph API task
1101
+ * object from the given params, calls client.createTask(), and returns a
1102
+ * durable `td_` token for the created task.
1292
1103
  */
1293
1104
  async createTaskAsync(params) {
1294
- const graphListId = this.idCache.taskLists.get(params.task_list_id);
1295
- if (graphListId == null)
1296
- throw new Error(`Task list ID ${params.task_list_id} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1105
+ const graphListId = this.toGraphId(params.task_list_id, 'taskList');
1297
1106
  const graphTask = {
1298
1107
  title: params.title,
1299
1108
  };
@@ -1339,22 +1148,14 @@ export class GraphRepository {
1339
1148
  };
1340
1149
  }
1341
1150
  const created = await this.client.createTask(graphListId, graphTask);
1342
- const graphId = created.id;
1343
- const numericId = hashStringToNumber(graphId);
1344
- this.idCache.tasks.set(numericId, { taskListId: graphListId, taskId: graphId });
1345
- return numericId;
1151
+ return this.mintAliasComposite('task', { taskListId: graphListId, taskId: created.id });
1346
1152
  }
1347
1153
  /**
1348
1154
  * Updates an existing task.
1349
- *
1350
- * Looks up the Graph task info from idCache.tasks, then calls
1351
- * client.updateTask(). Throws if the task is not cached.
1352
1155
  */
1353
1156
  async updateTaskAsync(taskId, updates) {
1354
- const taskInfo = this.idCache.tasks.get(taskId);
1355
- if (taskInfo == null)
1356
- throw new Error(`Task ID ${taskId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1357
- await this.client.updateTask(taskInfo.taskListId, taskInfo.taskId, updates);
1157
+ const { taskListId, taskId: gTaskId } = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1158
+ await this.client.updateTask(taskListId, gTaskId, updates);
1358
1159
  }
1359
1160
  /**
1360
1161
  * Marks a task as completed.
@@ -1373,64 +1174,42 @@ export class GraphRepository {
1373
1174
  }
1374
1175
  /**
1375
1176
  * Deletes a task.
1376
- *
1377
- * Looks up the Graph task info from idCache.tasks, calls
1378
- * client.deleteTask(), and removes the entry from idCache.
1379
- * Throws if the task is not cached.
1380
1177
  */
1381
1178
  async deleteTaskAsync(taskId) {
1382
- const taskInfo = this.idCache.tasks.get(taskId);
1383
- if (taskInfo == null)
1384
- throw new Error(`Task ID ${taskId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1385
- await this.client.deleteTask(taskInfo.taskListId, taskInfo.taskId);
1386
- this.idCache.tasks.delete(taskId);
1179
+ const { taskListId, taskId: gTaskId } = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1180
+ await this.client.deleteTask(taskListId, gTaskId);
1387
1181
  }
1388
1182
  /**
1389
1183
  * Creates a new task list.
1390
- *
1391
- * Calls client.createTaskList(), caches the resulting ID in
1392
- * idCache.taskLists, and returns a numeric ID.
1393
1184
  */
1394
1185
  async createTaskListAsync(displayName) {
1395
1186
  const created = await this.client.createTaskList(displayName);
1396
- const graphId = created.id;
1397
- const numericId = hashStringToNumber(graphId);
1398
- this.idCache.taskLists.set(numericId, graphId);
1399
- return numericId;
1187
+ return this.mintAlias('taskList', created.id);
1400
1188
  }
1401
1189
  /**
1402
1190
  * Renames a task list.
1403
1191
  */
1404
1192
  async renameTaskListAsync(listId, name) {
1405
- const graphId = this.idCache.taskLists.get(listId);
1406
- if (graphId == null)
1407
- throw new Error(`Task list ID ${listId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1193
+ const graphId = this.toGraphId(listId, 'taskList');
1408
1194
  await this.client.updateTaskList(graphId, { displayName: name });
1409
1195
  }
1410
1196
  /**
1411
1197
  * Deletes a task list.
1412
1198
  */
1413
1199
  async deleteTaskListAsync(listId) {
1414
- const graphId = this.idCache.taskLists.get(listId);
1415
- if (graphId == null)
1416
- throw new Error(`Task list ID ${listId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1200
+ const graphId = this.toGraphId(listId, 'taskList');
1417
1201
  await this.client.deleteTaskList(graphId);
1418
- this.idCache.taskLists.delete(listId);
1419
1202
  }
1420
1203
  // ===========================================================================
1421
1204
  // Checklist Items
1422
1205
  // ===========================================================================
1423
1206
  async listChecklistItemsAsync(taskId) {
1424
- const taskInfo = this.idCache.tasks.get(taskId);
1425
- if (taskInfo == null)
1426
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1207
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1427
1208
  const items = await this.client.listChecklistItems(taskInfo.taskListId, taskInfo.taskId);
1428
1209
  return items.map((item) => {
1429
1210
  const graphId = item.id;
1430
- const numericId = hashStringToNumber(graphId);
1431
- this.idCache.checklistItems.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: graphId });
1432
1211
  return {
1433
- id: numericId,
1212
+ id: this.mintAliasComposite('checklistItem', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: graphId }),
1434
1213
  displayName: item.displayName ?? '',
1435
1214
  isChecked: item.isChecked ?? false,
1436
1215
  createdDateTime: item.createdDateTime ?? '',
@@ -1438,19 +1217,12 @@ export class GraphRepository {
1438
1217
  });
1439
1218
  }
1440
1219
  async createChecklistItemAsync(taskId, displayName, isChecked = false) {
1441
- const taskInfo = this.idCache.tasks.get(taskId);
1442
- if (taskInfo == null)
1443
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1220
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1444
1221
  const item = await this.client.createChecklistItem(taskInfo.taskListId, taskInfo.taskId, displayName, isChecked);
1445
- const graphId = item.id;
1446
- const numericId = hashStringToNumber(graphId);
1447
- this.idCache.checklistItems.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: graphId });
1448
- return numericId;
1222
+ return this.mintAliasComposite('checklistItem', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: item.id });
1449
1223
  }
1450
1224
  async updateChecklistItemAsync(checklistItemId, updates) {
1451
- const cached = this.idCache.checklistItems.get(checklistItemId);
1452
- if (cached == null)
1453
- throw new Error(`Checklist item ID ${checklistItemId} not found in cache. Try listing checklist items first.`);
1225
+ const cached = this.toGraphParts(checklistItemId, 'checklistItem', ['taskListId', 'taskId', 'checklistItemId']);
1454
1226
  const graphUpdates = {};
1455
1227
  if (updates.displayName != null)
1456
1228
  graphUpdates['displayName'] = updates.displayName;
@@ -1459,26 +1231,19 @@ export class GraphRepository {
1459
1231
  await this.client.updateChecklistItem(cached.taskListId, cached.taskId, cached.checklistItemId, graphUpdates);
1460
1232
  }
1461
1233
  async deleteChecklistItemAsync(checklistItemId) {
1462
- const cached = this.idCache.checklistItems.get(checklistItemId);
1463
- if (cached == null)
1464
- throw new Error(`Checklist item ID ${checklistItemId} not found in cache. Try listing checklist items first.`);
1234
+ const cached = this.toGraphParts(checklistItemId, 'checklistItem', ['taskListId', 'taskId', 'checklistItemId']);
1465
1235
  await this.client.deleteChecklistItem(cached.taskListId, cached.taskId, cached.checklistItemId);
1466
- this.idCache.checklistItems.delete(checklistItemId);
1467
1236
  }
1468
1237
  // ===========================================================================
1469
1238
  // Linked Resources
1470
1239
  // ===========================================================================
1471
1240
  async listLinkedResourcesAsync(taskId) {
1472
- const taskInfo = this.idCache.tasks.get(taskId);
1473
- if (taskInfo == null)
1474
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1241
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1475
1242
  const items = await this.client.listLinkedResources(taskInfo.taskListId, taskInfo.taskId);
1476
1243
  return items.map((item) => {
1477
1244
  const graphId = item.id;
1478
- const numericId = hashStringToNumber(graphId);
1479
- this.idCache.linkedResources.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: graphId });
1480
1245
  return {
1481
- id: numericId,
1246
+ id: this.mintAliasComposite('linkedResource', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: graphId }),
1482
1247
  webUrl: item.webUrl ?? '',
1483
1248
  applicationName: item.applicationName ?? '',
1484
1249
  displayName: item.displayName ?? '',
@@ -1486,36 +1251,24 @@ export class GraphRepository {
1486
1251
  });
1487
1252
  }
1488
1253
  async createLinkedResourceAsync(taskId, webUrl, applicationName, displayName) {
1489
- const taskInfo = this.idCache.tasks.get(taskId);
1490
- if (taskInfo == null)
1491
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1254
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1492
1255
  const item = await this.client.createLinkedResource(taskInfo.taskListId, taskInfo.taskId, webUrl, applicationName, displayName);
1493
- const graphId = item.id;
1494
- const numericId = hashStringToNumber(graphId);
1495
- this.idCache.linkedResources.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: graphId });
1496
- return numericId;
1256
+ return this.mintAliasComposite('linkedResource', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: item.id });
1497
1257
  }
1498
1258
  async deleteLinkedResourceAsync(linkedResourceId) {
1499
- const cached = this.idCache.linkedResources.get(linkedResourceId);
1500
- if (cached == null)
1501
- throw new Error(`Linked resource ID ${linkedResourceId} not found in cache. Try listing linked resources first.`);
1259
+ const cached = this.toGraphParts(linkedResourceId, 'linkedResource', ['taskListId', 'taskId', 'linkedResourceId']);
1502
1260
  await this.client.deleteLinkedResource(cached.taskListId, cached.taskId, cached.linkedResourceId);
1503
- this.idCache.linkedResources.delete(linkedResourceId);
1504
1261
  }
1505
1262
  // ===========================================================================
1506
1263
  // Task Attachments
1507
1264
  // ===========================================================================
1508
1265
  async listTaskAttachmentsAsync(taskId) {
1509
- const taskInfo = this.idCache.tasks.get(taskId);
1510
- if (taskInfo == null)
1511
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1266
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1512
1267
  const items = await this.client.listTaskAttachments(taskInfo.taskListId, taskInfo.taskId);
1513
1268
  return items.map((item) => {
1514
1269
  const graphId = item.id;
1515
- const numericId = hashStringToNumber(graphId);
1516
- this.idCache.taskAttachments.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: graphId });
1517
1270
  return {
1518
- id: numericId,
1271
+ id: this.mintAliasComposite('taskAttachment', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: graphId }),
1519
1272
  name: item['name'] ?? '',
1520
1273
  size: item.size ?? 0,
1521
1274
  contentType: item.contentType ?? '',
@@ -1523,21 +1276,13 @@ export class GraphRepository {
1523
1276
  });
1524
1277
  }
1525
1278
  async createTaskAttachmentAsync(taskId, name, contentBytes, contentType) {
1526
- const taskInfo = this.idCache.tasks.get(taskId);
1527
- if (taskInfo == null)
1528
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1279
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1529
1280
  const item = await this.client.createTaskAttachment(taskInfo.taskListId, taskInfo.taskId, name, contentBytes, contentType);
1530
- const graphId = item.id;
1531
- const numericId = hashStringToNumber(graphId);
1532
- this.idCache.taskAttachments.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: graphId });
1533
- return numericId;
1281
+ return this.mintAliasComposite('taskAttachment', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: item.id });
1534
1282
  }
1535
1283
  async deleteTaskAttachmentAsync(taskAttachmentId) {
1536
- const cached = this.idCache.taskAttachments.get(taskAttachmentId);
1537
- if (cached == null)
1538
- throw new Error(`Task attachment ID ${taskAttachmentId} not found in cache. Try listing task attachments first.`);
1284
+ const cached = this.toGraphParts(taskAttachmentId, 'taskAttachment', ['taskListId', 'taskId', 'attachmentId']);
1539
1285
  await this.client.deleteTaskAttachment(cached.taskListId, cached.taskId, cached.attachmentId);
1540
- this.idCache.taskAttachments.delete(taskAttachmentId);
1541
1286
  }
1542
1287
  // ===========================================================================
1543
1288
  // Mail Rules (Async)
@@ -1549,10 +1294,8 @@ export class GraphRepository {
1549
1294
  const rules = await this.client.listMailRules();
1550
1295
  return rules.map((rule) => {
1551
1296
  const graphId = rule.id;
1552
- const numericId = hashStringToNumber(graphId);
1553
- this.idCache.rules.set(numericId, graphId);
1554
1297
  return {
1555
- id: numericId,
1298
+ id: this.mintAlias('mailRule', graphId),
1556
1299
  displayName: rule.displayName ?? '',
1557
1300
  sequence: rule.sequence ?? 0,
1558
1301
  isEnabled: rule.isEnabled ?? true,
@@ -1566,20 +1309,14 @@ export class GraphRepository {
1566
1309
  */
1567
1310
  async createMailRuleAsync(rule) {
1568
1311
  const created = await this.client.createMailRule(rule);
1569
- const graphId = created.id;
1570
- const numericId = hashStringToNumber(graphId);
1571
- this.idCache.rules.set(numericId, graphId);
1572
- return numericId;
1312
+ return this.mintAlias('mailRule', created.id);
1573
1313
  }
1574
1314
  /**
1575
1315
  * Deletes an inbox mail rule.
1576
1316
  */
1577
1317
  async deleteMailRuleAsync(ruleId) {
1578
- const graphId = this.idCache.rules.get(ruleId);
1579
- if (graphId == null)
1580
- throw new Error(`Rule ID ${ruleId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1318
+ const graphId = this.toGraphId(ruleId, 'mailRule');
1581
1319
  await this.client.deleteMailRule(graphId);
1582
- this.idCache.rules.delete(ruleId);
1583
1320
  }
1584
1321
  // ===========================================================================
1585
1322
  // Automatic Replies (Out of Office)
@@ -1659,10 +1396,8 @@ export class GraphRepository {
1659
1396
  const categories = await this.client.listMasterCategories();
1660
1397
  return categories.map((cat) => {
1661
1398
  const graphId = cat.id;
1662
- const numericId = hashStringToNumber(graphId);
1663
- this.idCache.categories.set(numericId, graphId);
1664
1399
  return {
1665
- id: numericId,
1400
+ id: this.mintAlias('category', graphId),
1666
1401
  name: cat.displayName ?? '',
1667
1402
  color: cat.color ?? 'none',
1668
1403
  };
@@ -1673,20 +1408,14 @@ export class GraphRepository {
1673
1408
  */
1674
1409
  async createCategoryAsync(name, color) {
1675
1410
  const created = await this.client.createMasterCategory(name, color);
1676
- const graphId = created.id;
1677
- const numericId = hashStringToNumber(graphId);
1678
- this.idCache.categories.set(numericId, graphId);
1679
- return numericId;
1411
+ return this.mintAlias('category', created.id);
1680
1412
  }
1681
1413
  /**
1682
1414
  * Deletes a master category.
1683
1415
  */
1684
1416
  async deleteCategoryAsync(categoryId) {
1685
- const graphId = this.idCache.categories.get(categoryId);
1686
- if (graphId == null)
1687
- throw new Error(`Category ID ${categoryId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1417
+ const graphId = this.toGraphId(categoryId, 'category');
1688
1418
  await this.client.deleteMasterCategory(graphId);
1689
- this.idCache.categories.delete(categoryId);
1690
1419
  }
1691
1420
  // ===========================================================================
1692
1421
  // Focused Inbox Overrides
@@ -1698,10 +1427,8 @@ export class GraphRepository {
1698
1427
  const overrides = await this.client.listFocusedOverrides();
1699
1428
  return overrides.map((o) => {
1700
1429
  const graphId = o.id;
1701
- const numericId = hashStringToNumber(graphId);
1702
- this.idCache.focusedOverrides.set(numericId, graphId);
1703
1430
  return {
1704
- id: numericId,
1431
+ id: this.mintAlias('focusedOverride', graphId),
1705
1432
  senderAddress: o.senderEmailAddress?.address ?? '',
1706
1433
  classifyAs: o.classifyAs ?? '',
1707
1434
  };
@@ -1712,20 +1439,14 @@ export class GraphRepository {
1712
1439
  */
1713
1440
  async createFocusedOverrideAsync(senderAddress, classifyAs) {
1714
1441
  const created = await this.client.createFocusedOverride(senderAddress, classifyAs);
1715
- const graphId = created.id;
1716
- const numericId = hashStringToNumber(graphId);
1717
- this.idCache.focusedOverrides.set(numericId, graphId);
1718
- return numericId;
1442
+ return this.mintAlias('focusedOverride', created.id);
1719
1443
  }
1720
1444
  /**
1721
1445
  * Deletes a focused inbox override.
1722
1446
  */
1723
1447
  async deleteFocusedOverrideAsync(overrideId) {
1724
- const graphId = this.idCache.focusedOverrides.get(overrideId);
1725
- if (graphId == null)
1726
- throw new Error(`Focused override ID ${overrideId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1448
+ const graphId = this.toGraphId(overrideId, 'focusedOverride');
1727
1449
  await this.client.deleteFocusedOverride(graphId);
1728
- this.idCache.focusedOverrides.delete(overrideId);
1729
1450
  }
1730
1451
  // ===========================================================================
1731
1452
  // Message Headers & MIME
@@ -1734,21 +1455,17 @@ export class GraphRepository {
1734
1455
  * Gets internet message headers for an email.
1735
1456
  */
1736
1457
  async getMessageHeadersAsync(emailId) {
1737
- const graphId = this.idCache.messages.get(emailId);
1738
- if (graphId == null)
1739
- throw new Error(`Email ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1458
+ const graphId = this.toGraphId(emailId, 'message');
1740
1459
  return await this.client.getMessageHeaders(graphId);
1741
1460
  }
1742
1461
  /**
1743
1462
  * Gets the MIME content of a message and saves it as an .eml file.
1744
1463
  */
1745
1464
  async getMessageMimeAsync(emailId) {
1746
- const graphId = this.idCache.messages.get(emailId);
1747
- if (graphId == null)
1748
- throw new Error(`Email ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1465
+ const graphId = this.toGraphId(emailId, 'message');
1749
1466
  const mime = await this.client.getMessageMime(graphId);
1750
1467
  const downloadDir = getDownloadDir();
1751
- const filePath = path.join(downloadDir, `email-${emailId}.eml`);
1468
+ const filePath = path.join(downloadDir, `email-${createHash('sha1').update(graphId).digest('hex').slice(0, 16)}.eml`);
1752
1469
  fs.writeFileSync(filePath, mime, 'utf-8');
1753
1470
  return { filePath };
1754
1471
  }
@@ -1779,15 +1496,16 @@ export class GraphRepository {
1779
1496
  // ===========================================================================
1780
1497
  /**
1781
1498
  * Lists all calendar groups.
1499
+ *
1500
+ * Orphan entity: no Graph URL takes a calendar-group id as a path segment,
1501
+ * so this returns the raw Graph id string rather than minting a token.
1782
1502
  */
1783
1503
  async listCalendarGroupsAsync() {
1784
1504
  const groups = await this.client.listCalendarGroups();
1785
1505
  return groups.map((group) => {
1786
1506
  const graphId = group.id;
1787
- const numericId = hashStringToNumber(graphId);
1788
- this.idCache.calendarGroups.set(numericId, graphId);
1789
1507
  return {
1790
- id: numericId,
1508
+ id: graphId,
1791
1509
  name: group.name ?? '',
1792
1510
  classId: group.classId?.toString() ?? '',
1793
1511
  };
@@ -1798,10 +1516,7 @@ export class GraphRepository {
1798
1516
  */
1799
1517
  async createCalendarGroupAsync(name) {
1800
1518
  const created = await this.client.createCalendarGroup(name);
1801
- const graphId = created.id;
1802
- const numericId = hashStringToNumber(graphId);
1803
- this.idCache.calendarGroups.set(numericId, graphId);
1804
- return numericId;
1519
+ return created.id;
1805
1520
  }
1806
1521
  // ===========================================================================
1807
1522
  // Calendar Permissions
@@ -1810,17 +1525,12 @@ export class GraphRepository {
1810
1525
  * Lists all permissions for a calendar.
1811
1526
  */
1812
1527
  async listCalendarPermissionsAsync(calendarId) {
1813
- const graphCalendarId = this.idCache.calendars.get(calendarId);
1814
- if (graphCalendarId == null) {
1815
- throw new Error(`Calendar ID ${calendarId} not found in cache. Please call list_calendars first.`);
1816
- }
1528
+ const graphCalendarId = this.toGraphId(calendarId, 'folder');
1817
1529
  const permissions = await this.client.listCalendarPermissions(graphCalendarId);
1818
1530
  return permissions.map((perm) => {
1819
1531
  const graphPermId = perm.id;
1820
- const numericId = hashStringToNumber(graphPermId);
1821
- this.idCache.calendarPermissions.set(numericId, { calendarId: graphCalendarId, permissionId: graphPermId });
1822
1532
  return {
1823
- id: numericId,
1533
+ id: this.mintAliasComposite('calendarPermission', { calendarId: graphCalendarId, permissionId: graphPermId }),
1824
1534
  emailAddress: perm.emailAddress?.address ?? '',
1825
1535
  role: perm.role ?? 'none',
1826
1536
  isRemovable: perm.isRemovable ?? false,
@@ -1832,10 +1542,7 @@ export class GraphRepository {
1832
1542
  * Creates a calendar permission (shares a calendar with someone).
1833
1543
  */
1834
1544
  async createCalendarPermissionAsync(calendarId, email, role) {
1835
- const graphCalendarId = this.idCache.calendars.get(calendarId);
1836
- if (graphCalendarId == null) {
1837
- throw new Error(`Calendar ID ${calendarId} not found in cache. Please call list_calendars first.`);
1838
- }
1545
+ const graphCalendarId = this.toGraphId(calendarId, 'folder');
1839
1546
  const permission = await this.client.createCalendarPermission(graphCalendarId, {
1840
1547
  emailAddress: {
1841
1548
  address: email,
@@ -1843,21 +1550,14 @@ export class GraphRepository {
1843
1550
  },
1844
1551
  role,
1845
1552
  });
1846
- const graphPermId = permission.id;
1847
- const numericId = hashStringToNumber(graphPermId);
1848
- this.idCache.calendarPermissions.set(numericId, { calendarId: graphCalendarId, permissionId: graphPermId });
1849
- return numericId;
1553
+ return this.mintAliasComposite('calendarPermission', { calendarId: graphCalendarId, permissionId: permission.id });
1850
1554
  }
1851
1555
  /**
1852
1556
  * Deletes a calendar permission.
1853
1557
  */
1854
1558
  async deleteCalendarPermissionAsync(permissionId) {
1855
- const cached = this.idCache.calendarPermissions.get(permissionId);
1856
- if (cached == null) {
1857
- throw new Error(`Calendar permission ID ${permissionId} not found in cache. Please call list_calendar_permissions first.`);
1858
- }
1859
- await this.client.deleteCalendarPermission(cached.calendarId, cached.permissionId);
1860
- this.idCache.calendarPermissions.delete(permissionId);
1559
+ const { calendarId, permissionId: permId } = this.toGraphParts(permissionId, 'calendarPermission', ['calendarId', 'permissionId']);
1560
+ await this.client.deleteCalendarPermission(calendarId, permId);
1861
1561
  }
1862
1562
  // ===========================================================================
1863
1563
  // Room Lists & Rooms
@@ -1892,80 +1592,61 @@ export class GraphRepository {
1892
1592
  const teams = await this.client.listJoinedTeams();
1893
1593
  return teams.map((team) => {
1894
1594
  const graphId = team.id;
1895
- const numericId = hashStringToNumber(graphId);
1896
- this.idCache.teams.set(numericId, graphId);
1897
- return { id: numericId, name: team.displayName ?? '', description: team.description ?? '' };
1595
+ return { id: this.mintAlias('team', graphId), name: team.displayName ?? '', description: team.description ?? '' };
1898
1596
  });
1899
1597
  }
1900
1598
  /**
1901
1599
  * Lists all channels in a team with cached numeric IDs.
1902
1600
  */
1903
1601
  async listChannelsAsync(teamId) {
1904
- const graphTeamId = this.idCache.teams.get(teamId);
1905
- if (graphTeamId == null)
1906
- throw new Error(`Team ID ${teamId} not found in cache. Try listing teams first.`);
1602
+ const graphTeamId = await this.resolveTeamId(teamId);
1907
1603
  const channels = await this.client.listChannels(graphTeamId);
1908
1604
  return channels.map((ch) => {
1909
1605
  const graphId = ch.id;
1910
- const numericId = hashStringToNumber(graphId);
1911
- this.idCache.channels.set(numericId, { teamId: graphTeamId, channelId: graphId });
1912
- return { id: numericId, name: ch.displayName ?? '', description: ch.description ?? '', membershipType: ch.membershipType ?? 'standard' };
1606
+ const id = this.mintAliasComposite('channel', { teamId: graphTeamId, channelId: graphId });
1607
+ return { id, name: ch.displayName ?? '', description: ch.description ?? '', membershipType: ch.membershipType ?? 'standard' };
1913
1608
  });
1914
1609
  }
1915
1610
  /**
1916
- * Gets a specific channel by cached numeric ID.
1611
+ * Gets a specific channel by durable cn_ token.
1917
1612
  */
1918
1613
  async getChannelAsync(channelId) {
1919
- const cached = this.idCache.channels.get(channelId);
1920
- if (cached == null)
1921
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
1922
- const ch = await this.client.getChannel(cached.teamId, cached.channelId);
1923
- return { id: channelId, name: ch.displayName ?? '', description: ch.description ?? '', membershipType: ch.membershipType ?? 'standard', webUrl: ch.webUrl ?? '' };
1614
+ const { teamId, channelId: graphChannelId } = this.toGraphParts(channelId, 'channel', ['teamId', 'channelId']);
1615
+ const ch = await this.client.getChannel(teamId, graphChannelId);
1616
+ return { id: String(channelId), name: ch.displayName ?? '', description: ch.description ?? '', membershipType: ch.membershipType ?? 'standard', webUrl: ch.webUrl ?? '' };
1924
1617
  }
1925
1618
  /**
1926
1619
  * Creates a new channel in a team.
1927
1620
  */
1928
1621
  async createChannelAsync(teamId, name, description) {
1929
- const graphTeamId = this.idCache.teams.get(teamId);
1930
- if (graphTeamId == null)
1931
- throw new Error(`Team ID ${teamId} not found in cache. Try listing teams first.`);
1622
+ const graphTeamId = await this.resolveTeamId(teamId);
1932
1623
  const ch = await this.client.createChannel(graphTeamId, name, description);
1933
- const graphId = ch.id;
1934
- const numericId = hashStringToNumber(graphId);
1935
- this.idCache.channels.set(numericId, { teamId: graphTeamId, channelId: graphId });
1936
- return numericId;
1624
+ return this.mintAliasComposite('channel', { teamId: graphTeamId, channelId: ch.id });
1937
1625
  }
1938
1626
  /**
1939
1627
  * Updates a channel's properties.
1940
1628
  */
1941
1629
  async updateChannelAsync(channelId, updates) {
1942
- const cached = this.idCache.channels.get(channelId);
1943
- if (cached == null)
1944
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
1630
+ const { teamId, channelId: graphChannelId } = this.toGraphParts(channelId, 'channel', ['teamId', 'channelId']);
1945
1631
  const graphUpdates = {};
1946
1632
  if (updates.name != null)
1947
1633
  graphUpdates['displayName'] = updates.name;
1948
1634
  if (updates.description != null)
1949
1635
  graphUpdates['description'] = updates.description;
1950
- await this.client.updateChannel(cached.teamId, cached.channelId, graphUpdates);
1636
+ await this.client.updateChannel(teamId, graphChannelId, graphUpdates);
1951
1637
  }
1952
1638
  /**
1953
1639
  * Deletes a channel.
1954
1640
  */
1955
1641
  async deleteChannelAsync(channelId) {
1956
- const cached = this.idCache.channels.get(channelId);
1957
- if (cached == null)
1958
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
1959
- await this.client.deleteChannel(cached.teamId, cached.channelId);
1960
- this.idCache.channels.delete(channelId);
1642
+ const { teamId, channelId: graphChannelId } = this.toGraphParts(channelId, 'channel', ['teamId', 'channelId']);
1643
+ await this.client.deleteChannel(teamId, graphChannelId);
1961
1644
  }
1962
1645
  /**
1963
1646
  * Lists members of a team.
1964
1647
  */
1965
1648
  async listTeamMembersAsync(teamId) {
1966
- const graphTeamId = this.idCache.teams.get(teamId);
1967
- if (graphTeamId == null)
1968
- throw new Error(`Team ID ${teamId} not found in cache. Try listing teams first.`);
1649
+ const graphTeamId = await this.resolveTeamId(teamId);
1969
1650
  const members = await this.client.listTeamMembers(graphTeamId);
1970
1651
  return members.map((m) => ({
1971
1652
  id: m.id ?? '',
@@ -1981,16 +1662,12 @@ export class GraphRepository {
1981
1662
  * Lists recent messages in a channel.
1982
1663
  */
1983
1664
  async listChannelMessagesAsync(channelId, limit = 25) {
1984
- const cached = this.idCache.channels.get(channelId);
1985
- if (cached == null)
1986
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
1987
- const messages = await this.client.listChannelMessages(cached.teamId, cached.channelId, limit);
1665
+ const { teamId, channelId: graphChannelId } = this.toGraphParts(channelId, 'channel', ['teamId', 'channelId']);
1666
+ const messages = await this.client.listChannelMessages(teamId, graphChannelId, limit);
1988
1667
  return messages.map((msg) => {
1989
1668
  const graphId = msg.id;
1990
- const numericId = hashStringToNumber(graphId);
1991
- this.idCache.channelMessages.set(numericId, { teamId: cached.teamId, channelId: cached.channelId, messageId: graphId });
1992
1669
  return {
1993
- id: numericId,
1670
+ id: this.mintAliasComposite('channelMessage', { teamId, channelId: graphChannelId, messageId: graphId }),
1994
1671
  senderName: msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? '',
1995
1672
  senderEmail: msg.from?.user?.email ?? '',
1996
1673
  bodyPreview: msg.body?.content?.substring(0, 200) ?? '',
@@ -2004,18 +1681,21 @@ export class GraphRepository {
2004
1681
  * Gets a specific channel message with its replies.
2005
1682
  */
2006
1683
  async getChannelMessageAsync(messageId) {
2007
- const cached = this.idCache.channelMessages.get(messageId);
2008
- if (cached == null)
2009
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
1684
+ const { teamId, channelId, messageId: graphMessageId } = this.toGraphParts(messageId, 'channelMessage', ['teamId', 'channelId', 'messageId']);
2010
1685
  const [msg, repliesRaw] = await Promise.all([
2011
- this.client.getChannelMessage(cached.teamId, cached.channelId, cached.messageId),
2012
- this.client.listChannelMessageReplies(cached.teamId, cached.channelId, cached.messageId),
1686
+ this.client.getChannelMessage(teamId, channelId, graphMessageId),
1687
+ this.client.listChannelMessageReplies(teamId, channelId, graphMessageId),
2013
1688
  ]);
2014
1689
  const replies = repliesRaw.map((r) => {
2015
1690
  const rGraphId = r.id;
2016
- const rNumericId = hashStringToNumber(rGraphId);
1691
+ // Reply ids are display-only: a reply is a child entity addressed at
1692
+ // /messages/{parentId}/replies/{replyId}, but this xm_ token (like the
1693
+ // pre-migration numeric id before it) carries only {teamId, channelId,
1694
+ // messageId}, so feeding it back to a message op resolves then 404s. No
1695
+ // tool consumes a reply id — react/reply take the parent message id.
1696
+ // Reply-level operations are tracked in issue #50.
2017
1697
  return {
2018
- id: rNumericId,
1698
+ id: this.mintAliasComposite('channelMessage', { teamId, channelId, messageId: rGraphId }),
2019
1699
  senderName: r.from?.user?.displayName ?? r.from?.application?.displayName ?? '',
2020
1700
  senderEmail: r.from?.user?.email ?? '',
2021
1701
  bodyContent: r.body?.content ?? '',
@@ -2024,7 +1704,7 @@ export class GraphRepository {
2024
1704
  };
2025
1705
  });
2026
1706
  return {
2027
- id: messageId,
1707
+ id: String(messageId),
2028
1708
  senderName: msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? '',
2029
1709
  senderEmail: msg.from?.user?.email ?? '',
2030
1710
  bodyContent: msg.body?.content ?? '',
@@ -2037,27 +1717,18 @@ export class GraphRepository {
2037
1717
  * Sends a new message to a channel.
2038
1718
  */
2039
1719
  async sendChannelMessageAsync(channelId, body, contentType = 'html') {
2040
- const cached = this.idCache.channels.get(channelId);
2041
- if (cached == null)
2042
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
2043
- const msg = await this.client.sendChannelMessage(cached.teamId, cached.channelId, body, contentType);
1720
+ const { teamId, channelId: graphChannelId } = this.toGraphParts(channelId, 'channel', ['teamId', 'channelId']);
1721
+ const msg = await this.client.sendChannelMessage(teamId, graphChannelId, body, contentType);
2044
1722
  const graphId = msg.id;
2045
- const numericId = hashStringToNumber(graphId);
2046
- this.idCache.channelMessages.set(numericId, { teamId: cached.teamId, channelId: cached.channelId, messageId: graphId });
2047
- return numericId;
1723
+ return this.mintAliasComposite('channelMessage', { teamId, channelId: graphChannelId, messageId: graphId });
2048
1724
  }
2049
1725
  /**
2050
1726
  * Replies to a channel message.
2051
1727
  */
2052
1728
  async replyToChannelMessageAsync(messageId, body, contentType = 'html') {
2053
- const cached = this.idCache.channelMessages.get(messageId);
2054
- if (cached == null)
2055
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2056
- const reply = await this.client.replyToChannelMessage(cached.teamId, cached.channelId, cached.messageId, body, contentType);
2057
- const graphId = reply.id;
2058
- const numericId = hashStringToNumber(graphId);
2059
- this.idCache.channelMessages.set(numericId, { teamId: cached.teamId, channelId: cached.channelId, messageId: graphId });
2060
- return numericId;
1729
+ const { teamId, channelId, messageId: graphMessageId } = this.toGraphParts(messageId, 'channelMessage', ['teamId', 'channelId', 'messageId']);
1730
+ const reply = await this.client.replyToChannelMessage(teamId, channelId, graphMessageId, body, contentType);
1731
+ return this.mintAliasComposite('channelMessage', { teamId, channelId, messageId: reply.id });
2061
1732
  }
2062
1733
  // ===========================================================================
2063
1734
  // Chats
@@ -2066,14 +1737,12 @@ export class GraphRepository {
2066
1737
  const chats = await this.client.listChats(limit);
2067
1738
  return chats.map((chat) => {
2068
1739
  const graphId = chat.id;
2069
- const numericId = hashStringToNumber(graphId);
2070
- this.idCache.chats.set(numericId, graphId);
2071
1740
  const chatRecord = chat;
2072
1741
  const preview = chatRecord.lastMessagePreview;
2073
1742
  const previewBody = preview?.body;
2074
1743
  const previewContent = previewBody?.content?.substring(0, 200) ?? '';
2075
1744
  return {
2076
- id: numericId,
1745
+ id: this.mintAlias('chat', graphId),
2077
1746
  topic: chat.topic ?? '',
2078
1747
  chatType: chat.chatType ?? 'oneOnOne',
2079
1748
  lastMessagePreview: previewContent,
@@ -2082,12 +1751,10 @@ export class GraphRepository {
2082
1751
  });
2083
1752
  }
2084
1753
  async getChatAsync(chatId) {
2085
- const graphId = this.idCache.chats.get(chatId);
2086
- if (graphId == null)
2087
- throw new Error(`Chat ID ${chatId} not found in cache. Try listing chats first.`);
1754
+ const graphId = await this.resolveChatId(chatId);
2088
1755
  const chat = await this.client.getChat(graphId);
2089
1756
  return {
2090
- id: chatId,
1757
+ id: String(chatId),
2091
1758
  topic: chat.topic ?? '',
2092
1759
  chatType: chat.chatType ?? 'oneOnOne',
2093
1760
  createdDateTime: chat.createdDateTime ?? '',
@@ -2095,16 +1762,12 @@ export class GraphRepository {
2095
1762
  };
2096
1763
  }
2097
1764
  async listChatMessagesAsync(chatId, limit = 25) {
2098
- const graphChatId = this.idCache.chats.get(chatId);
2099
- if (graphChatId == null)
2100
- throw new Error(`Chat ID ${chatId} not found in cache. Try listing chats first.`);
1765
+ const graphChatId = await this.resolveChatId(chatId);
2101
1766
  const messages = await this.client.listChatMessages(graphChatId, limit);
2102
1767
  return messages.map((msg) => {
2103
1768
  const graphId = msg.id;
2104
- const numericId = hashStringToNumber(graphId);
2105
- this.idCache.chatMessages.set(numericId, { chatId: graphChatId, messageId: graphId });
2106
1769
  return {
2107
- id: numericId,
1770
+ id: this.mintAliasComposite('chatMessage', { chatId: graphChatId, messageId: graphId }),
2108
1771
  senderName: msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? '',
2109
1772
  senderEmail: msg.from?.user?.email ?? '',
2110
1773
  bodyPreview: msg.body?.content?.substring(0, 200) ?? '',
@@ -2115,14 +1778,10 @@ export class GraphRepository {
2115
1778
  });
2116
1779
  }
2117
1780
  async sendChatMessageAsync(chatId, body, contentType = 'html') {
2118
- const graphChatId = this.idCache.chats.get(chatId);
2119
- if (graphChatId == null)
2120
- throw new Error(`Chat ID ${chatId} not found in cache. Try listing chats first.`);
1781
+ const graphChatId = await this.resolveChatId(chatId);
2121
1782
  const msg = await this.client.sendChatMessage(graphChatId, body, contentType);
2122
1783
  const graphId = msg.id;
2123
- const numericId = hashStringToNumber(graphId);
2124
- this.idCache.chatMessages.set(numericId, { chatId: graphChatId, messageId: graphId });
2125
- return numericId;
1784
+ return this.mintAliasComposite('chatMessage', { chatId: graphChatId, messageId: graphId });
2126
1785
  }
2127
1786
  // ===========================================================================
2128
1787
  // Message Reactions
@@ -2141,52 +1800,38 @@ export class GraphRepository {
2141
1800
  });
2142
1801
  };
2143
1802
  if (messageType === 'channel') {
2144
- const cached = this.idCache.channelMessages.get(messageId);
2145
- if (cached == null)
2146
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2147
- const msg = await this.client.getChannelMessage(cached.teamId, cached.channelId, cached.messageId);
1803
+ const { teamId, channelId, messageId: graphMessageId } = this.toGraphParts(messageId, 'channelMessage', ['teamId', 'channelId', 'messageId']);
1804
+ const msg = await this.client.getChannelMessage(teamId, channelId, graphMessageId);
2148
1805
  return mapReactions(msg);
2149
1806
  }
2150
1807
  else {
2151
- const cached = this.idCache.chatMessages.get(messageId);
2152
- if (cached == null)
2153
- throw new Error(`Message ID ${messageId} not found in cache. Try listing chat messages first.`);
2154
- const msg = await this.client.getChatMessage(cached.chatId, cached.messageId);
1808
+ const { chatId, messageId: graphMessageId } = this.toGraphParts(messageId, 'chatMessage', ['chatId', 'messageId']);
1809
+ const msg = await this.client.getChatMessage(chatId, graphMessageId);
2155
1810
  return mapReactions(msg);
2156
1811
  }
2157
1812
  }
2158
1813
  async addMessageReactionAsync(messageId, messageType, reactionType) {
2159
1814
  if (messageType === 'channel') {
2160
- const cached = this.idCache.channelMessages.get(messageId);
2161
- if (cached == null)
2162
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2163
- await this.client.setChannelMessageReaction(cached.teamId, cached.channelId, cached.messageId, reactionType);
1815
+ const { teamId, channelId, messageId: graphMessageId } = this.toGraphParts(messageId, 'channelMessage', ['teamId', 'channelId', 'messageId']);
1816
+ await this.client.setChannelMessageReaction(teamId, channelId, graphMessageId, reactionType);
2164
1817
  }
2165
1818
  else {
2166
- const cached = this.idCache.chatMessages.get(messageId);
2167
- if (cached == null)
2168
- throw new Error(`Message ID ${messageId} not found in cache. Try listing chat messages first.`);
2169
- await this.client.setChatMessageReaction(cached.chatId, cached.messageId, reactionType);
1819
+ const { chatId, messageId: graphMessageId } = this.toGraphParts(messageId, 'chatMessage', ['chatId', 'messageId']);
1820
+ await this.client.setChatMessageReaction(chatId, graphMessageId, reactionType);
2170
1821
  }
2171
1822
  }
2172
1823
  async removeMessageReactionAsync(messageId, messageType, reactionType) {
2173
1824
  if (messageType === 'channel') {
2174
- const cached = this.idCache.channelMessages.get(messageId);
2175
- if (cached == null)
2176
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2177
- await this.client.unsetChannelMessageReaction(cached.teamId, cached.channelId, cached.messageId, reactionType);
1825
+ const { teamId, channelId, messageId: graphMessageId } = this.toGraphParts(messageId, 'channelMessage', ['teamId', 'channelId', 'messageId']);
1826
+ await this.client.unsetChannelMessageReaction(teamId, channelId, graphMessageId, reactionType);
2178
1827
  }
2179
1828
  else {
2180
- const cached = this.idCache.chatMessages.get(messageId);
2181
- if (cached == null)
2182
- throw new Error(`Message ID ${messageId} not found in cache. Try listing chat messages first.`);
2183
- await this.client.unsetChatMessageReaction(cached.chatId, cached.messageId, reactionType);
1829
+ const { chatId, messageId: graphMessageId } = this.toGraphParts(messageId, 'chatMessage', ['chatId', 'messageId']);
1830
+ await this.client.unsetChatMessageReaction(chatId, graphMessageId, reactionType);
2184
1831
  }
2185
1832
  }
2186
1833
  async listChatMembersAsync(chatId) {
2187
- const graphChatId = this.idCache.chats.get(chatId);
2188
- if (graphChatId == null)
2189
- throw new Error(`Chat ID ${chatId} not found in cache. Try listing chats first.`);
1834
+ const graphChatId = await this.resolveChatId(chatId);
2190
1835
  const members = await this.client.listChatMembers(graphChatId);
2191
1836
  return members.map((m) => ({
2192
1837
  displayName: m.displayName ?? '',
@@ -2198,17 +1843,14 @@ export class GraphRepository {
2198
1843
  // Planner Plans
2199
1844
  // ===========================================================================
2200
1845
  /**
2201
- * Lists all plans the current user has.
1846
+ * Lists all plans the current user has, minting durable pl_ tokens.
2202
1847
  */
2203
1848
  async listPlansAsync() {
2204
1849
  const plans = await this.client.listPlans();
2205
1850
  return plans.map((plan) => {
2206
1851
  const graphId = plan.id;
2207
- const numericId = hashStringToNumber(graphId);
2208
- const etag = plan['@odata.etag'] ?? '';
2209
- this.idCache.plans.set(numericId, { planId: graphId, etag });
2210
1852
  return {
2211
- id: numericId,
1853
+ id: this.mintAlias('plan', graphId),
2212
1854
  title: plan.title ?? '',
2213
1855
  owner: plan.owner ?? '',
2214
1856
  createdDateTime: plan.createdDateTime ?? '',
@@ -2216,21 +1858,17 @@ export class GraphRepository {
2216
1858
  });
2217
1859
  }
2218
1860
  /**
2219
- * Gets a specific plan by cached numeric ID.
1861
+ * Gets a specific plan.
2220
1862
  */
2221
1863
  async getPlanAsync(planId) {
2222
- const cached = this.idCache.plans.get(planId);
2223
- if (cached == null)
2224
- throw new Error(`Plan ID ${planId} not found in cache. Try listing plans first.`);
2225
- const plan = await this.client.getPlan(cached.planId);
2226
- const etag = plan['@odata.etag'] ?? '';
2227
- this.idCache.plans.set(planId, { planId: cached.planId, etag });
1864
+ const graphPlanId = await this.resolvePlanId(planId);
1865
+ const plan = await this.client.getPlan(graphPlanId);
2228
1866
  return {
2229
- id: planId,
1867
+ id: String(planId),
2230
1868
  title: plan.title ?? '',
2231
1869
  owner: plan.owner ?? '',
2232
1870
  createdDateTime: plan.createdDateTime ?? '',
2233
- etag,
1871
+ etag: this.extractEtag(plan),
2234
1872
  };
2235
1873
  }
2236
1874
  /**
@@ -2238,46 +1876,34 @@ export class GraphRepository {
2238
1876
  */
2239
1877
  async createPlanAsync(title, groupId) {
2240
1878
  const plan = await this.client.createPlan(title, groupId);
2241
- const graphId = plan.id;
2242
- const numericId = hashStringToNumber(graphId);
2243
- const etag = plan['@odata.etag'] ?? '';
2244
- this.idCache.plans.set(numericId, { planId: graphId, etag });
2245
- return numericId;
1879
+ return this.mintAlias('plan', plan.id);
2246
1880
  }
2247
1881
  /**
2248
- * Updates a plan (requires cached ETag).
1882
+ * Updates a plan (U5b-5: fetches a fresh etag immediately before the write —
1883
+ * etags are never cached across calls).
2249
1884
  */
2250
1885
  async updatePlanAsync(planId, updates) {
2251
- const cached = this.idCache.plans.get(planId);
2252
- if (cached == null)
2253
- throw new Error(`Plan ID ${planId} not found in cache. Try listing plans first.`);
1886
+ const graphPlanId = await this.resolvePlanId(planId);
2254
1887
  const graphUpdates = {};
2255
1888
  if (updates.title != null)
2256
1889
  graphUpdates['title'] = updates.title;
2257
- const result = await this.client.updatePlan(cached.planId, graphUpdates, cached.etag);
2258
- const newEtag = result['@odata.etag'] ?? cached.etag;
2259
- this.idCache.plans.set(planId, { planId: cached.planId, etag: newEtag });
1890
+ await this.withFreshEtag(async () => this.extractEtag(await this.client.getPlan(graphPlanId)), (etag) => this.client.updatePlan(graphPlanId, graphUpdates, etag));
2260
1891
  }
2261
1892
  // ===========================================================================
2262
1893
  // Planner Buckets
2263
1894
  // ===========================================================================
2264
1895
  /**
2265
- * Lists all buckets in a plan.
1896
+ * Lists all buckets in a plan, minting durable pb_ tokens.
2266
1897
  */
2267
1898
  async listBucketsAsync(planId) {
2268
- const cached = this.idCache.plans.get(planId);
2269
- if (cached == null)
2270
- throw new Error(`Plan ID ${planId} not found in cache. Try listing plans first.`);
2271
- const buckets = await this.client.listBuckets(cached.planId);
1899
+ const graphPlanId = await this.resolvePlanId(planId);
1900
+ const buckets = await this.client.listBuckets(graphPlanId);
2272
1901
  return buckets.map((bucket) => {
2273
1902
  const graphId = bucket.id;
2274
- const numericId = hashStringToNumber(graphId);
2275
- const etag = bucket['@odata.etag'] ?? '';
2276
- this.idCache.plannerBuckets.set(numericId, { planId: cached.planId, bucketId: graphId, etag });
2277
1903
  return {
2278
- id: numericId,
1904
+ id: this.mintAlias('plannerBucket', graphId),
2279
1905
  name: bucket.name ?? '',
2280
- planId,
1906
+ planId: String(planId),
2281
1907
  orderHint: bucket.orderHint ?? '',
2282
1908
  };
2283
1909
  });
@@ -2286,65 +1912,42 @@ export class GraphRepository {
2286
1912
  * Creates a new bucket in a plan.
2287
1913
  */
2288
1914
  async createBucketAsync(planId, name) {
2289
- const cached = this.idCache.plans.get(planId);
2290
- if (cached == null)
2291
- throw new Error(`Plan ID ${planId} not found in cache. Try listing plans first.`);
2292
- const bucket = await this.client.createBucket(cached.planId, name);
2293
- const graphId = bucket.id;
2294
- const numericId = hashStringToNumber(graphId);
2295
- const etag = bucket['@odata.etag'] ?? '';
2296
- this.idCache.plannerBuckets.set(numericId, { planId: cached.planId, bucketId: graphId, etag });
2297
- return numericId;
1915
+ const graphPlanId = await this.resolvePlanId(planId);
1916
+ const bucket = await this.client.createBucket(graphPlanId, name);
1917
+ return this.mintAlias('plannerBucket', bucket.id);
2298
1918
  }
2299
1919
  /**
2300
- * Updates a bucket (requires cached ETag).
1920
+ * Updates a bucket (U5b-5: fetches a fresh etag immediately before the write).
2301
1921
  */
2302
1922
  async updateBucketAsync(bucketId, updates) {
2303
- const cached = this.idCache.plannerBuckets.get(bucketId);
2304
- if (cached == null)
2305
- throw new Error(`Bucket ID ${bucketId} not found in cache. Try listing buckets first.`);
1923
+ const graphBucketId = this.toGraphId(bucketId, 'plannerBucket');
2306
1924
  const graphUpdates = {};
2307
1925
  if (updates.name != null)
2308
1926
  graphUpdates['name'] = updates.name;
2309
- const result = await this.client.updateBucket(cached.bucketId, graphUpdates, cached.etag);
2310
- const newEtag = result['@odata.etag'] ?? cached.etag;
2311
- this.idCache.plannerBuckets.set(bucketId, { planId: cached.planId, bucketId: cached.bucketId, etag: newEtag });
1927
+ await this.withFreshEtag(async () => this.extractEtag(await this.client.getBucket(graphBucketId)), (etag) => this.client.updateBucket(graphBucketId, graphUpdates, etag));
2312
1928
  }
2313
1929
  /**
2314
- * Deletes a bucket (requires cached ETag).
1930
+ * Deletes a bucket (U5b-5: fetches a fresh etag immediately before the write).
2315
1931
  */
2316
1932
  async deleteBucketAsync(bucketId) {
2317
- const cached = this.idCache.plannerBuckets.get(bucketId);
2318
- if (cached == null)
2319
- throw new Error(`Bucket ID ${bucketId} not found in cache. Try listing buckets first.`);
2320
- await this.client.deleteBucket(cached.bucketId, cached.etag);
2321
- this.idCache.plannerBuckets.delete(bucketId);
1933
+ const graphBucketId = this.toGraphId(bucketId, 'plannerBucket');
1934
+ await this.withFreshEtag(async () => this.extractEtag(await this.client.getBucket(graphBucketId)), (etag) => this.client.deleteBucket(graphBucketId, etag));
2322
1935
  }
2323
1936
  // ===========================================================================
2324
1937
  // Planner Tasks
2325
1938
  // ===========================================================================
2326
1939
  /**
2327
- * Lists all tasks in a plan.
1940
+ * Lists all tasks in a plan, minting durable pt_ tokens.
2328
1941
  */
2329
1942
  async listPlannerTasksAsync(planId) {
2330
- const cached = this.idCache.plans.get(planId);
2331
- if (cached == null)
2332
- throw new Error(`Plan ID ${planId} not found in cache. Try listing plans first.`);
2333
- const tasks = await this.client.listPlannerTasks(cached.planId);
1943
+ const graphPlanId = await this.resolvePlanId(planId);
1944
+ const tasks = await this.client.listPlannerTasks(graphPlanId);
2334
1945
  return tasks.map((task) => {
2335
1946
  const graphId = task.id;
2336
- const numericId = hashStringToNumber(graphId);
2337
- const etag = task['@odata.etag'] ?? '';
2338
- this.idCache.plannerTasks.set(numericId, { taskId: graphId, etag });
2339
- // Resolve bucket numeric ID if present
2340
- let bucketNumericId = null;
2341
- if (task.bucketId != null) {
2342
- bucketNumericId = hashStringToNumber(task.bucketId);
2343
- }
2344
1947
  return {
2345
- id: numericId,
1948
+ id: this.mintAlias('plannerTask', graphId),
2346
1949
  title: task.title ?? '',
2347
- bucketId: bucketNumericId,
1950
+ bucketId: task.bucketId != null ? this.mintAlias('plannerBucket', task.bucketId) : null,
2348
1951
  assignees: task.assignments != null ? Object.keys(task.assignments) : [],
2349
1952
  percentComplete: task.percentComplete ?? 0,
2350
1953
  priority: task.priority ?? 5,
@@ -2355,23 +1958,39 @@ export class GraphRepository {
2355
1958
  });
2356
1959
  }
2357
1960
  /**
2358
- * Gets a specific planner task by cached numeric ID.
1961
+ * Lists all Planner tasks assigned to the signed-in user, across every plan
1962
+ * (`GET /me/planner/tasks`). Unlike the plan-scoped list, each task carries its
1963
+ * own `planId`; the plan/bucket/task durable tokens are minted so follow-up
1964
+ * get/update calls resolve without a re-list.
1965
+ */
1966
+ async listMyPlannerTasksAsync() {
1967
+ const tasks = await this.client.listMyPlannerTasks();
1968
+ return tasks.map((task) => {
1969
+ const graphId = task.id;
1970
+ return {
1971
+ id: this.mintAlias('plannerTask', graphId),
1972
+ title: task.title ?? '',
1973
+ planId: task.planId != null ? this.mintAlias('plan', task.planId) : '',
1974
+ bucketId: task.bucketId != null ? this.mintAlias('plannerBucket', task.bucketId) : null,
1975
+ assignees: task.assignments != null ? Object.keys(task.assignments) : [],
1976
+ percentComplete: task.percentComplete ?? 0,
1977
+ priority: task.priority ?? 5,
1978
+ startDateTime: task.startDateTime ?? '',
1979
+ dueDateTime: task.dueDateTime ?? '',
1980
+ createdDateTime: task.createdDateTime ?? '',
1981
+ };
1982
+ });
1983
+ }
1984
+ /**
1985
+ * Gets a specific planner task.
2359
1986
  */
2360
1987
  async getPlannerTaskAsync(taskId) {
2361
- const cached = this.idCache.plannerTasks.get(taskId);
2362
- if (cached == null)
2363
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2364
- const task = await this.client.getPlannerTask(cached.taskId);
2365
- const etag = task['@odata.etag'] ?? '';
2366
- this.idCache.plannerTasks.set(taskId, { taskId: cached.taskId, etag });
2367
- let bucketNumericId = null;
2368
- if (task.bucketId != null) {
2369
- bucketNumericId = hashStringToNumber(task.bucketId);
2370
- }
1988
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
1989
+ const task = await this.client.getPlannerTask(gTaskId);
2371
1990
  return {
2372
- id: taskId,
1991
+ id: String(taskId),
2373
1992
  title: task.title ?? '',
2374
- bucketId: bucketNumericId,
1993
+ bucketId: task.bucketId != null ? this.mintAlias('plannerBucket', task.bucketId) : null,
2375
1994
  assignees: task.assignments != null ? Object.keys(task.assignments) : [],
2376
1995
  percentComplete: task.percentComplete ?? 0,
2377
1996
  priority: task.priority ?? 5,
@@ -2380,22 +1999,17 @@ export class GraphRepository {
2380
1999
  createdDateTime: task.createdDateTime ?? '',
2381
2000
  conversationThreadId: task.conversationThreadId ?? '',
2382
2001
  orderHint: task.orderHint ?? '',
2383
- etag,
2002
+ etag: this.extractEtag(task),
2384
2003
  };
2385
2004
  }
2386
2005
  /**
2387
2006
  * Creates a new planner task.
2388
2007
  */
2389
2008
  async createPlannerTaskAsync(planId, title, bucketId, assignments, priority, startDate, dueDate) {
2390
- const cachedPlan = this.idCache.plans.get(planId);
2391
- if (cachedPlan == null)
2392
- throw new Error(`Plan ID ${planId} not found in cache. Try listing plans first.`);
2393
- const body = { planId: cachedPlan.planId, title };
2009
+ const graphPlanId = await this.resolvePlanId(planId);
2010
+ const body = { planId: graphPlanId, title };
2394
2011
  if (bucketId != null) {
2395
- const cachedBucket = this.idCache.plannerBuckets.get(bucketId);
2396
- if (cachedBucket == null)
2397
- throw new Error(`Bucket ID ${bucketId} not found in cache. Try listing buckets first.`);
2398
- body.bucketId = cachedBucket.bucketId;
2012
+ body.bucketId = this.toGraphId(bucketId, 'plannerBucket');
2399
2013
  }
2400
2014
  if (assignments != null)
2401
2015
  body.assignments = assignments;
@@ -2406,28 +2020,18 @@ export class GraphRepository {
2406
2020
  if (dueDate != null)
2407
2021
  body.dueDateTime = dueDate;
2408
2022
  const task = await this.client.createPlannerTask(body);
2409
- const graphId = task.id;
2410
- const numericId = hashStringToNumber(graphId);
2411
- const etag = task['@odata.etag'] ?? '';
2412
- this.idCache.plannerTasks.set(numericId, { taskId: graphId, etag });
2413
- return numericId;
2023
+ return this.mintAlias('plannerTask', task.id);
2414
2024
  }
2415
2025
  /**
2416
- * Updates a planner task (requires cached ETag).
2026
+ * Updates a planner task (U5b-5: fetches a fresh etag immediately before the write).
2417
2027
  */
2418
2028
  async updatePlannerTaskAsync(taskId, updates) {
2419
- const cached = this.idCache.plannerTasks.get(taskId);
2420
- if (cached == null)
2421
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2029
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
2422
2030
  const graphUpdates = {};
2423
2031
  if (updates.title != null)
2424
2032
  graphUpdates['title'] = updates.title;
2425
- if (updates.bucketId != null) {
2426
- const cachedBucket = this.idCache.plannerBuckets.get(updates.bucketId);
2427
- if (cachedBucket == null)
2428
- throw new Error(`Bucket ID ${updates.bucketId} not found in cache. Try listing buckets first.`);
2429
- graphUpdates['bucketId'] = cachedBucket.bucketId;
2430
- }
2033
+ if (updates.bucketId != null)
2034
+ graphUpdates['bucketId'] = this.toGraphId(updates.bucketId, 'plannerBucket');
2431
2035
  if (updates.percentComplete != null)
2432
2036
  graphUpdates['percentComplete'] = updates.percentComplete;
2433
2037
  if (updates.priority != null)
@@ -2438,39 +2042,31 @@ export class GraphRepository {
2438
2042
  graphUpdates['dueDateTime'] = updates.dueDate;
2439
2043
  if (updates.assignments != null)
2440
2044
  graphUpdates['assignments'] = updates.assignments;
2441
- const result = await this.client.updatePlannerTask(cached.taskId, graphUpdates, cached.etag);
2442
- const newEtag = result['@odata.etag'] ?? cached.etag;
2443
- this.idCache.plannerTasks.set(taskId, { taskId: cached.taskId, etag: newEtag });
2045
+ await this.withFreshEtag(async () => this.extractEtag(await this.client.getPlannerTask(gTaskId)), (etag) => this.client.updatePlannerTask(gTaskId, graphUpdates, etag));
2444
2046
  }
2445
2047
  /**
2446
- * Deletes a planner task (requires cached ETag).
2048
+ * Deletes a planner task (U5b-5: fetches a fresh etag immediately before the write).
2447
2049
  */
2448
2050
  async deletePlannerTaskAsync(taskId) {
2449
- const cached = this.idCache.plannerTasks.get(taskId);
2450
- if (cached == null)
2451
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2452
- await this.client.deletePlannerTask(cached.taskId, cached.etag);
2453
- this.idCache.plannerTasks.delete(taskId);
2051
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
2052
+ await this.withFreshEtag(async () => this.extractEtag(await this.client.getPlannerTask(gTaskId)), (etag) => this.client.deletePlannerTask(gTaskId, etag));
2454
2053
  }
2455
2054
  // ===========================================================================
2456
2055
  // Planner Task Details
2457
2056
  // ===========================================================================
2458
2057
  /**
2459
- * Gets details for a planner task (description, checklist, references).
2058
+ * Gets details for a planner task (description, checklist, references). Task
2059
+ * details piggyback the pt_ task token — they have no id of their own.
2460
2060
  */
2461
2061
  async getPlannerTaskDetailsAsync(taskId) {
2462
- const cached = this.idCache.plannerTasks.get(taskId);
2463
- if (cached == null)
2464
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2465
- const details = await this.client.getPlannerTaskDetails(cached.taskId);
2466
- const etag = details['@odata.etag'] ?? '';
2467
- this.idCache.plannerTaskDetails.set(taskId, { taskId: cached.taskId, etag });
2062
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
2063
+ const details = await this.client.getPlannerTaskDetails(gTaskId);
2468
2064
  return {
2469
- id: taskId,
2065
+ id: String(taskId),
2470
2066
  description: details.description ?? '',
2471
2067
  checklist: details.checklist ?? {},
2472
2068
  references: details.references ?? {},
2473
- etag,
2069
+ etag: this.extractEtag(details),
2474
2070
  };
2475
2071
  }
2476
2072
  /**
@@ -2483,17 +2079,15 @@ export class GraphRepository {
2483
2079
  * have `details` set to `undefined`.
2484
2080
  */
2485
2081
  async listPlannerTasksWithDetailsAsync(planId) {
2486
- // Step 1: List all tasks (populates idCache.plannerTasks)
2082
+ // Step 1: List all tasks (mints pt_ tokens, registered in the alias store)
2487
2083
  const tasks = await this.listPlannerTasksAsync(planId);
2488
2084
  if (tasks.length === 0)
2489
2085
  return [];
2490
2086
  // Step 2: Build batch requests for each task's details
2491
2087
  const batchRequests = tasks.map((task) => {
2492
- const cached = this.idCache.plannerTasks.get(task.id);
2493
- // cached is guaranteed to exist since listPlannerTasksAsync just populated it
2494
- const graphTaskId = cached.taskId;
2088
+ const graphTaskId = this.toGraphId(task.id, 'plannerTask');
2495
2089
  return {
2496
- id: String(task.id),
2090
+ id: task.id,
2497
2091
  method: 'GET',
2498
2092
  url: `/planner/tasks/${graphTaskId}/details`,
2499
2093
  };
@@ -2502,15 +2096,10 @@ export class GraphRepository {
2502
2096
  const batchResults = await this.client.batchRequests(batchRequests);
2503
2097
  // Step 4: Merge details into tasks
2504
2098
  return tasks.map((task) => {
2505
- const result = batchResults.get(String(task.id));
2099
+ const result = batchResults.get(task.id);
2506
2100
  if (result != null && result.status >= 200 && result.status < 300) {
2507
2101
  const detailBody = result.body;
2508
2102
  const etag = result.headers?.ETag ?? result.headers?.etag ?? '';
2509
- // Cache the task detail ETag for later updates
2510
- const cachedTask = this.idCache.plannerTasks.get(task.id);
2511
- if (cachedTask != null) {
2512
- this.idCache.plannerTaskDetails.set(task.id, { taskId: cachedTask.taskId, etag });
2513
- }
2514
2103
  return {
2515
2104
  ...task,
2516
2105
  details: {
@@ -2526,15 +2115,12 @@ export class GraphRepository {
2526
2115
  });
2527
2116
  }
2528
2117
  /**
2529
- * Updates details for a planner task (requires cached ETag from getPlannerTaskDetailsAsync).
2118
+ * Updates details for a planner task (U5b-5: fetches a fresh etag immediately
2119
+ * before the write — the task details etag is independent of the task's own
2120
+ * etag).
2530
2121
  */
2531
2122
  async updatePlannerTaskDetailsAsync(taskId, updates) {
2532
- const cachedTask = this.idCache.plannerTasks.get(taskId);
2533
- if (cachedTask == null)
2534
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2535
- const cachedDetails = this.idCache.plannerTaskDetails.get(taskId);
2536
- if (cachedDetails == null)
2537
- throw new Error(`Task details ETag for task ${taskId} not found in cache. Call get_planner_task_details first.`);
2123
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
2538
2124
  const graphUpdates = {};
2539
2125
  if (updates.description != null)
2540
2126
  graphUpdates['description'] = updates.description;
@@ -2542,9 +2128,7 @@ export class GraphRepository {
2542
2128
  graphUpdates['checklist'] = updates.checklist;
2543
2129
  if (updates.references != null)
2544
2130
  graphUpdates['references'] = updates.references;
2545
- const result = await this.client.updatePlannerTaskDetails(cachedTask.taskId, graphUpdates, cachedDetails.etag);
2546
- const newEtag = result['@odata.etag'] ?? cachedDetails.etag;
2547
- this.idCache.plannerTaskDetails.set(taskId, { taskId: cachedTask.taskId, etag: newEtag });
2131
+ await this.withFreshEtag(async () => this.extractEtag(await this.client.getPlannerTaskDetails(gTaskId)), (etag) => this.client.updatePlannerTaskDetails(gTaskId, graphUpdates, etag));
2548
2132
  }
2549
2133
  // ===========================================================================
2550
2134
  // Planner Visualization Data
@@ -2569,7 +2153,7 @@ export class GraphRepository {
2569
2153
  tasks: tasks.map(t => ({
2570
2154
  id: t.id,
2571
2155
  title: t.title,
2572
- bucketId: t.bucketId ?? 0,
2156
+ bucketId: t.bucketId ?? '',
2573
2157
  percentComplete: t.percentComplete,
2574
2158
  priority: t.priority,
2575
2159
  startDateTime: t.startDateTime || null,
@@ -2585,10 +2169,8 @@ export class GraphRepository {
2585
2169
  const meetings = await this.client.listOnlineMeetings(limit ?? 20);
2586
2170
  return meetings.map((meeting) => {
2587
2171
  const graphId = meeting.id ?? '';
2588
- const numericId = hashStringToNumber(graphId);
2589
- this.idCache.onlineMeetings.set(numericId, graphId);
2590
2172
  return {
2591
- id: numericId,
2173
+ id: this.mintAlias('onlineMeeting', graphId),
2592
2174
  subject: meeting.subject ?? '',
2593
2175
  startDateTime: meeting.startDateTime ?? '',
2594
2176
  endDateTime: meeting.endDateTime ?? '',
@@ -2598,103 +2180,96 @@ export class GraphRepository {
2598
2180
  }
2599
2181
  async getOnlineMeetingAsync(meetingId) {
2600
2182
  const mapMeeting = (m) => ({
2601
- id: meetingId,
2183
+ id: String(meetingId),
2602
2184
  subject: m.subject ?? '',
2603
2185
  startDateTime: m.startDateTime ?? '',
2604
2186
  endDateTime: m.endDateTime ?? '',
2605
2187
  joinUrl: m.joinWebUrl ?? '',
2606
2188
  participants: m.participants ?? null,
2607
2189
  });
2608
- const graphId = this.idCache.onlineMeetings.get(meetingId);
2609
- if (graphId == null) {
2610
- // Try to refresh cache
2611
- await this.listOnlineMeetingsAsync(100);
2612
- const refreshedId = this.idCache.onlineMeetings.get(meetingId);
2613
- if (refreshedId == null)
2614
- return undefined;
2615
- const meeting = await this.client.getOnlineMeeting(refreshedId);
2616
- return mapMeeting(meeting);
2190
+ // om_ tokens resolve from the alias store. On a cold miss (never listed this
2191
+ // session, or a lost store) re-list — listOnlineMeetingsAsync deterministically
2192
+ // re-mints and re-stores the same token — then retry the resolve, matching
2193
+ // the resolveTeamId self-heal pattern. A final miss means "not found" (the
2194
+ // documented contract), not an error.
2195
+ let graphId;
2196
+ try {
2197
+ graphId = this.toGraphId(meetingId, 'onlineMeeting');
2198
+ }
2199
+ catch (e) {
2200
+ if (e instanceof IdUnknownError) {
2201
+ await this.listOnlineMeetingsAsync();
2202
+ try {
2203
+ graphId = this.toGraphId(meetingId, 'onlineMeeting');
2204
+ }
2205
+ catch (e2) {
2206
+ if (e2 instanceof IdUnknownError)
2207
+ return undefined;
2208
+ throw e2;
2209
+ }
2210
+ }
2211
+ else {
2212
+ throw e;
2213
+ }
2617
2214
  }
2618
2215
  const meeting = await this.client.getOnlineMeeting(graphId);
2619
2216
  return mapMeeting(meeting);
2620
2217
  }
2621
2218
  async listMeetingRecordingsAsync(meetingId) {
2622
- const graphMeetingId = this.idCache.onlineMeetings.get(meetingId);
2623
- if (graphMeetingId == null)
2624
- throw new Error(`Meeting ID ${meetingId} not found in cache. Try listing online meetings first.`);
2219
+ const graphMeetingId = this.toGraphId(meetingId, 'onlineMeeting');
2625
2220
  const recordings = await this.client.listMeetingRecordings(graphMeetingId);
2626
2221
  return recordings.map((recording) => {
2627
2222
  const graphId = recording.id ?? '';
2628
- const numericId = hashStringToNumber(graphId);
2629
- this.idCache.recordings.set(numericId, { meetingId: graphMeetingId, recordingId: graphId });
2630
2223
  return {
2631
- id: numericId,
2224
+ id: this.mintAliasComposite('recording', { meetingId: graphMeetingId, recordingId: graphId }),
2632
2225
  createdDateTime: recording.createdDateTime ?? '',
2633
2226
  recordingContentUrl: recording.recordingContentUrl ?? '',
2634
2227
  };
2635
2228
  });
2636
2229
  }
2637
2230
  async downloadMeetingRecordingAsync(recordingId, outputPath) {
2638
- const cached = this.idCache.recordings.get(recordingId);
2639
- if (cached == null)
2640
- throw new Error(`Recording ID ${recordingId} not found in cache. Call list_meeting_recordings first.`);
2641
- const content = await this.client.getMeetingRecordingContent(cached.meetingId, cached.recordingId);
2231
+ const { meetingId, recordingId: recId } = this.toGraphParts(recordingId, 'recording', ['meetingId', 'recordingId']);
2232
+ const content = await this.client.getMeetingRecordingContent(meetingId, recId);
2642
2233
  fs.writeFileSync(outputPath, Buffer.from(content));
2643
2234
  return outputPath;
2644
2235
  }
2645
2236
  async listMeetingTranscriptsAsync(meetingId) {
2646
- const graphMeetingId = this.idCache.onlineMeetings.get(meetingId);
2647
- if (graphMeetingId == null)
2648
- throw new Error(`Meeting ID ${meetingId} not found in cache. Try listing online meetings first.`);
2237
+ const graphMeetingId = this.toGraphId(meetingId, 'onlineMeeting');
2649
2238
  const transcripts = await this.client.listMeetingTranscripts(graphMeetingId);
2650
2239
  return transcripts.map((transcript) => {
2651
2240
  const graphId = transcript.id ?? '';
2652
- const numericId = hashStringToNumber(graphId);
2653
- this.idCache.transcripts.set(numericId, { meetingId: graphMeetingId, transcriptId: graphId });
2654
2241
  return {
2655
- id: numericId,
2242
+ id: this.mintAliasComposite('transcript', { meetingId: graphMeetingId, transcriptId: graphId }),
2656
2243
  createdDateTime: transcript.createdDateTime ?? '',
2657
2244
  contentUrl: transcript.contentUrl ?? '',
2658
2245
  };
2659
2246
  });
2660
2247
  }
2661
2248
  async getMeetingTranscriptContentAsync(transcriptId, format) {
2662
- const cached = this.idCache.transcripts.get(transcriptId);
2663
- if (cached == null)
2664
- throw new Error(`Transcript ID ${transcriptId} not found in cache. Call list_meeting_transcripts first.`);
2665
- return await this.client.getMeetingTranscriptContent(cached.meetingId, cached.transcriptId, format ?? 'text/vtt');
2249
+ const { meetingId, transcriptId: tId } = this.toGraphParts(transcriptId, 'transcript', ['meetingId', 'transcriptId']);
2250
+ return await this.client.getMeetingTranscriptContent(meetingId, tId, format ?? 'text/vtt');
2666
2251
  }
2667
2252
  // ===========================================================================
2668
2253
  // Excel Online (Workbook)
2669
2254
  // ===========================================================================
2670
2255
  async listWorksheetsAsync(fileId) {
2671
- const driveItemId = this.idCache.driveItems.get(fileId);
2672
- if (driveItemId == null)
2673
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2256
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2674
2257
  return await this.client.listWorksheets(driveItemId);
2675
2258
  }
2676
2259
  async getWorksheetRangeAsync(fileId, worksheetName, range) {
2677
- const driveItemId = this.idCache.driveItems.get(fileId);
2678
- if (driveItemId == null)
2679
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2260
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2680
2261
  return await this.client.getWorksheetRange(driveItemId, worksheetName, range);
2681
2262
  }
2682
2263
  async getUsedRangeAsync(fileId, worksheetName) {
2683
- const driveItemId = this.idCache.driveItems.get(fileId);
2684
- if (driveItemId == null)
2685
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2264
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2686
2265
  return await this.client.getUsedRange(driveItemId, worksheetName);
2687
2266
  }
2688
2267
  async updateWorksheetRangeAsync(fileId, worksheetName, range, values) {
2689
- const driveItemId = this.idCache.driveItems.get(fileId);
2690
- if (driveItemId == null)
2691
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2268
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2692
2269
  return await this.client.updateWorksheetRange(driveItemId, worksheetName, range, values);
2693
2270
  }
2694
2271
  async getTableDataAsync(fileId, tableName) {
2695
- const driveItemId = this.idCache.driveItems.get(fileId);
2696
- if (driveItemId == null)
2697
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2272
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2698
2273
  return await this.client.getTableData(driveItemId, tableName);
2699
2274
  }
2700
2275
  // OneDrive
@@ -2703,19 +2278,15 @@ export class GraphRepository {
2703
2278
  * Lists files/folders in a drive folder (or root).
2704
2279
  */
2705
2280
  async listDriveItemsAsync(folderId) {
2706
- let graphFolderId;
2707
- if (folderId != null) {
2708
- graphFolderId = this.idCache.driveItems.get(folderId);
2709
- if (graphFolderId == null)
2710
- throw new Error(`Drive item ID ${folderId} not found in cache. Try listing drive items first.`);
2711
- }
2281
+ const graphFolderId = folderId != null
2282
+ ? this.toGraphId(folderId, 'driveItem')
2283
+ : undefined;
2712
2284
  const items = await this.client.listDriveItems(graphFolderId);
2285
+ // Rows carry self-encoding dr_ tokens (minted here) — no cache.
2713
2286
  return items.map((item) => {
2714
2287
  const itemId = item.id;
2715
- const numericId = hashStringToNumber(itemId);
2716
- this.idCache.driveItems.set(numericId, itemId);
2717
2288
  return {
2718
- id: numericId,
2289
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2719
2290
  name: item.name ?? '',
2720
2291
  size: item.size ?? 0,
2721
2292
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2731,10 +2302,8 @@ export class GraphRepository {
2731
2302
  const items = await this.client.searchDriveItems(query, limit);
2732
2303
  return items.map((item) => {
2733
2304
  const itemId = item.id;
2734
- const numericId = hashStringToNumber(itemId);
2735
- this.idCache.driveItems.set(numericId, itemId);
2736
2305
  return {
2737
- id: numericId,
2306
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2738
2307
  name: item.name ?? '',
2739
2308
  size: item.size ?? 0,
2740
2309
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2747,15 +2316,13 @@ export class GraphRepository {
2747
2316
  * Gets metadata for a specific drive item.
2748
2317
  */
2749
2318
  async getDriveItemAsync(itemId) {
2750
- const graphId = this.idCache.driveItems.get(itemId);
2751
- if (graphId == null)
2752
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2319
+ const graphId = this.toGraphId(itemId, 'driveItem');
2753
2320
  const item = await this.client.getDriveItem(graphId);
2754
2321
  const fileObj = item.file;
2755
2322
  const createdByObj = item.createdBy;
2756
2323
  const createdByUser = createdByObj?.user;
2757
2324
  return {
2758
- id: itemId,
2325
+ id: mintSelfEncoded('driveItem', graphId),
2759
2326
  name: item.name ?? '',
2760
2327
  size: item.size ?? 0,
2761
2328
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2769,9 +2336,7 @@ export class GraphRepository {
2769
2336
  * Downloads a drive item to a local file.
2770
2337
  */
2771
2338
  async downloadFileAsync(itemId, outputPath) {
2772
- const graphId = this.idCache.driveItems.get(itemId);
2773
- if (graphId == null)
2774
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2339
+ const graphId = this.toGraphId(itemId, 'driveItem');
2775
2340
  const content = await this.client.downloadDriveItem(graphId);
2776
2341
  const buffer = Buffer.from(content);
2777
2342
  fs.writeFileSync(outputPath, buffer);
@@ -2784,9 +2349,7 @@ export class GraphRepository {
2784
2349
  const content = fs.readFileSync(localFilePath);
2785
2350
  const result = await this.client.uploadDriveItem(parentPath, fileName, content);
2786
2351
  const resultId = result.id;
2787
- const numericId = hashStringToNumber(resultId);
2788
- this.idCache.driveItems.set(numericId, resultId);
2789
- return numericId;
2352
+ return mintSelfEncoded('driveItem', resultId);
2790
2353
  }
2791
2354
  /**
2792
2355
  * Lists recently accessed drive items.
@@ -2795,10 +2358,8 @@ export class GraphRepository {
2795
2358
  const items = await this.client.listRecentDriveItems();
2796
2359
  return items.map((item) => {
2797
2360
  const itemId = item.id;
2798
- const numericId = hashStringToNumber(itemId);
2799
- this.idCache.driveItems.set(numericId, itemId);
2800
2361
  return {
2801
- id: numericId,
2362
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2802
2363
  name: item.name ?? '',
2803
2364
  size: item.size ?? 0,
2804
2365
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2814,10 +2375,8 @@ export class GraphRepository {
2814
2375
  const items = await this.client.listSharedWithMe();
2815
2376
  return items.map((item) => {
2816
2377
  const itemId = item.id;
2817
- const numericId = hashStringToNumber(itemId);
2818
- this.idCache.driveItems.set(numericId, itemId);
2819
2378
  return {
2820
- id: numericId,
2379
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2821
2380
  name: item.name ?? '',
2822
2381
  size: item.size ?? 0,
2823
2382
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2830,9 +2389,7 @@ export class GraphRepository {
2830
2389
  * Creates a sharing link for a drive item.
2831
2390
  */
2832
2391
  async createSharingLinkAsync(itemId, type, scope) {
2833
- const graphId = this.idCache.driveItems.get(itemId);
2834
- if (graphId == null)
2835
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2392
+ const graphId = this.toGraphId(itemId, 'driveItem');
2836
2393
  const result = await this.client.createSharingLink(graphId, type, scope);
2837
2394
  const link = result.link;
2838
2395
  return {
@@ -2845,17 +2402,14 @@ export class GraphRepository {
2845
2402
  * Deletes a drive item.
2846
2403
  */
2847
2404
  async deleteDriveItemAsync(itemId) {
2848
- const graphId = this.idCache.driveItems.get(itemId);
2849
- if (graphId == null)
2850
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2405
+ const graphId = this.toGraphId(itemId, 'driveItem');
2851
2406
  await this.client.deleteDriveItem(graphId);
2852
- this.idCache.driveItems.delete(itemId);
2853
2407
  }
2854
2408
  // ===========================================================================
2855
2409
  // SharePoint Sites & Document Libraries
2856
2410
  // ===========================================================================
2857
2411
  /**
2858
- * Lists followed SharePoint sites, caching IDs.
2412
+ * Lists followed SharePoint sites, minting durable si_ tokens.
2859
2413
  */
2860
2414
  async listSitesAsync() {
2861
2415
  const sites = await this.client.listFollowedSites();
@@ -2863,10 +2417,8 @@ export class GraphRepository {
2863
2417
  for (const site of sites) {
2864
2418
  const siteId = site.id;
2865
2419
  if (siteId != null) {
2866
- const numericId = hashStringToNumber(siteId);
2867
- this.idCache.sites.set(numericId, siteId);
2868
2420
  result.push({
2869
- id: numericId,
2421
+ id: this.mintAlias('site', siteId),
2870
2422
  name: site.name ?? '',
2871
2423
  webUrl: site.webUrl ?? '',
2872
2424
  displayName: site.displayName ?? '',
@@ -2876,7 +2428,7 @@ export class GraphRepository {
2876
2428
  return result;
2877
2429
  }
2878
2430
  /**
2879
- * Searches SharePoint sites by keyword, caching IDs.
2431
+ * Searches SharePoint sites by keyword, minting durable si_ tokens.
2880
2432
  */
2881
2433
  async searchSitesAsync(query) {
2882
2434
  const sites = await this.client.searchSites(query);
@@ -2884,10 +2436,8 @@ export class GraphRepository {
2884
2436
  for (const site of sites) {
2885
2437
  const siteId = site.id;
2886
2438
  if (siteId != null) {
2887
- const numericId = hashStringToNumber(siteId);
2888
- this.idCache.sites.set(numericId, siteId);
2889
2439
  result.push({
2890
- id: numericId,
2440
+ id: this.mintAlias('site', siteId),
2891
2441
  name: site.name ?? '',
2892
2442
  webUrl: site.webUrl ?? '',
2893
2443
  displayName: site.displayName ?? '',
@@ -2900,12 +2450,10 @@ export class GraphRepository {
2900
2450
  * Gets details for a specific SharePoint site.
2901
2451
  */
2902
2452
  async getSiteAsync(siteId) {
2903
- const graphId = this.idCache.sites.get(siteId);
2904
- if (graphId == null)
2905
- throw new Error(`Site ID ${siteId} not found in cache. Try listing or searching sites first.`);
2453
+ const graphId = this.toGraphId(siteId, 'site');
2906
2454
  const site = await this.client.getSite(graphId);
2907
2455
  return {
2908
- id: siteId,
2456
+ id: String(siteId),
2909
2457
  name: site.name ?? '',
2910
2458
  webUrl: site.webUrl ?? '',
2911
2459
  displayName: site.displayName ?? '',
@@ -2913,21 +2461,17 @@ export class GraphRepository {
2913
2461
  };
2914
2462
  }
2915
2463
  /**
2916
- * Lists document libraries for a SharePoint site, caching IDs.
2464
+ * Lists document libraries for a SharePoint site, minting durable dl_ tokens.
2917
2465
  */
2918
2466
  async listDocumentLibrariesAsync(siteId) {
2919
- const graphSiteId = this.idCache.sites.get(siteId);
2920
- if (graphSiteId == null)
2921
- throw new Error(`Site ID ${siteId} not found in cache. Try listing or searching sites first.`);
2467
+ const graphSiteId = this.toGraphId(siteId, 'site');
2922
2468
  const drives = await this.client.listDocumentLibraries(graphSiteId);
2923
2469
  const result = [];
2924
2470
  for (const drive of drives) {
2925
2471
  const driveId = drive.id;
2926
2472
  if (driveId != null) {
2927
- const numericId = hashStringToNumber(driveId);
2928
- this.idCache.documentLibraries.set(numericId, { siteId: graphSiteId, driveId });
2929
2473
  result.push({
2930
- id: numericId,
2474
+ id: this.mintAliasComposite('documentLibrary', { siteId: graphSiteId, driveId }),
2931
2475
  name: drive.name ?? '',
2932
2476
  webUrl: drive.webUrl ?? '',
2933
2477
  driveType: drive.driveType ?? '',
@@ -2937,28 +2481,21 @@ export class GraphRepository {
2937
2481
  return result;
2938
2482
  }
2939
2483
  /**
2940
- * Lists items in a document library or folder, caching IDs.
2484
+ * Lists items in a document library or folder, minting durable li_ tokens.
2941
2485
  */
2942
2486
  async listLibraryItemsAsync(libraryId, folderId) {
2943
- const libCached = this.idCache.documentLibraries.get(libraryId);
2944
- if (libCached == null)
2945
- throw new Error(`Library ID ${libraryId} not found in cache. Try listing document libraries first.`);
2487
+ const libCached = this.toGraphParts(libraryId, 'documentLibrary', ['siteId', 'driveId']);
2946
2488
  let folderItemId;
2947
2489
  if (folderId != null) {
2948
- const folderCached = this.idCache.libraryDriveItems.get(folderId);
2949
- if (folderCached == null)
2950
- throw new Error(`Folder ID ${folderId} not found in cache. Try listing library items first.`);
2951
- folderItemId = folderCached.itemId;
2490
+ folderItemId = this.toGraphParts(folderId, 'libraryDriveItem', ['driveId', 'itemId']).itemId;
2952
2491
  }
2953
2492
  const items = await this.client.listLibraryItems(libCached.driveId, folderItemId);
2954
2493
  const result = [];
2955
2494
  for (const item of items) {
2956
2495
  const itemGraphId = item.id;
2957
2496
  if (itemGraphId != null) {
2958
- const numericId = hashStringToNumber(itemGraphId);
2959
- this.idCache.libraryDriveItems.set(numericId, { driveId: libCached.driveId, itemId: itemGraphId });
2960
2497
  result.push({
2961
- id: numericId,
2498
+ id: this.mintAliasComposite('libraryDriveItem', { driveId: libCached.driveId, itemId: itemGraphId }),
2962
2499
  name: item.name ?? '',
2963
2500
  size: item.size ?? 0,
2964
2501
  webUrl: item.webUrl ?? '',
@@ -2973,9 +2510,7 @@ export class GraphRepository {
2973
2510
  * Downloads a file from a document library to the specified path.
2974
2511
  */
2975
2512
  async downloadLibraryFileAsync(itemId, outputPath) {
2976
- const cached = this.idCache.libraryDriveItems.get(itemId);
2977
- if (cached == null)
2978
- throw new Error(`Item ID ${itemId} not found in cache. Try listing library items first.`);
2513
+ const cached = this.toGraphParts(itemId, 'libraryDriveItem', ['driveId', 'itemId']);
2979
2514
  const content = await this.client.downloadLibraryFile(cached.driveId, cached.itemId);
2980
2515
  const resolvedPath = path.resolve(outputPath);
2981
2516
  const dir = path.dirname(resolvedPath);
@@ -2984,16 +2519,171 @@ export class GraphRepository {
2984
2519
  return resolvedPath;
2985
2520
  }
2986
2521
  /**
2987
- * Gets the Graph string ID for a folder from the cache.
2522
+ * Resolves a folder id (durable `fd_` token or raw Graph id) to its Graph id.
2988
2523
  */
2989
2524
  getFolderGraphId(folderId) {
2990
- return this.idCache.folders.get(folderId);
2525
+ return this.toGraphId(folderId, 'folder');
2991
2526
  }
2527
+ // ===========================================================================
2528
+ // SharePoint Lists
2529
+ // ===========================================================================
2530
+ /**
2531
+ * Lists the SharePoint lists in a site, minting durable sl_ tokens. Each list
2532
+ * token carries the {siteId, listId} tuple its Graph URL needs.
2533
+ */
2534
+ async listSharePointListsAsync(siteId) {
2535
+ const graphSiteId = this.toGraphId(siteId, 'site');
2536
+ const lists = await this.client.listSharePointLists(graphSiteId);
2537
+ const result = [];
2538
+ for (const list of lists) {
2539
+ const listId = list.id;
2540
+ if (listId != null) {
2541
+ result.push({
2542
+ id: this.mintAliasComposite('sharePointList', { siteId: graphSiteId, listId }),
2543
+ name: list.name ?? '',
2544
+ displayName: list.displayName ?? '',
2545
+ description: list.description ?? '',
2546
+ webUrl: list.webUrl ?? '',
2547
+ });
2548
+ }
2549
+ }
2550
+ return result;
2551
+ }
2552
+ /**
2553
+ * Gets a specific SharePoint list, resolving the sl_ token to its tuple.
2554
+ */
2555
+ async getSharePointListAsync(listId) {
2556
+ const { siteId, listId: graphListId } = this.toGraphParts(listId, 'sharePointList', ['siteId', 'listId']);
2557
+ const list = await this.client.getSharePointList(siteId, graphListId);
2558
+ return {
2559
+ id: String(listId),
2560
+ name: list.name ?? '',
2561
+ displayName: list.displayName ?? '',
2562
+ description: list.description ?? '',
2563
+ webUrl: list.webUrl ?? '',
2564
+ };
2565
+ }
2566
+ /**
2567
+ * Creates a SharePoint list in a site, minting a durable sl_ token.
2568
+ */
2569
+ async createSharePointListAsync(siteId, displayName, description) {
2570
+ const graphSiteId = this.toGraphId(siteId, 'site');
2571
+ const body = {
2572
+ displayName,
2573
+ list: { template: 'genericList' },
2574
+ };
2575
+ if (description != null) {
2576
+ body.description = description;
2577
+ }
2578
+ const created = await this.client.createSharePointList(graphSiteId, body);
2579
+ const newListId = created.id;
2580
+ if (newListId == null || newListId.length === 0) {
2581
+ // Mint guard (matches #46/#47): a composite token minted with an empty id
2582
+ // would digest to a resolvable-but-wrong token and be reported as 'created'.
2583
+ throw new Error('SharePoint list creation returned no id.');
2584
+ }
2585
+ return this.mintAliasComposite('sharePointList', { siteId: graphSiteId, listId: newListId });
2586
+ }
2587
+ /**
2588
+ * Lists the column definitions for a SharePoint list. Columns are addressed by
2589
+ * name in item field values, so they carry no durable token.
2590
+ */
2591
+ async listSharePointListColumnsAsync(listId) {
2592
+ const { siteId, listId: graphListId } = this.toGraphParts(listId, 'sharePointList', ['siteId', 'listId']);
2593
+ const columns = await this.client.listSharePointListColumns(siteId, graphListId);
2594
+ return columns.map((col) => ({
2595
+ id: col.id ?? '',
2596
+ name: col.name ?? '',
2597
+ displayName: col.displayName ?? '',
2598
+ columnType: sharePointColumnType(col),
2599
+ required: col.required ?? false,
2600
+ readOnly: col.readOnly ?? false,
2601
+ }));
2602
+ }
2603
+ /**
2604
+ * Lists the items in a SharePoint list, minting durable sn_ tokens that carry
2605
+ * the {siteId, listId, itemId} tuple.
2606
+ */
2607
+ async listSharePointListItemsAsync(listId, limit = 50) {
2608
+ const { siteId, listId: graphListId } = this.toGraphParts(listId, 'sharePointList', ['siteId', 'listId']);
2609
+ const items = await this.client.listSharePointListItems(siteId, graphListId, limit);
2610
+ const result = [];
2611
+ for (const item of items) {
2612
+ const itemGraphId = item.id;
2613
+ if (itemGraphId != null) {
2614
+ result.push({
2615
+ id: this.mintAliasComposite('sharePointListItem', { siteId, listId: graphListId, itemId: itemGraphId }),
2616
+ fields: item.fields ?? {},
2617
+ webUrl: item.webUrl ?? '',
2618
+ createdDateTime: item.createdDateTime ?? '',
2619
+ lastModifiedDateTime: item.lastModifiedDateTime ?? '',
2620
+ });
2621
+ }
2622
+ }
2623
+ return result;
2624
+ }
2625
+ /**
2626
+ * Gets a specific SharePoint list item, resolving the sn_ token to its tuple.
2627
+ */
2628
+ async getSharePointListItemAsync(itemId) {
2629
+ const { siteId, listId, itemId: graphItemId } = this.toGraphParts(itemId, 'sharePointListItem', ['siteId', 'listId', 'itemId']);
2630
+ const item = await this.client.getSharePointListItem(siteId, listId, graphItemId);
2631
+ return {
2632
+ id: String(itemId),
2633
+ fields: item.fields ?? {},
2634
+ webUrl: item.webUrl ?? '',
2635
+ createdDateTime: item.createdDateTime ?? '',
2636
+ lastModifiedDateTime: item.lastModifiedDateTime ?? '',
2637
+ };
2638
+ }
2639
+ /**
2640
+ * Creates an item in a SharePoint list, minting a durable sn_ token.
2641
+ */
2642
+ async createSharePointListItemAsync(listId, fields) {
2643
+ const { siteId, listId: graphListId } = this.toGraphParts(listId, 'sharePointList', ['siteId', 'listId']);
2644
+ const created = await this.client.createSharePointListItem(siteId, graphListId, fields);
2645
+ const newItemId = created.id;
2646
+ if (newItemId == null || newItemId.length === 0) {
2647
+ // Mint guard (matches #46/#47): see createSharePointListAsync.
2648
+ throw new Error('SharePoint list item creation returned no id.');
2649
+ }
2650
+ return this.mintAliasComposite('sharePointListItem', { siteId, listId: graphListId, itemId: newItemId });
2651
+ }
2652
+ /**
2653
+ * Updates the field values of a SharePoint list item.
2654
+ */
2655
+ async updateSharePointListItemAsync(itemId, fields) {
2656
+ const { siteId, listId, itemId: graphItemId } = this.toGraphParts(itemId, 'sharePointListItem', ['siteId', 'listId', 'itemId']);
2657
+ await this.client.updateSharePointListItem(siteId, listId, graphItemId, fields);
2658
+ }
2659
+ /**
2660
+ * Deletes an item from a SharePoint list.
2661
+ */
2662
+ async deleteSharePointListItemAsync(itemId) {
2663
+ const { siteId, listId, itemId: graphItemId } = this.toGraphParts(itemId, 'sharePointListItem', ['siteId', 'listId', 'itemId']);
2664
+ await this.client.deleteSharePointListItem(siteId, listId, graphItemId);
2665
+ }
2666
+ }
2667
+ /**
2668
+ * Derives a SharePoint column's type from its definition facets. Graph encodes
2669
+ * the type as the presence of a facet (`text`, `number`, `boolean`, …) rather
2670
+ * than a single field, so return the first facet present or 'unknown'.
2671
+ */
2672
+ function sharePointColumnType(col) {
2673
+ const facets = [
2674
+ 'text', 'number', 'boolean', 'dateTime', 'choice', 'currency',
2675
+ 'personOrGroup', 'lookup', 'hyperlinkOrPicture', 'calculated',
2676
+ ];
2677
+ for (const facet of facets) {
2678
+ if (col[facet] != null)
2679
+ return facet;
2680
+ }
2681
+ return 'unknown';
2992
2682
  }
2993
2683
  /**
2994
2684
  * Creates a Microsoft Graph API repository.
2995
2685
  */
2996
- export function createGraphRepository(deviceCodeCallback) {
2997
- return new GraphRepository(deviceCodeCallback);
2686
+ export function createGraphRepository(deviceCodeCallback, store, accountId) {
2687
+ return new GraphRepository(deviceCodeCallback, store, accountId);
2998
2688
  }
2999
2689
  //# sourceMappingURL=repository.js.map