@jbctechsolutions/mcp-office365 3.0.0 → 4.0.1

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 (340) hide show
  1. package/README.md +42 -65
  2. package/dist/approval/hash.d.ts +8 -8
  3. package/dist/approval/token-manager.d.ts +12 -6
  4. package/dist/approval/token-manager.d.ts.map +1 -1
  5. package/dist/approval/token-manager.js +11 -3
  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 +4 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +18 -3
  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 +8 -0
  27. package/dist/graph/auth/config.js.map +1 -1
  28. package/dist/graph/auth/index.d.ts +1 -0
  29. package/dist/graph/auth/index.d.ts.map +1 -1
  30. package/dist/graph/auth/index.js +1 -0
  31. package/dist/graph/auth/index.js.map +1 -1
  32. package/dist/graph/client/graph-client.d.ts +105 -0
  33. package/dist/graph/client/graph-client.d.ts.map +1 -1
  34. package/dist/graph/client/graph-client.js +414 -18
  35. package/dist/graph/client/graph-client.js.map +1 -1
  36. package/dist/graph/client/immutable-id-middleware.d.ts +50 -0
  37. package/dist/graph/client/immutable-id-middleware.d.ts.map +1 -0
  38. package/dist/graph/client/immutable-id-middleware.js +137 -0
  39. package/dist/graph/client/immutable-id-middleware.js.map +1 -0
  40. package/dist/graph/content-readers.d.ts +0 -12
  41. package/dist/graph/content-readers.d.ts.map +1 -1
  42. package/dist/graph/content-readers.js +0 -21
  43. package/dist/graph/content-readers.js.map +1 -1
  44. package/dist/graph/index.d.ts +5 -6
  45. package/dist/graph/index.d.ts.map +1 -1
  46. package/dist/graph/index.js +5 -6
  47. package/dist/graph/index.js.map +1 -1
  48. package/dist/graph/mailbox-adapter.d.ts +15 -15
  49. package/dist/graph/mappers/contact-mapper.d.ts +4 -1
  50. package/dist/graph/mappers/contact-mapper.d.ts.map +1 -1
  51. package/dist/graph/mappers/contact-mapper.js +7 -3
  52. package/dist/graph/mappers/contact-mapper.js.map +1 -1
  53. package/dist/graph/mappers/email-mapper.d.ts.map +1 -1
  54. package/dist/graph/mappers/email-mapper.js +7 -4
  55. package/dist/graph/mappers/email-mapper.js.map +1 -1
  56. package/dist/graph/mappers/event-mapper.d.ts.map +1 -1
  57. package/dist/graph/mappers/event-mapper.js +6 -3
  58. package/dist/graph/mappers/event-mapper.js.map +1 -1
  59. package/dist/graph/mappers/folder-mapper.d.ts +0 -4
  60. package/dist/graph/mappers/folder-mapper.d.ts.map +1 -1
  61. package/dist/graph/mappers/folder-mapper.js +9 -19
  62. package/dist/graph/mappers/folder-mapper.js.map +1 -1
  63. package/dist/graph/mappers/index.d.ts +2 -2
  64. package/dist/graph/mappers/index.d.ts.map +1 -1
  65. package/dist/graph/mappers/index.js +2 -2
  66. package/dist/graph/mappers/index.js.map +1 -1
  67. package/dist/graph/mappers/task-mapper.d.ts +4 -2
  68. package/dist/graph/mappers/task-mapper.d.ts.map +1 -1
  69. package/dist/graph/mappers/task-mapper.js +7 -5
  70. package/dist/graph/mappers/task-mapper.js.map +1 -1
  71. package/dist/graph/mappers/utils.d.ts +0 -10
  72. package/dist/graph/mappers/utils.d.ts.map +1 -1
  73. package/dist/graph/mappers/utils.js +0 -19
  74. package/dist/graph/mappers/utils.js.map +1 -1
  75. package/dist/graph/repository.d.ts +400 -270
  76. package/dist/graph/repository.d.ts.map +1 -1
  77. package/dist/graph/repository.js +739 -1046
  78. package/dist/graph/repository.js.map +1 -1
  79. package/dist/ids/next-action.d.ts +29 -0
  80. package/dist/ids/next-action.d.ts.map +1 -0
  81. package/dist/ids/next-action.js +58 -0
  82. package/dist/ids/next-action.js.map +1 -0
  83. package/dist/ids/resolver.d.ts +2 -1
  84. package/dist/ids/resolver.d.ts.map +1 -1
  85. package/dist/ids/resolver.js +19 -4
  86. package/dist/ids/resolver.js.map +1 -1
  87. package/dist/ids/schema.d.ts +91 -0
  88. package/dist/ids/schema.d.ts.map +1 -0
  89. package/dist/ids/schema.js +131 -0
  90. package/dist/ids/schema.js.map +1 -0
  91. package/dist/ids/token.d.ts +7 -1
  92. package/dist/ids/token.d.ts.map +1 -1
  93. package/dist/ids/token.js +46 -7
  94. package/dist/ids/token.js.map +1 -1
  95. package/dist/index.d.ts +4 -6
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +63 -100
  98. package/dist/index.js.map +1 -1
  99. package/dist/parsers/index.d.ts +0 -1
  100. package/dist/parsers/index.d.ts.map +1 -1
  101. package/dist/parsers/index.js +0 -1
  102. package/dist/parsers/index.js.map +1 -1
  103. package/dist/registry/all-tools.d.ts.map +1 -1
  104. package/dist/registry/all-tools.js +8 -4
  105. package/dist/registry/all-tools.js.map +1 -1
  106. package/dist/registry/context.d.ts +1 -7
  107. package/dist/registry/context.d.ts.map +1 -1
  108. package/dist/registry/context.js +0 -11
  109. package/dist/registry/context.js.map +1 -1
  110. package/dist/registry/elicit-links.d.ts +35 -0
  111. package/dist/registry/elicit-links.d.ts.map +1 -0
  112. package/dist/registry/elicit-links.js +102 -0
  113. package/dist/registry/elicit-links.js.map +1 -0
  114. package/dist/registry/elicitor.d.ts +24 -0
  115. package/dist/registry/elicitor.d.ts.map +1 -0
  116. package/dist/registry/elicitor.js +51 -0
  117. package/dist/registry/elicitor.js.map +1 -0
  118. package/dist/registry/index.d.ts +2 -2
  119. package/dist/registry/index.d.ts.map +1 -1
  120. package/dist/registry/index.js +1 -1
  121. package/dist/registry/index.js.map +1 -1
  122. package/dist/registry/registry.d.ts +11 -0
  123. package/dist/registry/registry.d.ts.map +1 -1
  124. package/dist/registry/registry.js +87 -0
  125. package/dist/registry/registry.js.map +1 -1
  126. package/dist/registry/types.d.ts +61 -13
  127. package/dist/registry/types.d.ts.map +1 -1
  128. package/dist/state/delta-store.d.ts +85 -0
  129. package/dist/state/delta-store.d.ts.map +1 -0
  130. package/dist/state/delta-store.js +124 -0
  131. package/dist/state/delta-store.js.map +1 -0
  132. package/dist/state/schema.d.ts +2 -3
  133. package/dist/state/schema.d.ts.map +1 -1
  134. package/dist/state/schema.js +26 -3
  135. package/dist/state/schema.js.map +1 -1
  136. package/dist/state/store.d.ts +3 -0
  137. package/dist/state/store.d.ts.map +1 -1
  138. package/dist/state/store.js +4 -0
  139. package/dist/state/store.js.map +1 -1
  140. package/dist/tools/calendar-graph.d.ts.map +1 -1
  141. package/dist/tools/calendar-graph.js +10 -9
  142. package/dist/tools/calendar-graph.js.map +1 -1
  143. package/dist/tools/calendar-permissions.d.ts +7 -7
  144. package/dist/tools/calendar-permissions.d.ts.map +1 -1
  145. package/dist/tools/calendar-permissions.js +7 -4
  146. package/dist/tools/calendar-permissions.js.map +1 -1
  147. package/dist/tools/calendar.d.ts +14 -114
  148. package/dist/tools/calendar.d.ts.map +1 -1
  149. package/dist/tools/calendar.js +41 -202
  150. package/dist/tools/calendar.js.map +1 -1
  151. package/dist/tools/categories.d.ts +4 -4
  152. package/dist/tools/categories.d.ts.map +1 -1
  153. package/dist/tools/categories.js +5 -2
  154. package/dist/tools/categories.js.map +1 -1
  155. package/dist/tools/checklist-items.d.ts +9 -9
  156. package/dist/tools/checklist-items.d.ts.map +1 -1
  157. package/dist/tools/checklist-items.js +8 -5
  158. package/dist/tools/checklist-items.js.map +1 -1
  159. package/dist/tools/contact-folders.d.ts +2 -2
  160. package/dist/tools/contact-folders.d.ts.map +1 -1
  161. package/dist/tools/contact-folders.js +5 -2
  162. package/dist/tools/contact-folders.js.map +1 -1
  163. package/dist/tools/contacts-graph.d.ts +1 -1
  164. package/dist/tools/contacts-graph.d.ts.map +1 -1
  165. package/dist/tools/contacts-graph.js +8 -6
  166. package/dist/tools/contacts-graph.js.map +1 -1
  167. package/dist/tools/contacts.d.ts +9 -44
  168. package/dist/tools/contacts.d.ts.map +1 -1
  169. package/dist/tools/contacts.js +23 -131
  170. package/dist/tools/contacts.js.map +1 -1
  171. package/dist/tools/excel.d.ts +10 -10
  172. package/dist/tools/excel.d.ts.map +1 -1
  173. package/dist/tools/excel.js +9 -6
  174. package/dist/tools/excel.js.map +1 -1
  175. package/dist/tools/focused-overrides.d.ts +4 -4
  176. package/dist/tools/focused-overrides.d.ts.map +1 -1
  177. package/dist/tools/focused-overrides.js +5 -2
  178. package/dist/tools/focused-overrides.js.map +1 -1
  179. package/dist/tools/index.d.ts +4 -5
  180. package/dist/tools/index.d.ts.map +1 -1
  181. package/dist/tools/index.js +4 -6
  182. package/dist/tools/index.js.map +1 -1
  183. package/dist/tools/linked-resources.d.ts +7 -7
  184. package/dist/tools/linked-resources.d.ts.map +1 -1
  185. package/dist/tools/linked-resources.js +7 -4
  186. package/dist/tools/linked-resources.js.map +1 -1
  187. package/dist/tools/mail-graph.d.ts.map +1 -1
  188. package/dist/tools/mail-graph.js +21 -12
  189. package/dist/tools/mail-graph.js.map +1 -1
  190. package/dist/tools/mail-rules.d.ts +8 -8
  191. package/dist/tools/mail-rules.d.ts.map +1 -1
  192. package/dist/tools/mail-rules.js +7 -7
  193. package/dist/tools/mail-rules.js.map +1 -1
  194. package/dist/tools/mail-send.d.ts +27 -27
  195. package/dist/tools/mail-send.d.ts.map +1 -1
  196. package/dist/tools/mail-send.js +30 -19
  197. package/dist/tools/mail-send.js.map +1 -1
  198. package/dist/tools/mail.d.ts +22 -79
  199. package/dist/tools/mail.d.ts.map +1 -1
  200. package/dist/tools/mail.js +45 -293
  201. package/dist/tools/mail.js.map +1 -1
  202. package/dist/tools/mailbox-organization.d.ts +34 -37
  203. package/dist/tools/mailbox-organization.d.ts.map +1 -1
  204. package/dist/tools/mailbox-organization.js +72 -64
  205. package/dist/tools/mailbox-organization.js.map +1 -1
  206. package/dist/tools/meetings.d.ts +14 -14
  207. package/dist/tools/meetings.d.ts.map +1 -1
  208. package/dist/tools/meetings.js +8 -6
  209. package/dist/tools/meetings.js.map +1 -1
  210. package/dist/tools/onedrive.d.ts +16 -16
  211. package/dist/tools/onedrive.d.ts.map +1 -1
  212. package/dist/tools/onedrive.js +16 -11
  213. package/dist/tools/onedrive.js.map +1 -1
  214. package/dist/tools/onenote.d.ts +63 -0
  215. package/dist/tools/onenote.d.ts.map +1 -0
  216. package/dist/tools/onenote.js +221 -0
  217. package/dist/tools/onenote.js.map +1 -0
  218. package/dist/tools/planner-visualization.d.ts +5 -5
  219. package/dist/tools/planner-visualization.d.ts.map +1 -1
  220. package/dist/tools/planner-visualization.js +5 -4
  221. package/dist/tools/planner-visualization.js.map +1 -1
  222. package/dist/tools/planner.d.ts +59 -39
  223. package/dist/tools/planner.d.ts.map +1 -1
  224. package/dist/tools/planner.js +48 -23
  225. package/dist/tools/planner.js.map +1 -1
  226. package/dist/tools/shared-mailbox.d.ts +164 -0
  227. package/dist/tools/shared-mailbox.d.ts.map +1 -0
  228. package/dist/tools/shared-mailbox.js +318 -0
  229. package/dist/tools/shared-mailbox.js.map +1 -0
  230. package/dist/tools/sharepoint-lists.d.ts +131 -0
  231. package/dist/tools/sharepoint-lists.d.ts.map +1 -0
  232. package/dist/tools/sharepoint-lists.js +246 -0
  233. package/dist/tools/sharepoint-lists.js.map +1 -0
  234. package/dist/tools/sharepoint.d.ts +14 -14
  235. package/dist/tools/sharepoint.d.ts.map +1 -1
  236. package/dist/tools/sharepoint.js +11 -9
  237. package/dist/tools/sharepoint.js.map +1 -1
  238. package/dist/tools/task-attachments.d.ts +7 -7
  239. package/dist/tools/task-attachments.d.ts.map +1 -1
  240. package/dist/tools/task-attachments.js +7 -4
  241. package/dist/tools/task-attachments.js.map +1 -1
  242. package/dist/tools/task-lists.d.ts +3 -3
  243. package/dist/tools/task-lists.d.ts.map +1 -1
  244. package/dist/tools/task-lists.js +10 -6
  245. package/dist/tools/task-lists.js.map +1 -1
  246. package/dist/tools/tasks-graph.d.ts +2 -2
  247. package/dist/tools/tasks-graph.d.ts.map +1 -1
  248. package/dist/tools/tasks-graph.js +6 -4
  249. package/dist/tools/tasks-graph.js.map +1 -1
  250. package/dist/tools/tasks.d.ts +8 -43
  251. package/dist/tools/tasks.d.ts.map +1 -1
  252. package/dist/tools/tasks.js +19 -122
  253. package/dist/tools/tasks.js.map +1 -1
  254. package/dist/tools/teams.d.ts +46 -46
  255. package/dist/tools/teams.d.ts.map +1 -1
  256. package/dist/tools/teams.js +34 -26
  257. package/dist/tools/teams.js.map +1 -1
  258. package/dist/tools/what-changed.d.ts +48 -0
  259. package/dist/tools/what-changed.d.ts.map +1 -0
  260. package/dist/tools/what-changed.js +149 -0
  261. package/dist/tools/what-changed.js.map +1 -0
  262. package/dist/types/calendar.d.ts +1 -1
  263. package/dist/types/calendar.d.ts.map +1 -1
  264. package/dist/types/calendar.js.map +1 -1
  265. package/dist/types/contacts.d.ts +1 -1
  266. package/dist/types/contacts.d.ts.map +1 -1
  267. package/dist/types/contacts.js.map +1 -1
  268. package/dist/types/mail.d.ts +1 -1
  269. package/dist/types/mail.d.ts.map +1 -1
  270. package/dist/types/mail.js.map +1 -1
  271. package/dist/utils/errors.d.ts +15 -34
  272. package/dist/utils/errors.d.ts.map +1 -1
  273. package/dist/utils/errors.js +13 -49
  274. package/dist/utils/errors.js.map +1 -1
  275. package/dist/visualization/types.d.ts +4 -4
  276. package/package.json +3 -4
  277. package/dist/applescript/account-repository.d.ts +0 -30
  278. package/dist/applescript/account-repository.d.ts.map +0 -1
  279. package/dist/applescript/account-repository.js +0 -38
  280. package/dist/applescript/account-repository.js.map +0 -1
  281. package/dist/applescript/account-scripts.d.ts +0 -21
  282. package/dist/applescript/account-scripts.d.ts.map +0 -1
  283. package/dist/applescript/account-scripts.js +0 -180
  284. package/dist/applescript/account-scripts.js.map +0 -1
  285. package/dist/applescript/calendar-manager.d.ts +0 -44
  286. package/dist/applescript/calendar-manager.d.ts.map +0 -1
  287. package/dist/applescript/calendar-manager.js +0 -92
  288. package/dist/applescript/calendar-manager.js.map +0 -1
  289. package/dist/applescript/calendar-writer.d.ts +0 -36
  290. package/dist/applescript/calendar-writer.d.ts.map +0 -1
  291. package/dist/applescript/calendar-writer.js +0 -94
  292. package/dist/applescript/calendar-writer.js.map +0 -1
  293. package/dist/applescript/content-readers.d.ts +0 -114
  294. package/dist/applescript/content-readers.d.ts.map +0 -1
  295. package/dist/applescript/content-readers.js +0 -328
  296. package/dist/applescript/content-readers.js.map +0 -1
  297. package/dist/applescript/executor.d.ts +0 -60
  298. package/dist/applescript/executor.d.ts.map +0 -1
  299. package/dist/applescript/executor.js +0 -173
  300. package/dist/applescript/executor.js.map +0 -1
  301. package/dist/applescript/index.d.ts +0 -20
  302. package/dist/applescript/index.d.ts.map +0 -1
  303. package/dist/applescript/index.js +0 -29
  304. package/dist/applescript/index.js.map +0 -1
  305. package/dist/applescript/mail-sender.d.ts +0 -38
  306. package/dist/applescript/mail-sender.d.ts.map +0 -1
  307. package/dist/applescript/mail-sender.js +0 -67
  308. package/dist/applescript/mail-sender.js.map +0 -1
  309. package/dist/applescript/parser.d.ts +0 -235
  310. package/dist/applescript/parser.d.ts.map +0 -1
  311. package/dist/applescript/parser.js +0 -496
  312. package/dist/applescript/parser.js.map +0 -1
  313. package/dist/applescript/repository.d.ts +0 -64
  314. package/dist/applescript/repository.d.ts.map +0 -1
  315. package/dist/applescript/repository.js +0 -444
  316. package/dist/applescript/repository.js.map +0 -1
  317. package/dist/applescript/scripts.d.ts +0 -265
  318. package/dist/applescript/scripts.d.ts.map +0 -1
  319. package/dist/applescript/scripts.js +0 -1483
  320. package/dist/applescript/scripts.js.map +0 -1
  321. package/dist/parsers/olk15.d.ts +0 -87
  322. package/dist/parsers/olk15.d.ts.map +0 -1
  323. package/dist/parsers/olk15.js +0 -368
  324. package/dist/parsers/olk15.js.map +0 -1
  325. package/dist/tools/accounts.d.ts +0 -37
  326. package/dist/tools/accounts.d.ts.map +0 -1
  327. package/dist/tools/accounts.js +0 -68
  328. package/dist/tools/accounts.js.map +0 -1
  329. package/dist/tools/calendar-apple.d.ts +0 -39
  330. package/dist/tools/calendar-apple.d.ts.map +0 -1
  331. package/dist/tools/calendar-apple.js +0 -159
  332. package/dist/tools/calendar-apple.js.map +0 -1
  333. package/dist/tools/mail-apple.d.ts +0 -40
  334. package/dist/tools/mail-apple.d.ts.map +0 -1
  335. package/dist/tools/mail-apple.js +0 -146
  336. package/dist/tools/mail-apple.js.map +0 -1
  337. package/dist/tools/notes.d.ts +0 -86
  338. package/dist/tools/notes.d.ts.map +0 -1
  339. package/dist/tools/notes.js +0 -202
  340. 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,78 +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;
56
106
  }
57
107
  // ===========================================================================
58
108
  // Cache Resolvers (auto-fetch parent on cache miss)
59
109
  // ===========================================================================
60
110
  async resolveTeamId(teamId) {
61
- let graphId = this.idCache.teams.get(teamId);
62
- if (graphId != null)
63
- return graphId;
64
- await this.listTeamsAsync();
65
- graphId = this.idCache.teams.get(teamId);
66
- if (graphId != null)
67
- return graphId;
68
- throw new Error(`Team ID ${teamId} not found. Verify the ID is correct by calling list_teams.`);
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
+ }
69
124
  }
70
125
  async resolvePlanId(planId) {
71
- let cached = this.idCache.plans.get(planId);
72
- if (cached != null)
73
- return cached;
74
- await this.listPlansAsync();
75
- cached = this.idCache.plans.get(planId);
76
- if (cached != null)
77
- return cached;
78
- throw new Error(`Plan ID ${planId} not found. Verify the ID is correct by calling list_plans.`);
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
+ }
79
184
  }
80
185
  async resolveChatId(chatId) {
81
- let graphId = this.idCache.chats.get(chatId);
82
- if (graphId != null)
83
- return graphId;
84
- await this.listChatsAsync();
85
- graphId = this.idCache.chats.get(chatId);
86
- if (graphId != null)
87
- return graphId;
88
- throw new Error(`Chat ID ${chatId} not found. Verify the ID is correct by calling list_chats.`);
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
+ }
89
196
  }
90
197
  // ===========================================================================
91
198
  // Folders
@@ -97,31 +204,15 @@ export class GraphRepository {
97
204
  throw new Error('Use listFoldersAsync() for Graph repository');
98
205
  }
99
206
  async listFoldersAsync() {
207
+ // Rows carry self-encoding fd_ tokens (the mapper mints them) — no cache.
100
208
  const folders = await this.client.listMailFolders();
101
- // Update ID cache
102
- for (const folder of folders) {
103
- if (folder.id != null) {
104
- const numericId = hashStringToNumber(folder.id);
105
- this.idCache.folders.set(numericId, folder.id);
106
- }
107
- }
108
209
  return folders.map(mapMailFolderToRow);
109
210
  }
110
211
  getFolder(_id) {
111
212
  throw new Error('Use getFolderAsync() for Graph repository');
112
213
  }
113
214
  async getFolderAsync(id) {
114
- const graphId = this.idCache.folders.get(id);
115
- if (graphId == null) {
116
- // Try to find it by listing all folders
117
- await this.listFoldersAsync();
118
- const refreshedGraphId = this.idCache.folders.get(id);
119
- if (refreshedGraphId == null) {
120
- return undefined;
121
- }
122
- const folder = await this.client.getMailFolder(refreshedGraphId);
123
- return folder != null ? mapMailFolderToRow(folder) : undefined;
124
- }
215
+ const graphId = this.toGraphId(id, 'folder');
125
216
  const folder = await this.client.getMailFolder(graphId);
126
217
  return folder != null ? mapMailFolderToRow(folder) : undefined;
127
218
  }
@@ -132,58 +223,23 @@ export class GraphRepository {
132
223
  throw new Error('Use listEmailsAsync() for Graph repository');
133
224
  }
134
225
  async listEmailsAsync(folderId, limit, offset) {
135
- const graphFolderId = this.idCache.folders.get(folderId);
136
- if (graphFolderId == null) {
137
- // Refresh folder cache
138
- await this.listFoldersAsync();
139
- const refreshedId = this.idCache.folders.get(folderId);
140
- if (refreshedId == null) {
141
- return [];
142
- }
143
- return this.listEmailsWithGraphId(refreshedId, limit, offset);
144
- }
226
+ const graphFolderId = this.toGraphId(folderId, 'folder');
145
227
  return this.listEmailsWithGraphId(graphFolderId, limit, offset);
146
228
  }
147
229
  async listEmailsWithGraphId(folderId, limit, offset) {
230
+ // Rows carry self-encoding em_ tokens (the mapper mints them) — no cache.
148
231
  const messages = await this.client.listMessages(folderId, limit, offset);
149
- // Update ID cache
150
- for (const message of messages) {
151
- if (message.id != null) {
152
- const numericId = hashStringToNumber(message.id);
153
- this.idCache.messages.set(numericId, message.id);
154
- }
155
- if (message.conversationId != null) {
156
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
157
- }
158
- }
159
232
  return messages.map((m) => mapMessageToEmailRow(m, folderId));
160
233
  }
161
234
  listUnreadEmails(_folderId, _limit, _offset) {
162
235
  throw new Error('Use listUnreadEmailsAsync() for Graph repository');
163
236
  }
164
237
  async listUnreadEmailsAsync(folderId, limit, offset) {
165
- const graphFolderId = this.idCache.folders.get(folderId);
166
- if (graphFolderId == null) {
167
- await this.listFoldersAsync();
168
- const refreshedId = this.idCache.folders.get(folderId);
169
- if (refreshedId == null) {
170
- return [];
171
- }
172
- return this.listUnreadEmailsWithGraphId(refreshedId, limit, offset);
173
- }
238
+ const graphFolderId = this.toGraphId(folderId, 'folder');
174
239
  return this.listUnreadEmailsWithGraphId(graphFolderId, limit, offset);
175
240
  }
176
241
  async listUnreadEmailsWithGraphId(folderId, limit, offset) {
177
242
  const messages = await this.client.listUnreadMessages(folderId, limit, offset);
178
- for (const message of messages) {
179
- if (message.id != null) {
180
- const numericId = hashStringToNumber(message.id);
181
- this.idCache.messages.set(numericId, message.id);
182
- }
183
- if (message.conversationId != null) {
184
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
185
- }
186
- }
187
243
  return messages.map((m) => mapMessageToEmailRow(m, folderId));
188
244
  }
189
245
  searchEmails(_query, _limit) {
@@ -191,43 +247,17 @@ export class GraphRepository {
191
247
  }
192
248
  async searchEmailsAsync(query, limit) {
193
249
  const messages = await this.client.searchMessages(query, limit);
194
- for (const message of messages) {
195
- if (message.id != null) {
196
- const numericId = hashStringToNumber(message.id);
197
- this.idCache.messages.set(numericId, message.id);
198
- }
199
- if (message.conversationId != null) {
200
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
201
- }
202
- }
203
250
  return messages.map((m) => mapMessageToEmailRow(m));
204
251
  }
205
252
  searchEmailsInFolder(_folderId, _query, _limit) {
206
253
  throw new Error('Use searchEmailsInFolderAsync() for Graph repository');
207
254
  }
208
255
  async searchEmailsInFolderAsync(folderId, query, limit) {
209
- const graphFolderId = this.idCache.folders.get(folderId);
210
- if (graphFolderId == null) {
211
- await this.listFoldersAsync();
212
- const refreshedId = this.idCache.folders.get(folderId);
213
- if (refreshedId == null) {
214
- return [];
215
- }
216
- return this.searchEmailsInFolderWithGraphId(refreshedId, query, limit);
217
- }
256
+ const graphFolderId = this.toGraphId(folderId, 'folder');
218
257
  return this.searchEmailsInFolderWithGraphId(graphFolderId, query, limit);
219
258
  }
220
259
  async searchEmailsInFolderWithGraphId(folderId, query, limit) {
221
260
  const messages = await this.client.searchMessagesInFolder(folderId, query, limit);
222
- for (const message of messages) {
223
- if (message.id != null) {
224
- const numericId = hashStringToNumber(message.id);
225
- this.idCache.messages.set(numericId, message.id);
226
- }
227
- if (message.conversationId != null) {
228
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
229
- }
230
- }
231
261
  return messages.map((m) => mapMessageToEmailRow(m, folderId));
232
262
  }
233
263
  /**
@@ -237,14 +267,6 @@ export class GraphRepository {
237
267
  */
238
268
  async searchEmailsStructuredAsync(compiled, limit) {
239
269
  const messages = await this.runStructuredSearch(compiled, limit);
240
- for (const msg of messages) {
241
- if (msg.id != null) {
242
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
243
- }
244
- if (msg.conversationId != null) {
245
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
246
- }
247
- }
248
270
  return messages.map((m) => mapMessageToEmailRow(m));
249
271
  }
250
272
  runStructuredSearch(compiled, limit) {
@@ -258,22 +280,16 @@ export class GraphRepository {
258
280
  }
259
281
  }
260
282
  async checkNewEmailsAsync(folderId) {
261
- const graphFolderId = this.idCache.folders.get(folderId);
262
- if (graphFolderId == null)
263
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
264
- 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);
265
289
  const isInitialSync = existingDeltaLink == null;
266
290
  const { messages, deltaLink } = await this.client.getMessagesDelta(graphFolderId, existingDeltaLink);
267
291
  if (deltaLink) {
268
- this.deltaLinks.set(folderId, deltaLink);
269
- }
270
- for (const msg of messages) {
271
- if (msg.id != null) {
272
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
273
- }
274
- if (msg.conversationId != null) {
275
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
276
- }
292
+ this.deltaLinks.set(graphFolderId, deltaLink);
277
293
  }
278
294
  const activeMessages = messages.filter((m) => m['@removed'] == null);
279
295
  return {
@@ -284,61 +300,8 @@ export class GraphRepository {
284
300
  getEmail(_id) {
285
301
  throw new Error('Use getEmailAsync() for Graph repository');
286
302
  }
287
- /**
288
- * Populates the message ID cache by listing messages from mail folders.
289
- * Used as a fallback when getEmailAsync is called with an ID not yet in cache
290
- * (e.g. after server restart or when list_emails/search_emails wasn't called first).
291
- */
292
- async refreshMessageCacheForGetEmail(targetId) {
293
- let folders;
294
- try {
295
- folders = await this.listFoldersAsync();
296
- }
297
- catch {
298
- return false;
299
- }
300
- if (folders.length === 0)
301
- return false;
302
- const MESSAGE_LIMIT_PER_FOLDER = 100;
303
- const MAX_FOLDERS_TO_SCAN = 15;
304
- for (let i = 0; i < Math.min(folders.length, MAX_FOLDERS_TO_SCAN); i++) {
305
- const folder = folders[i];
306
- const graphFolderId = this.idCache.folders.get(folder.id);
307
- if (graphFolderId == null)
308
- continue;
309
- let messages;
310
- try {
311
- messages = await this.client.listMessages(graphFolderId, MESSAGE_LIMIT_PER_FOLDER, 0);
312
- }
313
- catch {
314
- continue;
315
- }
316
- if (!Array.isArray(messages))
317
- continue;
318
- for (const message of messages) {
319
- if (message.id != null) {
320
- const numericId = hashStringToNumber(message.id);
321
- this.idCache.messages.set(numericId, message.id);
322
- if (numericId === targetId)
323
- return true;
324
- }
325
- if (message.conversationId != null) {
326
- this.idCache.conversations.set(hashStringToNumber(message.conversationId), message.conversationId);
327
- }
328
- }
329
- }
330
- return this.idCache.messages.has(targetId);
331
- }
332
303
  async getEmailAsync(id) {
333
- let graphId = this.idCache.messages.get(id);
334
- if (graphId == null) {
335
- const found = await this.refreshMessageCacheForGetEmail(id);
336
- if (found)
337
- graphId = this.idCache.messages.get(id) ?? undefined;
338
- }
339
- if (graphId == null) {
340
- return undefined;
341
- }
304
+ const graphId = this.toGraphId(id, 'message');
342
305
  const message = await this.client.getMessage(graphId);
343
306
  return message != null ? mapMessageToEmailRow(message) : undefined;
344
307
  }
@@ -353,16 +316,7 @@ export class GraphRepository {
353
316
  throw new Error('Use getUnreadCountByFolderAsync() for Graph repository');
354
317
  }
355
318
  async getUnreadCountByFolderAsync(folderId) {
356
- const graphId = this.idCache.folders.get(folderId);
357
- if (graphId == null) {
358
- await this.listFoldersAsync();
359
- const refreshedId = this.idCache.folders.get(folderId);
360
- if (refreshedId == null) {
361
- return 0;
362
- }
363
- const folder = await this.client.getMailFolder(refreshedId);
364
- return folder?.unreadItemCount ?? 0;
365
- }
319
+ const graphId = this.toGraphId(folderId, 'folder');
366
320
  const folder = await this.client.getMailFolder(graphId);
367
321
  return folder?.unreadItemCount ?? 0;
368
322
  }
@@ -372,27 +326,19 @@ export class GraphRepository {
372
326
  /**
373
327
  * Lists all messages in a conversation thread.
374
328
  *
375
- * Looks up the message to get its conversationId, resolves the Graph string
376
- * 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.
377
332
  */
378
333
  async listConversationAsync(messageId, limit) {
379
- const email = await this.getEmailAsync(messageId);
380
- if (email == null)
381
- throw new Error(`Message ID ${messageId} not found`);
382
- 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)
383
340
  throw new Error('Message has no conversation ID');
384
- const graphConversationId = this.idCache.conversations.get(email.conversationId);
385
- if (graphConversationId == null)
386
- throw new Error('Conversation ID not found in cache. Try fetching the email first to populate the cache.');
387
- const messages = await this.client.listConversationMessages(graphConversationId, limit);
388
- for (const msg of messages) {
389
- if (msg.id != null) {
390
- this.idCache.messages.set(hashStringToNumber(msg.id), msg.id);
391
- }
392
- if (msg.conversationId != null) {
393
- this.idCache.conversations.set(hashStringToNumber(msg.conversationId), msg.conversationId);
394
- }
395
- }
341
+ const messages = await this.client.listConversationMessages(convId, limit);
396
342
  return messages.map((m) => mapMessageToEmailRow(m));
397
343
  }
398
344
  // ===========================================================================
@@ -402,72 +348,39 @@ export class GraphRepository {
402
348
  throw new Error('Use listCalendarsAsync() for Graph repository');
403
349
  }
404
350
  async listCalendarsAsync() {
351
+ // Rows carry self-encoding fd_ tokens (the mapper mints them) — no cache.
405
352
  const calendars = await this.client.listCalendars();
406
- for (const calendar of calendars) {
407
- if (calendar.id != null) {
408
- const numericId = hashStringToNumber(calendar.id);
409
- this.idCache.folders.set(numericId, calendar.id);
410
- this.idCache.calendars.set(numericId, calendar.id);
411
- }
412
- }
413
353
  return calendars.map(mapCalendarToFolderRow);
414
354
  }
415
355
  listEvents(_limit) {
416
356
  throw new Error('Use listEventsAsync() for Graph repository');
417
357
  }
418
358
  async listEventsAsync(limit) {
359
+ // Rows carry self-encoding ev_ tokens (the mapper mints them) — no cache.
419
360
  const events = await this.client.listEvents(limit);
420
- for (const event of events) {
421
- if (event.id != null) {
422
- const numericId = hashStringToNumber(event.id);
423
- this.idCache.events.set(numericId, event.id);
424
- }
425
- }
426
361
  return events.map((e) => mapEventToEventRow(e));
427
362
  }
428
363
  listEventsByFolder(_folderId, _limit) {
429
364
  throw new Error('Use listEventsByFolderAsync() for Graph repository');
430
365
  }
431
366
  async listEventsByFolderAsync(folderId, limit) {
432
- const graphCalendarId = this.idCache.folders.get(folderId);
433
- if (graphCalendarId == null) {
434
- return this.listEventsAsync(limit);
435
- }
367
+ const graphCalendarId = this.toGraphId(folderId, 'folder');
436
368
  const events = await this.client.listEvents(limit, graphCalendarId);
437
- for (const event of events) {
438
- if (event.id != null) {
439
- const numericId = hashStringToNumber(event.id);
440
- this.idCache.events.set(numericId, event.id);
441
- }
442
- }
443
369
  return events.map((e) => mapEventToEventRow(e, graphCalendarId));
444
370
  }
445
371
  searchEvents(_query, _startDate, _endDate, _limit) {
446
372
  throw new Error('Use searchEventsAsync() for Graph repository');
447
373
  }
448
374
  async searchEventsAsync(query, startDate, endDate, limit) {
449
- // 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.
450
377
  const start = startDate != null ? new Date(startDate) : undefined;
451
378
  const end = endDate != null ? new Date(endDate) : undefined;
452
379
  const events = await this.client.listEvents(1000, 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
- let rows = events.map((e) => mapEventToEventRow(e));
460
- // Filter by title client-side if query provided
461
- if (query != null) {
462
- const queryLower = query.toLowerCase();
463
- rows = rows.filter((row) => {
464
- // EventRow doesn't have title, so we need to check the original event
465
- const originalEvent = events.find((e) => e.id != null && hashStringToNumber(e.id) === row.id);
466
- const subject = originalEvent?.subject?.toLowerCase() ?? '';
467
- return subject.includes(queryLower);
468
- });
469
- }
470
- 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));
471
384
  }
472
385
  listEventsByDateRange(_startDate, _endDate, _limit) {
473
386
  throw new Error('Use listEventsByDateRangeAsync() for Graph repository');
@@ -476,36 +389,23 @@ export class GraphRepository {
476
389
  const start = new Date(startDate * 1000);
477
390
  const end = new Date(endDate * 1000);
478
391
  const events = await this.client.listEvents(limit, undefined, start, end);
479
- for (const event of events) {
480
- if (event.id != null) {
481
- const numericId = hashStringToNumber(event.id);
482
- this.idCache.events.set(numericId, event.id);
483
- }
484
- }
485
392
  return events.map((e) => mapEventToEventRow(e));
486
393
  }
487
394
  getEvent(_id) {
488
395
  throw new Error('Use getEventAsync() for Graph repository');
489
396
  }
490
397
  async getEventAsync(id) {
491
- const graphId = this.idCache.events.get(id);
492
- if (graphId == null) {
493
- return undefined;
494
- }
398
+ const graphId = this.toGraphId(id, 'event');
495
399
  const event = await this.client.getEvent(graphId);
496
400
  return event != null ? mapEventToEventRow(event) : undefined;
497
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
+ }
498
406
  async listEventInstancesAsync(eventId, startDate, endDate) {
499
- const graphId = this.idCache.events.get(eventId);
500
- if (graphId == null) {
501
- throw new Error(`Event ID ${eventId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
502
- }
407
+ const graphId = this.toGraphId(eventId, 'event');
503
408
  const instances = await this.client.listEventInstances(graphId, startDate, endDate);
504
- for (const inst of instances) {
505
- if (inst.id != null) {
506
- this.idCache.events.set(hashStringToNumber(inst.id), inst.id);
507
- }
508
- }
509
409
  return instances.map((e) => mapEventToEventRow(e));
510
410
  }
511
411
  // ===========================================================================
@@ -515,13 +415,9 @@ export class GraphRepository {
515
415
  throw new Error('Use listContactsAsync() for Graph repository');
516
416
  }
517
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.
518
420
  const contacts = await this.client.listContacts(limit, offset);
519
- for (const contact of contacts) {
520
- if (contact.id != null) {
521
- const numericId = hashStringToNumber(contact.id);
522
- this.idCache.contacts.set(numericId, contact.id);
523
- }
524
- }
525
421
  return contacts.map(mapContactToContactRow);
526
422
  }
527
423
  searchContacts(_query, _limit) {
@@ -529,25 +425,20 @@ export class GraphRepository {
529
425
  }
530
426
  async searchContactsAsync(query, limit) {
531
427
  const contacts = await this.client.searchContacts(query, limit);
532
- for (const contact of contacts) {
533
- if (contact.id != null) {
534
- const numericId = hashStringToNumber(contact.id);
535
- this.idCache.contacts.set(numericId, contact.id);
536
- }
537
- }
538
428
  return contacts.map(mapContactToContactRow);
539
429
  }
540
430
  getContact(_id) {
541
431
  throw new Error('Use getContactAsync() for Graph repository');
542
432
  }
543
433
  async getContactAsync(id) {
544
- const graphId = this.idCache.contacts.get(id);
545
- if (graphId == null) {
546
- return undefined;
547
- }
434
+ const graphId = this.toGraphId(id, 'contact');
548
435
  const contact = await this.client.getContact(graphId);
549
436
  return contact != null ? mapContactToContactRow(contact) : undefined;
550
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
+ }
551
442
  // ===========================================================================
552
443
  // Contact Folders
553
444
  // ===========================================================================
@@ -555,10 +446,8 @@ export class GraphRepository {
555
446
  const folders = await this.client.listContactFolders();
556
447
  return folders.map((folder) => {
557
448
  const graphId = folder.id;
558
- const numericId = hashStringToNumber(graphId);
559
- this.idCache.contactFolders.set(numericId, graphId);
560
449
  return {
561
- id: numericId,
450
+ id: this.mintAlias('contactFolder', graphId),
562
451
  name: folder.displayName ?? '',
563
452
  parentFolderId: folder.parentFolderId ?? null,
564
453
  };
@@ -566,47 +455,30 @@ export class GraphRepository {
566
455
  }
567
456
  async createContactFolderAsync(name) {
568
457
  const created = await this.client.createContactFolder(name);
569
- const graphId = created.id;
570
- const numericId = hashStringToNumber(graphId);
571
- this.idCache.contactFolders.set(numericId, graphId);
572
- return numericId;
458
+ return this.mintAlias('contactFolder', created.id);
573
459
  }
574
460
  async deleteContactFolderAsync(folderId) {
575
- const graphId = this.idCache.contactFolders.get(folderId);
576
- if (graphId == null)
577
- 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');
578
462
  await this.client.deleteContactFolder(graphId);
579
- this.idCache.contactFolders.delete(folderId);
580
463
  }
581
464
  async listContactsInFolderAsync(folderId, limit = 100) {
582
- const graphId = this.idCache.contactFolders.get(folderId);
583
- if (graphId == null)
584
- 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');
585
466
  const contacts = await this.client.listContactsInFolder(graphId, limit);
586
- return contacts.map((c) => {
587
- if (c.id != null) {
588
- this.idCache.contacts.set(hashStringToNumber(c.id), c.id);
589
- }
590
- return mapContactToContactRow(c);
591
- });
467
+ return contacts.map(mapContactToContactRow);
592
468
  }
593
469
  // ===========================================================================
594
470
  // Contact Photos
595
471
  // ===========================================================================
596
472
  async getContactPhotoAsync(contactId) {
597
- const graphId = this.idCache.contacts.get(contactId);
598
- if (graphId == null)
599
- 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');
600
474
  const photoData = await this.client.getContactPhoto(graphId);
601
475
  const downloadDir = getDownloadDir();
602
- 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`);
603
477
  fs.writeFileSync(filePath, Buffer.from(photoData));
604
478
  return { filePath, contentType: 'image/jpeg' };
605
479
  }
606
480
  async setContactPhotoAsync(contactId, filePath) {
607
- const graphId = this.idCache.contacts.get(contactId);
608
- if (graphId == null)
609
- 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');
610
482
  const photoData = fs.readFileSync(filePath);
611
483
  const ext = path.extname(filePath).toLowerCase();
612
484
  const contentType = ext === '.png' ? 'image/png' : 'image/jpeg';
@@ -620,66 +492,65 @@ export class GraphRepository {
620
492
  }
621
493
  async listTasksAsync(limit, offset) {
622
494
  const tasks = await this.client.listAllTasks(limit, offset, true);
623
- for (const task of tasks) {
624
- if (task.id != null && task.taskListId != null) {
625
- const numericId = hashStringToNumber(task.id);
626
- this.idCache.tasks.set(numericId, { taskListId: task.taskListId, taskId: task.id });
627
- const listNumericId = hashStringToNumber(task.taskListId);
628
- this.idCache.taskLists.set(listNumericId, task.taskListId);
629
- }
630
- }
631
- 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
+ });
632
502
  }
633
503
  listIncompleteTasks(_limit, _offset) {
634
504
  throw new Error('Use listIncompleteTasksAsync() for Graph repository');
635
505
  }
636
506
  async listIncompleteTasksAsync(limit, offset) {
637
507
  const tasks = await this.client.listAllTasks(limit, offset, false);
638
- for (const task of tasks) {
639
- if (task.id != null && task.taskListId != null) {
640
- const numericId = hashStringToNumber(task.id);
641
- this.idCache.tasks.set(numericId, { taskListId: task.taskListId, taskId: task.id });
642
- const listNumericId = hashStringToNumber(task.taskListId);
643
- this.idCache.taskLists.set(listNumericId, task.taskListId);
644
- }
645
- }
646
- 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
+ });
647
515
  }
648
516
  searchTasks(_query, _limit) {
649
517
  throw new Error('Use searchTasksAsync() for Graph repository');
650
518
  }
651
519
  async searchTasksAsync(query, limit) {
652
520
  const tasks = await this.client.searchTasks(query, limit);
653
- for (const task of tasks) {
654
- if (task.id != null && task.taskListId != null) {
655
- const numericId = hashStringToNumber(task.id);
656
- this.idCache.tasks.set(numericId, { taskListId: task.taskListId, taskId: task.id });
657
- }
658
- }
659
- 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
+ });
660
530
  }
661
531
  getTask(_id) {
662
532
  throw new Error('Use getTaskAsync() for Graph repository');
663
533
  }
664
534
  async getTaskAsync(id) {
665
- const taskInfo = this.idCache.tasks.get(id);
666
- if (taskInfo == null) {
535
+ let taskInfo;
536
+ try {
537
+ taskInfo = this.toGraphParts(id, 'task', ['taskListId', 'taskId']);
538
+ }
539
+ catch {
667
540
  return undefined;
668
541
  }
669
542
  const task = await this.client.getTask(taskInfo.taskListId, taskInfo.taskId);
670
543
  if (task == null) {
671
544
  return undefined;
672
545
  }
673
- return mapTaskToTaskRow({ ...task, taskListId: taskInfo.taskListId });
546
+ return mapTaskToTaskRow({ ...task, taskListId: taskInfo.taskListId }, String(id), this.mintAlias('taskList', taskInfo.taskListId));
674
547
  }
675
548
  async listTaskListsAsync() {
676
549
  const lists = await this.client.listTaskLists();
677
550
  return lists.map((list) => {
678
551
  const graphId = list.id;
679
- const numericId = hashStringToNumber(graphId);
680
- this.idCache.taskLists.set(numericId, graphId);
681
552
  return {
682
- id: numericId,
553
+ id: this.mintAlias('taskList', graphId),
683
554
  name: list.displayName ?? '',
684
555
  isDefault: list.wellknownListName === 'defaultList',
685
556
  };
@@ -720,37 +591,33 @@ export class GraphRepository {
720
591
  return this.client;
721
592
  }
722
593
  /**
723
- * 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.
724
596
  */
725
597
  getGraphIdForDraft(draftId) {
726
- return this.idCache.messages.get(draftId);
598
+ return this.toGraphId(draftId, 'message');
727
599
  }
728
600
  /**
729
- * Gets the Graph string ID from a numeric ID.
601
+ * Gets task info (Graph taskListId/taskId) from a durable `td_` token.
730
602
  */
731
- getGraphId(type, numericId) {
732
- switch (type) {
733
- case 'folder':
734
- return this.idCache.folders.get(numericId);
735
- case 'message':
736
- return this.idCache.messages.get(numericId);
737
- case 'event':
738
- return this.idCache.events.get(numericId);
739
- case 'contact':
740
- 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;
741
609
  }
742
610
  }
743
611
  /**
744
- * Gets task info from a numeric ID.
745
- */
746
- getTaskInfo(numericId) {
747
- return this.idCache.tasks.get(numericId);
748
- }
749
- /**
750
- * 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.
751
613
  */
752
- getTaskListGraphId(numericId) {
753
- return this.idCache.taskLists.get(numericId);
614
+ getTaskListGraphId(id) {
615
+ try {
616
+ return this.toGraphId(id, 'taskList');
617
+ }
618
+ catch {
619
+ return undefined;
620
+ }
754
621
  }
755
622
  // ===========================================================================
756
623
  // Write Operations (Async)
@@ -797,42 +664,28 @@ export class GraphRepository {
797
664
  }
798
665
  // Async implementations
799
666
  async moveEmailAsync(emailId, destinationFolderId) {
800
- const graphMessageId = this.idCache.messages.get(emailId);
801
- const graphFolderId = this.idCache.folders.get(destinationFolderId);
802
- if (graphMessageId == null)
803
- throw new Error(`Message ID ${emailId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
804
- if (graphFolderId == null)
805
- 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');
806
669
  await this.client.moveMessage(graphMessageId, graphFolderId);
807
670
  }
808
671
  async deleteEmailAsync(emailId) {
809
- const graphId = this.idCache.messages.get(emailId);
810
- if (graphId == null)
811
- 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');
812
673
  await this.client.deleteMessage(graphId);
813
674
  }
814
675
  async archiveEmailAsync(emailId) {
815
- const graphId = this.idCache.messages.get(emailId);
816
- if (graphId == null)
817
- 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');
818
677
  await this.client.archiveMessage(graphId);
819
678
  }
820
679
  async junkEmailAsync(emailId) {
821
- const graphId = this.idCache.messages.get(emailId);
822
- if (graphId == null)
823
- 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');
824
681
  await this.client.junkMessage(graphId);
825
682
  }
826
683
  async markEmailReadAsync(emailId, isRead) {
827
- const graphId = this.idCache.messages.get(emailId);
828
- if (graphId == null)
829
- 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');
830
685
  await this.client.updateMessage(graphId, { isRead });
831
686
  }
832
687
  async setEmailFlagAsync(emailId, flagStatus) {
833
- const graphId = this.idCache.messages.get(emailId);
834
- if (graphId == null)
835
- 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');
836
689
  const flagStatusMap = {
837
690
  0: 'notFlagged',
838
691
  1: 'flagged',
@@ -843,55 +696,35 @@ export class GraphRepository {
843
696
  });
844
697
  }
845
698
  async setEmailCategoriesAsync(emailId, categories) {
846
- const graphId = this.idCache.messages.get(emailId);
847
- if (graphId == null)
848
- 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');
849
700
  await this.client.updateMessage(graphId, { categories });
850
701
  }
851
702
  async setEmailImportanceAsync(emailId, importance) {
852
- const graphId = this.idCache.messages.get(emailId);
853
- if (graphId == null)
854
- 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');
855
704
  await this.client.updateMessage(graphId, { importance });
856
705
  }
857
706
  async createFolderAsync(name, parentFolderId) {
858
707
  const graphParentId = parentFolderId != null
859
- ? this.idCache.folders.get(parentFolderId)
708
+ ? this.toGraphId(parentFolderId, 'folder')
860
709
  : undefined;
861
- const folder = await this.client.createMailFolder(name, graphParentId ?? undefined);
862
- // Update cache with new folder
863
- if (folder.id != null) {
864
- const numericId = hashStringToNumber(folder.id);
865
- this.idCache.folders.set(numericId, folder.id);
866
- }
710
+ const folder = await this.client.createMailFolder(name, graphParentId);
867
711
  return mapMailFolderToRow(folder);
868
712
  }
869
713
  async deleteFolderAsync(folderId) {
870
- const graphId = this.idCache.folders.get(folderId);
871
- if (graphId == null)
872
- 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');
873
715
  await this.client.deleteMailFolder(graphId);
874
- this.idCache.folders.delete(folderId);
875
716
  }
876
717
  async renameFolderAsync(folderId, newName) {
877
- const graphId = this.idCache.folders.get(folderId);
878
- if (graphId == null)
879
- 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');
880
719
  await this.client.renameMailFolder(graphId, newName);
881
720
  }
882
721
  async moveFolderAsync(folderId, destinationParentId) {
883
- const graphFolderId = this.idCache.folders.get(folderId);
884
- const graphParentId = this.idCache.folders.get(destinationParentId);
885
- if (graphFolderId == null)
886
- throw new Error(`Folder ID ${folderId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
887
- if (graphParentId == null)
888
- 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');
889
724
  await this.client.moveMailFolder(graphFolderId, graphParentId);
890
725
  }
891
726
  async emptyFolderAsync(folderId) {
892
- const graphId = this.idCache.folders.get(folderId);
893
- if (graphId == null)
894
- 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');
895
728
  await this.client.emptyMailFolder(graphId);
896
729
  }
897
730
  // ===========================================================================
@@ -901,7 +734,7 @@ export class GraphRepository {
901
734
  * Creates a new draft message.
902
735
  *
903
736
  * Converts email address strings to Recipient objects, calls the Graph client,
904
- * adds the returned draft to idCache.messages, and returns its numeric ID.
737
+ * and returns a durable `em_` token plus the raw Graph id.
905
738
  */
906
739
  async createDraftAsync(params) {
907
740
  const toRecipients = (params.to ?? []).map(addr => ({
@@ -921,19 +754,15 @@ export class GraphRepository {
921
754
  bccRecipients,
922
755
  });
923
756
  const graphId = draft.id;
924
- const numericId = hashStringToNumber(graphId);
925
- this.idCache.messages.set(numericId, graphId);
926
- return { numericId, graphId };
757
+ return { token: mintSelfEncoded('message', graphId), graphId };
927
758
  }
928
759
  /**
929
760
  * Updates an existing draft message.
930
761
  *
931
- * 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.
932
763
  */
933
764
  async updateDraftAsync(draftId, updates) {
934
- const graphId = this.idCache.messages.get(draftId);
935
- if (graphId == null)
936
- 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');
937
766
  await this.client.updateDraft(graphId, updates);
938
767
  }
939
768
  /**
@@ -947,12 +776,10 @@ export class GraphRepository {
947
776
  /**
948
777
  * Sends an existing draft message.
949
778
  *
950
- * 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.
951
780
  */
952
781
  async sendDraftAsync(draftId) {
953
- const graphId = this.idCache.messages.get(draftId);
954
- if (graphId == null)
955
- 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');
956
783
  await this.client.sendDraft(graphId);
957
784
  }
958
785
  /**
@@ -981,24 +808,20 @@ export class GraphRepository {
981
808
  /**
982
809
  * Replies to a message (or replies all).
983
810
  *
984
- * 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.
985
812
  */
986
813
  async replyMessageAsync(messageId, comment, replyAll) {
987
- const graphId = this.idCache.messages.get(messageId);
988
- if (graphId == null)
989
- 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');
990
815
  await this.client.replyMessage(graphId, comment, replyAll);
991
816
  }
992
817
  /**
993
818
  * Forwards a message to specified recipients.
994
819
  *
995
- * Looks up the Graph string ID from idCache.messages, converts recipient
996
- * 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.
997
822
  */
998
823
  async forwardMessageAsync(messageId, toRecipients, comment) {
999
- const graphId = this.idCache.messages.get(messageId);
1000
- if (graphId == null)
1001
- 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');
1002
825
  const recipients = toRecipients.map(addr => ({
1003
826
  emailAddress: { address: addr },
1004
827
  }));
@@ -1010,42 +833,33 @@ export class GraphRepository {
1010
833
  /**
1011
834
  * Creates a reply (or reply-all) draft for a message.
1012
835
  *
1013
- * Looks up the Graph string ID from idCache.messages, creates the draft
1014
- * 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.
1015
838
  *
1016
- * @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.
1017
840
  */
1018
841
  async replyAsDraftAsync(messageId, replyAll = false, 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.`);
842
+ const graphMessageId = this.toGraphId(messageId, 'message');
1022
843
  // Pass comment/body through createReply so the quoted thread is preserved
1023
844
  const body = comment != null ? { contentType: bodyType, content: comment } : undefined;
1024
845
  const draft = replyAll
1025
846
  ? await this.client.createReplyAllDraft(graphMessageId, undefined, body)
1026
847
  : await this.client.createReplyDraft(graphMessageId, undefined, body);
1027
848
  const graphId = draft.id;
1028
- const numericId = hashStringToNumber(graphId);
1029
- this.idCache.messages.set(numericId, graphId);
1030
- return { numericId, graphId };
849
+ return { token: mintSelfEncoded('message', graphId), graphId };
1031
850
  }
1032
851
  /**
1033
852
  * Creates a forward draft for a message.
1034
853
  *
1035
- * Looks up the Graph string ID from idCache.messages, creates the draft
1036
- * via the client, caches the new draft ID, and optionally updates the
1037
- * 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.
1038
856
  *
1039
- * @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.
1040
858
  */
1041
859
  async forwardAsDraftAsync(messageId, toRecipients, comment, bodyType = 'text') {
1042
- const graphMessageId = this.idCache.messages.get(messageId);
1043
- if (graphMessageId == null)
1044
- 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');
1045
861
  const draft = await this.client.createForwardDraft(graphMessageId);
1046
862
  const graphId = draft.id;
1047
- const numericId = hashStringToNumber(graphId);
1048
- this.idCache.messages.set(numericId, graphId);
1049
863
  const updates = {};
1050
864
  if (toRecipients != null && toRecipients.length > 0) {
1051
865
  updates.toRecipients = toRecipients.map(addr => ({
@@ -1058,7 +872,7 @@ export class GraphRepository {
1058
872
  if (Object.keys(updates).length > 0) {
1059
873
  await this.client.updateDraft(graphId, updates);
1060
874
  }
1061
- return { numericId, graphId };
875
+ return { token: mintSelfEncoded('message', graphId), graphId };
1062
876
  }
1063
877
  // ---------------------------------------------------------------------------
1064
878
  // Calendar Scheduling
@@ -1100,26 +914,18 @@ export class GraphRepository {
1100
914
  /**
1101
915
  * Lists attachments for a given email.
1102
916
  *
1103
- * Looks up the Graph message ID from idCache.messages, calls
1104
- * client.listAttachments, hashes each attachment ID to a numeric key,
1105
- * 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.
1106
919
  *
1107
920
  * @returns Array of attachment metadata objects.
1108
921
  */
1109
922
  async listAttachmentsAsync(emailId) {
1110
- const graphMessageId = this.idCache.messages.get(emailId);
1111
- if (graphMessageId == null)
1112
- 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');
1113
924
  const attachments = await this.client.listAttachments(graphMessageId);
1114
925
  return attachments.map((att) => {
1115
926
  const attId = att.id ?? '';
1116
- const numericId = hashStringToNumber(attId);
1117
- this.idCache.attachments.set(numericId, {
1118
- messageId: graphMessageId,
1119
- attachmentId: attId,
1120
- });
1121
927
  return {
1122
- id: numericId,
928
+ id: this.mintAliasComposite('attachment', { messageId: graphMessageId, attachmentId: attId }),
1123
929
  name: att.name ?? '',
1124
930
  size: att.size ?? 0,
1125
931
  contentType: att.contentType ?? 'application/octet-stream',
@@ -1130,16 +936,14 @@ export class GraphRepository {
1130
936
  /**
1131
937
  * Downloads an attachment for a given email.
1132
938
  *
1133
- * Looks up { messageId, attachmentId } from idCache.attachments,
1134
- * then delegates to the downloadAttachment helper which fetches
1135
- * 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.
1136
942
  *
1137
943
  * @returns Metadata about the downloaded file including its local path.
1138
944
  */
1139
945
  async downloadAttachmentAsync(attachmentId) {
1140
- const cached = this.idCache.attachments.get(attachmentId);
1141
- if (cached == null)
1142
- throw new Error(`Attachment ID ${attachmentId} not found in cache. Call list_attachments first.`);
946
+ const cached = this.toGraphParts(attachmentId, 'attachment', ['messageId', 'attachmentId']);
1143
947
  return downloadAttachment(this.client, cached.messageId, cached.attachmentId);
1144
948
  }
1145
949
  // ===========================================================================
@@ -1185,13 +989,11 @@ export class GraphRepository {
1185
989
  graphEvent.onlineMeetingProvider = params.online_meeting_provider ?? 'teamsForBusiness';
1186
990
  }
1187
991
  const graphCalendarId = params.calendarId != null
1188
- ? this.idCache.folders.get(params.calendarId)
992
+ ? this.toGraphId(params.calendarId, 'folder')
1189
993
  : undefined;
1190
994
  const created = await this.client.createEvent(graphEvent, graphCalendarId);
1191
995
  const graphId = created.id;
1192
- const numericId = hashStringToNumber(graphId);
1193
- this.idCache.events.set(numericId, graphId);
1194
- return numericId;
996
+ return mintSelfEncoded('event', graphId);
1195
997
  }
1196
998
  /**
1197
999
  * Updates an existing calendar event.
@@ -1200,9 +1002,7 @@ export class GraphRepository {
1200
1002
  * client.updateEvent(). Throws if the event is not cached.
1201
1003
  */
1202
1004
  async updateEventAsync(eventId, updates) {
1203
- const graphId = this.idCache.events.get(eventId);
1204
- if (graphId == null)
1205
- 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');
1206
1006
  await this.client.updateEvent(graphId, updates);
1207
1007
  }
1208
1008
  /**
@@ -1213,11 +1013,8 @@ export class GraphRepository {
1213
1013
  * Throws if the event is not cached.
1214
1014
  */
1215
1015
  async deleteEventAsync(eventId) {
1216
- const graphId = this.idCache.events.get(eventId);
1217
- if (graphId == null)
1218
- 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');
1219
1017
  await this.client.deleteEvent(graphId);
1220
- this.idCache.events.delete(eventId);
1221
1018
  }
1222
1019
  /**
1223
1020
  * Responds to a calendar event invitation.
@@ -1226,9 +1023,7 @@ export class GraphRepository {
1226
1023
  * client.respondToEvent(). Throws if the event is not cached.
1227
1024
  */
1228
1025
  async respondToEventAsync(eventId, response, sendResponse, comment) {
1229
- const graphId = this.idCache.events.get(eventId);
1230
- if (graphId == null)
1231
- 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');
1232
1027
  await this.client.respondToEvent(graphId, response, sendResponse, comment);
1233
1028
  }
1234
1029
  // ===========================================================================
@@ -1273,9 +1068,7 @@ export class GraphRepository {
1273
1068
  }
1274
1069
  const created = await this.client.createContact(graphContact);
1275
1070
  const graphId = created.id;
1276
- const numericId = hashStringToNumber(graphId);
1277
- this.idCache.contacts.set(numericId, graphId);
1278
- return numericId;
1071
+ return mintSelfEncoded('contact', graphId);
1279
1072
  }
1280
1073
  /**
1281
1074
  * Updates an existing contact.
@@ -1284,9 +1077,7 @@ export class GraphRepository {
1284
1077
  * client.updateContact(). Throws if the contact is not cached.
1285
1078
  */
1286
1079
  async updateContactAsync(contactId, updates) {
1287
- const graphId = this.idCache.contacts.get(contactId);
1288
- if (graphId == null)
1289
- 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');
1290
1081
  await this.client.updateContact(graphId, updates);
1291
1082
  }
1292
1083
  /**
@@ -1297,11 +1088,8 @@ export class GraphRepository {
1297
1088
  * Throws if the contact is not cached.
1298
1089
  */
1299
1090
  async deleteContactAsync(contactId) {
1300
- const graphId = this.idCache.contacts.get(contactId);
1301
- if (graphId == null)
1302
- 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');
1303
1092
  await this.client.deleteContact(graphId);
1304
- this.idCache.contacts.delete(contactId);
1305
1093
  }
1306
1094
  // ===========================================================================
1307
1095
  // Task Write Operations (Async)
@@ -1309,14 +1097,12 @@ export class GraphRepository {
1309
1097
  /**
1310
1098
  * Creates a new task in a task list.
1311
1099
  *
1312
- * Looks up the Graph task list ID from idCache.taskLists, builds a
1313
- * Graph API task object from the given params, calls client.createTask(),
1314
- * 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.
1315
1103
  */
1316
1104
  async createTaskAsync(params) {
1317
- const graphListId = this.idCache.taskLists.get(params.task_list_id);
1318
- if (graphListId == null)
1319
- 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');
1320
1106
  const graphTask = {
1321
1107
  title: params.title,
1322
1108
  };
@@ -1362,22 +1148,14 @@ export class GraphRepository {
1362
1148
  };
1363
1149
  }
1364
1150
  const created = await this.client.createTask(graphListId, graphTask);
1365
- const graphId = created.id;
1366
- const numericId = hashStringToNumber(graphId);
1367
- this.idCache.tasks.set(numericId, { taskListId: graphListId, taskId: graphId });
1368
- return numericId;
1151
+ return this.mintAliasComposite('task', { taskListId: graphListId, taskId: created.id });
1369
1152
  }
1370
1153
  /**
1371
1154
  * Updates an existing task.
1372
- *
1373
- * Looks up the Graph task info from idCache.tasks, then calls
1374
- * client.updateTask(). Throws if the task is not cached.
1375
1155
  */
1376
1156
  async updateTaskAsync(taskId, updates) {
1377
- const taskInfo = this.idCache.tasks.get(taskId);
1378
- if (taskInfo == null)
1379
- throw new Error(`Task ID ${taskId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1380
- 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);
1381
1159
  }
1382
1160
  /**
1383
1161
  * Marks a task as completed.
@@ -1396,64 +1174,42 @@ export class GraphRepository {
1396
1174
  }
1397
1175
  /**
1398
1176
  * Deletes a task.
1399
- *
1400
- * Looks up the Graph task info from idCache.tasks, calls
1401
- * client.deleteTask(), and removes the entry from idCache.
1402
- * Throws if the task is not cached.
1403
1177
  */
1404
1178
  async deleteTaskAsync(taskId) {
1405
- const taskInfo = this.idCache.tasks.get(taskId);
1406
- if (taskInfo == null)
1407
- throw new Error(`Task ID ${taskId} not found in cache. Try searching for or listing the item first to refresh the cache.`);
1408
- await this.client.deleteTask(taskInfo.taskListId, taskInfo.taskId);
1409
- this.idCache.tasks.delete(taskId);
1179
+ const { taskListId, taskId: gTaskId } = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1180
+ await this.client.deleteTask(taskListId, gTaskId);
1410
1181
  }
1411
1182
  /**
1412
1183
  * Creates a new task list.
1413
- *
1414
- * Calls client.createTaskList(), caches the resulting ID in
1415
- * idCache.taskLists, and returns a numeric ID.
1416
1184
  */
1417
1185
  async createTaskListAsync(displayName) {
1418
1186
  const created = await this.client.createTaskList(displayName);
1419
- const graphId = created.id;
1420
- const numericId = hashStringToNumber(graphId);
1421
- this.idCache.taskLists.set(numericId, graphId);
1422
- return numericId;
1187
+ return this.mintAlias('taskList', created.id);
1423
1188
  }
1424
1189
  /**
1425
1190
  * Renames a task list.
1426
1191
  */
1427
1192
  async renameTaskListAsync(listId, name) {
1428
- const graphId = this.idCache.taskLists.get(listId);
1429
- if (graphId == null)
1430
- 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');
1431
1194
  await this.client.updateTaskList(graphId, { displayName: name });
1432
1195
  }
1433
1196
  /**
1434
1197
  * Deletes a task list.
1435
1198
  */
1436
1199
  async deleteTaskListAsync(listId) {
1437
- const graphId = this.idCache.taskLists.get(listId);
1438
- if (graphId == null)
1439
- 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');
1440
1201
  await this.client.deleteTaskList(graphId);
1441
- this.idCache.taskLists.delete(listId);
1442
1202
  }
1443
1203
  // ===========================================================================
1444
1204
  // Checklist Items
1445
1205
  // ===========================================================================
1446
1206
  async listChecklistItemsAsync(taskId) {
1447
- const taskInfo = this.idCache.tasks.get(taskId);
1448
- if (taskInfo == null)
1449
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1207
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1450
1208
  const items = await this.client.listChecklistItems(taskInfo.taskListId, taskInfo.taskId);
1451
1209
  return items.map((item) => {
1452
1210
  const graphId = item.id;
1453
- const numericId = hashStringToNumber(graphId);
1454
- this.idCache.checklistItems.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: graphId });
1455
1211
  return {
1456
- id: numericId,
1212
+ id: this.mintAliasComposite('checklistItem', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: graphId }),
1457
1213
  displayName: item.displayName ?? '',
1458
1214
  isChecked: item.isChecked ?? false,
1459
1215
  createdDateTime: item.createdDateTime ?? '',
@@ -1461,19 +1217,12 @@ export class GraphRepository {
1461
1217
  });
1462
1218
  }
1463
1219
  async createChecklistItemAsync(taskId, displayName, isChecked = false) {
1464
- const taskInfo = this.idCache.tasks.get(taskId);
1465
- if (taskInfo == null)
1466
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1220
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1467
1221
  const item = await this.client.createChecklistItem(taskInfo.taskListId, taskInfo.taskId, displayName, isChecked);
1468
- const graphId = item.id;
1469
- const numericId = hashStringToNumber(graphId);
1470
- this.idCache.checklistItems.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: graphId });
1471
- return numericId;
1222
+ return this.mintAliasComposite('checklistItem', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, checklistItemId: item.id });
1472
1223
  }
1473
1224
  async updateChecklistItemAsync(checklistItemId, updates) {
1474
- const cached = this.idCache.checklistItems.get(checklistItemId);
1475
- if (cached == null)
1476
- 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']);
1477
1226
  const graphUpdates = {};
1478
1227
  if (updates.displayName != null)
1479
1228
  graphUpdates['displayName'] = updates.displayName;
@@ -1482,26 +1231,19 @@ export class GraphRepository {
1482
1231
  await this.client.updateChecklistItem(cached.taskListId, cached.taskId, cached.checklistItemId, graphUpdates);
1483
1232
  }
1484
1233
  async deleteChecklistItemAsync(checklistItemId) {
1485
- const cached = this.idCache.checklistItems.get(checklistItemId);
1486
- if (cached == null)
1487
- 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']);
1488
1235
  await this.client.deleteChecklistItem(cached.taskListId, cached.taskId, cached.checklistItemId);
1489
- this.idCache.checklistItems.delete(checklistItemId);
1490
1236
  }
1491
1237
  // ===========================================================================
1492
1238
  // Linked Resources
1493
1239
  // ===========================================================================
1494
1240
  async listLinkedResourcesAsync(taskId) {
1495
- const taskInfo = this.idCache.tasks.get(taskId);
1496
- if (taskInfo == null)
1497
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1241
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1498
1242
  const items = await this.client.listLinkedResources(taskInfo.taskListId, taskInfo.taskId);
1499
1243
  return items.map((item) => {
1500
1244
  const graphId = item.id;
1501
- const numericId = hashStringToNumber(graphId);
1502
- this.idCache.linkedResources.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: graphId });
1503
1245
  return {
1504
- id: numericId,
1246
+ id: this.mintAliasComposite('linkedResource', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: graphId }),
1505
1247
  webUrl: item.webUrl ?? '',
1506
1248
  applicationName: item.applicationName ?? '',
1507
1249
  displayName: item.displayName ?? '',
@@ -1509,36 +1251,24 @@ export class GraphRepository {
1509
1251
  });
1510
1252
  }
1511
1253
  async createLinkedResourceAsync(taskId, webUrl, applicationName, displayName) {
1512
- const taskInfo = this.idCache.tasks.get(taskId);
1513
- if (taskInfo == null)
1514
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1254
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1515
1255
  const item = await this.client.createLinkedResource(taskInfo.taskListId, taskInfo.taskId, webUrl, applicationName, displayName);
1516
- const graphId = item.id;
1517
- const numericId = hashStringToNumber(graphId);
1518
- this.idCache.linkedResources.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: graphId });
1519
- return numericId;
1256
+ return this.mintAliasComposite('linkedResource', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, linkedResourceId: item.id });
1520
1257
  }
1521
1258
  async deleteLinkedResourceAsync(linkedResourceId) {
1522
- const cached = this.idCache.linkedResources.get(linkedResourceId);
1523
- if (cached == null)
1524
- 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']);
1525
1260
  await this.client.deleteLinkedResource(cached.taskListId, cached.taskId, cached.linkedResourceId);
1526
- this.idCache.linkedResources.delete(linkedResourceId);
1527
1261
  }
1528
1262
  // ===========================================================================
1529
1263
  // Task Attachments
1530
1264
  // ===========================================================================
1531
1265
  async listTaskAttachmentsAsync(taskId) {
1532
- const taskInfo = this.idCache.tasks.get(taskId);
1533
- if (taskInfo == null)
1534
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1266
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1535
1267
  const items = await this.client.listTaskAttachments(taskInfo.taskListId, taskInfo.taskId);
1536
1268
  return items.map((item) => {
1537
1269
  const graphId = item.id;
1538
- const numericId = hashStringToNumber(graphId);
1539
- this.idCache.taskAttachments.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: graphId });
1540
1270
  return {
1541
- id: numericId,
1271
+ id: this.mintAliasComposite('taskAttachment', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: graphId }),
1542
1272
  name: item['name'] ?? '',
1543
1273
  size: item.size ?? 0,
1544
1274
  contentType: item.contentType ?? '',
@@ -1546,21 +1276,13 @@ export class GraphRepository {
1546
1276
  });
1547
1277
  }
1548
1278
  async createTaskAttachmentAsync(taskId, name, contentBytes, contentType) {
1549
- const taskInfo = this.idCache.tasks.get(taskId);
1550
- if (taskInfo == null)
1551
- throw new Error(`Task ID ${taskId} not found in cache. Try listing tasks first.`);
1279
+ const taskInfo = this.toGraphParts(taskId, 'task', ['taskListId', 'taskId']);
1552
1280
  const item = await this.client.createTaskAttachment(taskInfo.taskListId, taskInfo.taskId, name, contentBytes, contentType);
1553
- const graphId = item.id;
1554
- const numericId = hashStringToNumber(graphId);
1555
- this.idCache.taskAttachments.set(numericId, { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: graphId });
1556
- return numericId;
1281
+ return this.mintAliasComposite('taskAttachment', { taskListId: taskInfo.taskListId, taskId: taskInfo.taskId, attachmentId: item.id });
1557
1282
  }
1558
1283
  async deleteTaskAttachmentAsync(taskAttachmentId) {
1559
- const cached = this.idCache.taskAttachments.get(taskAttachmentId);
1560
- if (cached == null)
1561
- 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']);
1562
1285
  await this.client.deleteTaskAttachment(cached.taskListId, cached.taskId, cached.attachmentId);
1563
- this.idCache.taskAttachments.delete(taskAttachmentId);
1564
1286
  }
1565
1287
  // ===========================================================================
1566
1288
  // Mail Rules (Async)
@@ -1572,10 +1294,8 @@ export class GraphRepository {
1572
1294
  const rules = await this.client.listMailRules();
1573
1295
  return rules.map((rule) => {
1574
1296
  const graphId = rule.id;
1575
- const numericId = hashStringToNumber(graphId);
1576
- this.idCache.rules.set(numericId, graphId);
1577
1297
  return {
1578
- id: numericId,
1298
+ id: this.mintAlias('mailRule', graphId),
1579
1299
  displayName: rule.displayName ?? '',
1580
1300
  sequence: rule.sequence ?? 0,
1581
1301
  isEnabled: rule.isEnabled ?? true,
@@ -1589,20 +1309,14 @@ export class GraphRepository {
1589
1309
  */
1590
1310
  async createMailRuleAsync(rule) {
1591
1311
  const created = await this.client.createMailRule(rule);
1592
- const graphId = created.id;
1593
- const numericId = hashStringToNumber(graphId);
1594
- this.idCache.rules.set(numericId, graphId);
1595
- return numericId;
1312
+ return this.mintAlias('mailRule', created.id);
1596
1313
  }
1597
1314
  /**
1598
1315
  * Deletes an inbox mail rule.
1599
1316
  */
1600
1317
  async deleteMailRuleAsync(ruleId) {
1601
- const graphId = this.idCache.rules.get(ruleId);
1602
- if (graphId == null)
1603
- 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');
1604
1319
  await this.client.deleteMailRule(graphId);
1605
- this.idCache.rules.delete(ruleId);
1606
1320
  }
1607
1321
  // ===========================================================================
1608
1322
  // Automatic Replies (Out of Office)
@@ -1682,10 +1396,8 @@ export class GraphRepository {
1682
1396
  const categories = await this.client.listMasterCategories();
1683
1397
  return categories.map((cat) => {
1684
1398
  const graphId = cat.id;
1685
- const numericId = hashStringToNumber(graphId);
1686
- this.idCache.categories.set(numericId, graphId);
1687
1399
  return {
1688
- id: numericId,
1400
+ id: this.mintAlias('category', graphId),
1689
1401
  name: cat.displayName ?? '',
1690
1402
  color: cat.color ?? 'none',
1691
1403
  };
@@ -1696,20 +1408,14 @@ export class GraphRepository {
1696
1408
  */
1697
1409
  async createCategoryAsync(name, color) {
1698
1410
  const created = await this.client.createMasterCategory(name, color);
1699
- const graphId = created.id;
1700
- const numericId = hashStringToNumber(graphId);
1701
- this.idCache.categories.set(numericId, graphId);
1702
- return numericId;
1411
+ return this.mintAlias('category', created.id);
1703
1412
  }
1704
1413
  /**
1705
1414
  * Deletes a master category.
1706
1415
  */
1707
1416
  async deleteCategoryAsync(categoryId) {
1708
- const graphId = this.idCache.categories.get(categoryId);
1709
- if (graphId == null)
1710
- 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');
1711
1418
  await this.client.deleteMasterCategory(graphId);
1712
- this.idCache.categories.delete(categoryId);
1713
1419
  }
1714
1420
  // ===========================================================================
1715
1421
  // Focused Inbox Overrides
@@ -1721,10 +1427,8 @@ export class GraphRepository {
1721
1427
  const overrides = await this.client.listFocusedOverrides();
1722
1428
  return overrides.map((o) => {
1723
1429
  const graphId = o.id;
1724
- const numericId = hashStringToNumber(graphId);
1725
- this.idCache.focusedOverrides.set(numericId, graphId);
1726
1430
  return {
1727
- id: numericId,
1431
+ id: this.mintAlias('focusedOverride', graphId),
1728
1432
  senderAddress: o.senderEmailAddress?.address ?? '',
1729
1433
  classifyAs: o.classifyAs ?? '',
1730
1434
  };
@@ -1735,20 +1439,14 @@ export class GraphRepository {
1735
1439
  */
1736
1440
  async createFocusedOverrideAsync(senderAddress, classifyAs) {
1737
1441
  const created = await this.client.createFocusedOverride(senderAddress, classifyAs);
1738
- const graphId = created.id;
1739
- const numericId = hashStringToNumber(graphId);
1740
- this.idCache.focusedOverrides.set(numericId, graphId);
1741
- return numericId;
1442
+ return this.mintAlias('focusedOverride', created.id);
1742
1443
  }
1743
1444
  /**
1744
1445
  * Deletes a focused inbox override.
1745
1446
  */
1746
1447
  async deleteFocusedOverrideAsync(overrideId) {
1747
- const graphId = this.idCache.focusedOverrides.get(overrideId);
1748
- if (graphId == null)
1749
- 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');
1750
1449
  await this.client.deleteFocusedOverride(graphId);
1751
- this.idCache.focusedOverrides.delete(overrideId);
1752
1450
  }
1753
1451
  // ===========================================================================
1754
1452
  // Message Headers & MIME
@@ -1757,21 +1455,17 @@ export class GraphRepository {
1757
1455
  * Gets internet message headers for an email.
1758
1456
  */
1759
1457
  async getMessageHeadersAsync(emailId) {
1760
- const graphId = this.idCache.messages.get(emailId);
1761
- if (graphId == null)
1762
- 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');
1763
1459
  return await this.client.getMessageHeaders(graphId);
1764
1460
  }
1765
1461
  /**
1766
1462
  * Gets the MIME content of a message and saves it as an .eml file.
1767
1463
  */
1768
1464
  async getMessageMimeAsync(emailId) {
1769
- const graphId = this.idCache.messages.get(emailId);
1770
- if (graphId == null)
1771
- 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');
1772
1466
  const mime = await this.client.getMessageMime(graphId);
1773
1467
  const downloadDir = getDownloadDir();
1774
- 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`);
1775
1469
  fs.writeFileSync(filePath, mime, 'utf-8');
1776
1470
  return { filePath };
1777
1471
  }
@@ -1802,15 +1496,16 @@ export class GraphRepository {
1802
1496
  // ===========================================================================
1803
1497
  /**
1804
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.
1805
1502
  */
1806
1503
  async listCalendarGroupsAsync() {
1807
1504
  const groups = await this.client.listCalendarGroups();
1808
1505
  return groups.map((group) => {
1809
1506
  const graphId = group.id;
1810
- const numericId = hashStringToNumber(graphId);
1811
- this.idCache.calendarGroups.set(numericId, graphId);
1812
1507
  return {
1813
- id: numericId,
1508
+ id: graphId,
1814
1509
  name: group.name ?? '',
1815
1510
  classId: group.classId?.toString() ?? '',
1816
1511
  };
@@ -1821,10 +1516,7 @@ export class GraphRepository {
1821
1516
  */
1822
1517
  async createCalendarGroupAsync(name) {
1823
1518
  const created = await this.client.createCalendarGroup(name);
1824
- const graphId = created.id;
1825
- const numericId = hashStringToNumber(graphId);
1826
- this.idCache.calendarGroups.set(numericId, graphId);
1827
- return numericId;
1519
+ return created.id;
1828
1520
  }
1829
1521
  // ===========================================================================
1830
1522
  // Calendar Permissions
@@ -1833,17 +1525,12 @@ export class GraphRepository {
1833
1525
  * Lists all permissions for a calendar.
1834
1526
  */
1835
1527
  async listCalendarPermissionsAsync(calendarId) {
1836
- const graphCalendarId = this.idCache.calendars.get(calendarId);
1837
- if (graphCalendarId == null) {
1838
- throw new Error(`Calendar ID ${calendarId} not found in cache. Please call list_calendars first.`);
1839
- }
1528
+ const graphCalendarId = this.toGraphId(calendarId, 'folder');
1840
1529
  const permissions = await this.client.listCalendarPermissions(graphCalendarId);
1841
1530
  return permissions.map((perm) => {
1842
1531
  const graphPermId = perm.id;
1843
- const numericId = hashStringToNumber(graphPermId);
1844
- this.idCache.calendarPermissions.set(numericId, { calendarId: graphCalendarId, permissionId: graphPermId });
1845
1532
  return {
1846
- id: numericId,
1533
+ id: this.mintAliasComposite('calendarPermission', { calendarId: graphCalendarId, permissionId: graphPermId }),
1847
1534
  emailAddress: perm.emailAddress?.address ?? '',
1848
1535
  role: perm.role ?? 'none',
1849
1536
  isRemovable: perm.isRemovable ?? false,
@@ -1855,10 +1542,7 @@ export class GraphRepository {
1855
1542
  * Creates a calendar permission (shares a calendar with someone).
1856
1543
  */
1857
1544
  async createCalendarPermissionAsync(calendarId, email, role) {
1858
- const graphCalendarId = this.idCache.calendars.get(calendarId);
1859
- if (graphCalendarId == null) {
1860
- throw new Error(`Calendar ID ${calendarId} not found in cache. Please call list_calendars first.`);
1861
- }
1545
+ const graphCalendarId = this.toGraphId(calendarId, 'folder');
1862
1546
  const permission = await this.client.createCalendarPermission(graphCalendarId, {
1863
1547
  emailAddress: {
1864
1548
  address: email,
@@ -1866,21 +1550,14 @@ export class GraphRepository {
1866
1550
  },
1867
1551
  role,
1868
1552
  });
1869
- const graphPermId = permission.id;
1870
- const numericId = hashStringToNumber(graphPermId);
1871
- this.idCache.calendarPermissions.set(numericId, { calendarId: graphCalendarId, permissionId: graphPermId });
1872
- return numericId;
1553
+ return this.mintAliasComposite('calendarPermission', { calendarId: graphCalendarId, permissionId: permission.id });
1873
1554
  }
1874
1555
  /**
1875
1556
  * Deletes a calendar permission.
1876
1557
  */
1877
1558
  async deleteCalendarPermissionAsync(permissionId) {
1878
- const cached = this.idCache.calendarPermissions.get(permissionId);
1879
- if (cached == null) {
1880
- throw new Error(`Calendar permission ID ${permissionId} not found in cache. Please call list_calendar_permissions first.`);
1881
- }
1882
- await this.client.deleteCalendarPermission(cached.calendarId, cached.permissionId);
1883
- this.idCache.calendarPermissions.delete(permissionId);
1559
+ const { calendarId, permissionId: permId } = this.toGraphParts(permissionId, 'calendarPermission', ['calendarId', 'permissionId']);
1560
+ await this.client.deleteCalendarPermission(calendarId, permId);
1884
1561
  }
1885
1562
  // ===========================================================================
1886
1563
  // Room Lists & Rooms
@@ -1915,9 +1592,7 @@ export class GraphRepository {
1915
1592
  const teams = await this.client.listJoinedTeams();
1916
1593
  return teams.map((team) => {
1917
1594
  const graphId = team.id;
1918
- const numericId = hashStringToNumber(graphId);
1919
- this.idCache.teams.set(numericId, graphId);
1920
- return { id: numericId, name: team.displayName ?? '', description: team.description ?? '' };
1595
+ return { id: this.mintAlias('team', graphId), name: team.displayName ?? '', description: team.description ?? '' };
1921
1596
  });
1922
1597
  }
1923
1598
  /**
@@ -1928,20 +1603,17 @@ export class GraphRepository {
1928
1603
  const channels = await this.client.listChannels(graphTeamId);
1929
1604
  return channels.map((ch) => {
1930
1605
  const graphId = ch.id;
1931
- const numericId = hashStringToNumber(graphId);
1932
- this.idCache.channels.set(numericId, { teamId: graphTeamId, channelId: graphId });
1933
- 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' };
1934
1608
  });
1935
1609
  }
1936
1610
  /**
1937
- * Gets a specific channel by cached numeric ID.
1611
+ * Gets a specific channel by durable cn_ token.
1938
1612
  */
1939
1613
  async getChannelAsync(channelId) {
1940
- const cached = this.idCache.channels.get(channelId);
1941
- if (cached == null)
1942
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
1943
- const ch = await this.client.getChannel(cached.teamId, cached.channelId);
1944
- 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 ?? '' };
1945
1617
  }
1946
1618
  /**
1947
1619
  * Creates a new channel in a team.
@@ -1949,34 +1621,26 @@ export class GraphRepository {
1949
1621
  async createChannelAsync(teamId, name, description) {
1950
1622
  const graphTeamId = await this.resolveTeamId(teamId);
1951
1623
  const ch = await this.client.createChannel(graphTeamId, name, description);
1952
- const graphId = ch.id;
1953
- const numericId = hashStringToNumber(graphId);
1954
- this.idCache.channels.set(numericId, { teamId: graphTeamId, channelId: graphId });
1955
- return numericId;
1624
+ return this.mintAliasComposite('channel', { teamId: graphTeamId, channelId: ch.id });
1956
1625
  }
1957
1626
  /**
1958
1627
  * Updates a channel's properties.
1959
1628
  */
1960
1629
  async updateChannelAsync(channelId, updates) {
1961
- const cached = this.idCache.channels.get(channelId);
1962
- if (cached == null)
1963
- 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']);
1964
1631
  const graphUpdates = {};
1965
1632
  if (updates.name != null)
1966
1633
  graphUpdates['displayName'] = updates.name;
1967
1634
  if (updates.description != null)
1968
1635
  graphUpdates['description'] = updates.description;
1969
- await this.client.updateChannel(cached.teamId, cached.channelId, graphUpdates);
1636
+ await this.client.updateChannel(teamId, graphChannelId, graphUpdates);
1970
1637
  }
1971
1638
  /**
1972
1639
  * Deletes a channel.
1973
1640
  */
1974
1641
  async deleteChannelAsync(channelId) {
1975
- const cached = this.idCache.channels.get(channelId);
1976
- if (cached == null)
1977
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
1978
- await this.client.deleteChannel(cached.teamId, cached.channelId);
1979
- this.idCache.channels.delete(channelId);
1642
+ const { teamId, channelId: graphChannelId } = this.toGraphParts(channelId, 'channel', ['teamId', 'channelId']);
1643
+ await this.client.deleteChannel(teamId, graphChannelId);
1980
1644
  }
1981
1645
  /**
1982
1646
  * Lists members of a team.
@@ -1998,16 +1662,12 @@ export class GraphRepository {
1998
1662
  * Lists recent messages in a channel.
1999
1663
  */
2000
1664
  async listChannelMessagesAsync(channelId, limit = 25) {
2001
- const cached = this.idCache.channels.get(channelId);
2002
- if (cached == null)
2003
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
2004
- 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);
2005
1667
  return messages.map((msg) => {
2006
1668
  const graphId = msg.id;
2007
- const numericId = hashStringToNumber(graphId);
2008
- this.idCache.channelMessages.set(numericId, { teamId: cached.teamId, channelId: cached.channelId, messageId: graphId });
2009
1669
  return {
2010
- id: numericId,
1670
+ id: this.mintAliasComposite('channelMessage', { teamId, channelId: graphChannelId, messageId: graphId }),
2011
1671
  senderName: msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? '',
2012
1672
  senderEmail: msg.from?.user?.email ?? '',
2013
1673
  bodyPreview: msg.body?.content?.substring(0, 200) ?? '',
@@ -2021,18 +1681,21 @@ export class GraphRepository {
2021
1681
  * Gets a specific channel message with its replies.
2022
1682
  */
2023
1683
  async getChannelMessageAsync(messageId) {
2024
- const cached = this.idCache.channelMessages.get(messageId);
2025
- if (cached == null)
2026
- 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']);
2027
1685
  const [msg, repliesRaw] = await Promise.all([
2028
- this.client.getChannelMessage(cached.teamId, cached.channelId, cached.messageId),
2029
- this.client.listChannelMessageReplies(cached.teamId, cached.channelId, cached.messageId),
1686
+ this.client.getChannelMessage(teamId, channelId, graphMessageId),
1687
+ this.client.listChannelMessageReplies(teamId, channelId, graphMessageId),
2030
1688
  ]);
2031
1689
  const replies = repliesRaw.map((r) => {
2032
1690
  const rGraphId = r.id;
2033
- 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.
2034
1697
  return {
2035
- id: rNumericId,
1698
+ id: this.mintAliasComposite('channelMessage', { teamId, channelId, messageId: rGraphId }),
2036
1699
  senderName: r.from?.user?.displayName ?? r.from?.application?.displayName ?? '',
2037
1700
  senderEmail: r.from?.user?.email ?? '',
2038
1701
  bodyContent: r.body?.content ?? '',
@@ -2041,7 +1704,7 @@ export class GraphRepository {
2041
1704
  };
2042
1705
  });
2043
1706
  return {
2044
- id: messageId,
1707
+ id: String(messageId),
2045
1708
  senderName: msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? '',
2046
1709
  senderEmail: msg.from?.user?.email ?? '',
2047
1710
  bodyContent: msg.body?.content ?? '',
@@ -2054,27 +1717,18 @@ export class GraphRepository {
2054
1717
  * Sends a new message to a channel.
2055
1718
  */
2056
1719
  async sendChannelMessageAsync(channelId, body, contentType = 'html') {
2057
- const cached = this.idCache.channels.get(channelId);
2058
- if (cached == null)
2059
- throw new Error(`Channel ID ${channelId} not found in cache. Try listing channels first.`);
2060
- 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);
2061
1722
  const graphId = msg.id;
2062
- const numericId = hashStringToNumber(graphId);
2063
- this.idCache.channelMessages.set(numericId, { teamId: cached.teamId, channelId: cached.channelId, messageId: graphId });
2064
- return numericId;
1723
+ return this.mintAliasComposite('channelMessage', { teamId, channelId: graphChannelId, messageId: graphId });
2065
1724
  }
2066
1725
  /**
2067
1726
  * Replies to a channel message.
2068
1727
  */
2069
1728
  async replyToChannelMessageAsync(messageId, body, contentType = 'html') {
2070
- const cached = this.idCache.channelMessages.get(messageId);
2071
- if (cached == null)
2072
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2073
- const reply = await this.client.replyToChannelMessage(cached.teamId, cached.channelId, cached.messageId, body, contentType);
2074
- const graphId = reply.id;
2075
- const numericId = hashStringToNumber(graphId);
2076
- this.idCache.channelMessages.set(numericId, { teamId: cached.teamId, channelId: cached.channelId, messageId: graphId });
2077
- 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 });
2078
1732
  }
2079
1733
  // ===========================================================================
2080
1734
  // Chats
@@ -2083,14 +1737,12 @@ export class GraphRepository {
2083
1737
  const chats = await this.client.listChats(limit);
2084
1738
  return chats.map((chat) => {
2085
1739
  const graphId = chat.id;
2086
- const numericId = hashStringToNumber(graphId);
2087
- this.idCache.chats.set(numericId, graphId);
2088
1740
  const chatRecord = chat;
2089
1741
  const preview = chatRecord.lastMessagePreview;
2090
1742
  const previewBody = preview?.body;
2091
1743
  const previewContent = previewBody?.content?.substring(0, 200) ?? '';
2092
1744
  return {
2093
- id: numericId,
1745
+ id: this.mintAlias('chat', graphId),
2094
1746
  topic: chat.topic ?? '',
2095
1747
  chatType: chat.chatType ?? 'oneOnOne',
2096
1748
  lastMessagePreview: previewContent,
@@ -2102,7 +1754,7 @@ export class GraphRepository {
2102
1754
  const graphId = await this.resolveChatId(chatId);
2103
1755
  const chat = await this.client.getChat(graphId);
2104
1756
  return {
2105
- id: chatId,
1757
+ id: String(chatId),
2106
1758
  topic: chat.topic ?? '',
2107
1759
  chatType: chat.chatType ?? 'oneOnOne',
2108
1760
  createdDateTime: chat.createdDateTime ?? '',
@@ -2114,10 +1766,8 @@ export class GraphRepository {
2114
1766
  const messages = await this.client.listChatMessages(graphChatId, limit);
2115
1767
  return messages.map((msg) => {
2116
1768
  const graphId = msg.id;
2117
- const numericId = hashStringToNumber(graphId);
2118
- this.idCache.chatMessages.set(numericId, { chatId: graphChatId, messageId: graphId });
2119
1769
  return {
2120
- id: numericId,
1770
+ id: this.mintAliasComposite('chatMessage', { chatId: graphChatId, messageId: graphId }),
2121
1771
  senderName: msg.from?.user?.displayName ?? msg.from?.application?.displayName ?? '',
2122
1772
  senderEmail: msg.from?.user?.email ?? '',
2123
1773
  bodyPreview: msg.body?.content?.substring(0, 200) ?? '',
@@ -2131,9 +1781,7 @@ export class GraphRepository {
2131
1781
  const graphChatId = await this.resolveChatId(chatId);
2132
1782
  const msg = await this.client.sendChatMessage(graphChatId, body, contentType);
2133
1783
  const graphId = msg.id;
2134
- const numericId = hashStringToNumber(graphId);
2135
- this.idCache.chatMessages.set(numericId, { chatId: graphChatId, messageId: graphId });
2136
- return numericId;
1784
+ return this.mintAliasComposite('chatMessage', { chatId: graphChatId, messageId: graphId });
2137
1785
  }
2138
1786
  // ===========================================================================
2139
1787
  // Message Reactions
@@ -2152,46 +1800,34 @@ export class GraphRepository {
2152
1800
  });
2153
1801
  };
2154
1802
  if (messageType === 'channel') {
2155
- const cached = this.idCache.channelMessages.get(messageId);
2156
- if (cached == null)
2157
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2158
- 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);
2159
1805
  return mapReactions(msg);
2160
1806
  }
2161
1807
  else {
2162
- const cached = this.idCache.chatMessages.get(messageId);
2163
- if (cached == null)
2164
- throw new Error(`Message ID ${messageId} not found in cache. Try listing chat messages first.`);
2165
- 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);
2166
1810
  return mapReactions(msg);
2167
1811
  }
2168
1812
  }
2169
1813
  async addMessageReactionAsync(messageId, messageType, reactionType) {
2170
1814
  if (messageType === 'channel') {
2171
- const cached = this.idCache.channelMessages.get(messageId);
2172
- if (cached == null)
2173
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2174
- 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);
2175
1817
  }
2176
1818
  else {
2177
- const cached = this.idCache.chatMessages.get(messageId);
2178
- if (cached == null)
2179
- throw new Error(`Message ID ${messageId} not found in cache. Try listing chat messages first.`);
2180
- 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);
2181
1821
  }
2182
1822
  }
2183
1823
  async removeMessageReactionAsync(messageId, messageType, reactionType) {
2184
1824
  if (messageType === 'channel') {
2185
- const cached = this.idCache.channelMessages.get(messageId);
2186
- if (cached == null)
2187
- throw new Error(`Message ID ${messageId} not found in cache. Try listing channel messages first.`);
2188
- 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);
2189
1827
  }
2190
1828
  else {
2191
- const cached = this.idCache.chatMessages.get(messageId);
2192
- if (cached == null)
2193
- throw new Error(`Message ID ${messageId} not found in cache. Try listing chat messages first.`);
2194
- 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);
2195
1831
  }
2196
1832
  }
2197
1833
  async listChatMembersAsync(chatId) {
@@ -2207,17 +1843,14 @@ export class GraphRepository {
2207
1843
  // Planner Plans
2208
1844
  // ===========================================================================
2209
1845
  /**
2210
- * Lists all plans the current user has.
1846
+ * Lists all plans the current user has, minting durable pl_ tokens.
2211
1847
  */
2212
1848
  async listPlansAsync() {
2213
1849
  const plans = await this.client.listPlans();
2214
1850
  return plans.map((plan) => {
2215
1851
  const graphId = plan.id;
2216
- const numericId = hashStringToNumber(graphId);
2217
- const etag = plan['@odata.etag'] ?? '';
2218
- this.idCache.plans.set(numericId, { planId: graphId, etag });
2219
1852
  return {
2220
- id: numericId,
1853
+ id: this.mintAlias('plan', graphId),
2221
1854
  title: plan.title ?? '',
2222
1855
  owner: plan.owner ?? '',
2223
1856
  createdDateTime: plan.createdDateTime ?? '',
@@ -2225,19 +1858,17 @@ export class GraphRepository {
2225
1858
  });
2226
1859
  }
2227
1860
  /**
2228
- * Gets a specific plan by cached numeric ID.
1861
+ * Gets a specific plan.
2229
1862
  */
2230
1863
  async getPlanAsync(planId) {
2231
- const cached = await this.resolvePlanId(planId);
2232
- const plan = await this.client.getPlan(cached.planId);
2233
- const etag = plan['@odata.etag'] ?? '';
2234
- this.idCache.plans.set(planId, { planId: cached.planId, etag });
1864
+ const graphPlanId = await this.resolvePlanId(planId);
1865
+ const plan = await this.client.getPlan(graphPlanId);
2235
1866
  return {
2236
- id: planId,
1867
+ id: String(planId),
2237
1868
  title: plan.title ?? '',
2238
1869
  owner: plan.owner ?? '',
2239
1870
  createdDateTime: plan.createdDateTime ?? '',
2240
- etag,
1871
+ etag: this.extractEtag(plan),
2241
1872
  };
2242
1873
  }
2243
1874
  /**
@@ -2245,42 +1876,34 @@ export class GraphRepository {
2245
1876
  */
2246
1877
  async createPlanAsync(title, groupId) {
2247
1878
  const plan = await this.client.createPlan(title, groupId);
2248
- const graphId = plan.id;
2249
- const numericId = hashStringToNumber(graphId);
2250
- const etag = plan['@odata.etag'] ?? '';
2251
- this.idCache.plans.set(numericId, { planId: graphId, etag });
2252
- return numericId;
1879
+ return this.mintAlias('plan', plan.id);
2253
1880
  }
2254
1881
  /**
2255
- * 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).
2256
1884
  */
2257
1885
  async updatePlanAsync(planId, updates) {
2258
- const cached = await this.resolvePlanId(planId);
1886
+ const graphPlanId = await this.resolvePlanId(planId);
2259
1887
  const graphUpdates = {};
2260
1888
  if (updates.title != null)
2261
1889
  graphUpdates['title'] = updates.title;
2262
- const result = await this.client.updatePlan(cached.planId, graphUpdates, cached.etag);
2263
- const newEtag = result['@odata.etag'] ?? cached.etag;
2264
- 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));
2265
1891
  }
2266
1892
  // ===========================================================================
2267
1893
  // Planner Buckets
2268
1894
  // ===========================================================================
2269
1895
  /**
2270
- * Lists all buckets in a plan.
1896
+ * Lists all buckets in a plan, minting durable pb_ tokens.
2271
1897
  */
2272
1898
  async listBucketsAsync(planId) {
2273
- const cached = await this.resolvePlanId(planId);
2274
- const buckets = await this.client.listBuckets(cached.planId);
1899
+ const graphPlanId = await this.resolvePlanId(planId);
1900
+ const buckets = await this.client.listBuckets(graphPlanId);
2275
1901
  return buckets.map((bucket) => {
2276
1902
  const graphId = bucket.id;
2277
- const numericId = hashStringToNumber(graphId);
2278
- const etag = bucket['@odata.etag'] ?? '';
2279
- this.idCache.plannerBuckets.set(numericId, { planId: cached.planId, bucketId: graphId, etag });
2280
1903
  return {
2281
- id: numericId,
1904
+ id: this.mintAlias('plannerBucket', graphId),
2282
1905
  name: bucket.name ?? '',
2283
- planId,
1906
+ planId: String(planId),
2284
1907
  orderHint: bucket.orderHint ?? '',
2285
1908
  };
2286
1909
  });
@@ -2289,61 +1912,42 @@ export class GraphRepository {
2289
1912
  * Creates a new bucket in a plan.
2290
1913
  */
2291
1914
  async createBucketAsync(planId, name) {
2292
- const cached = await this.resolvePlanId(planId);
2293
- const bucket = await this.client.createBucket(cached.planId, name);
2294
- const graphId = bucket.id;
2295
- const numericId = hashStringToNumber(graphId);
2296
- const etag = bucket['@odata.etag'] ?? '';
2297
- this.idCache.plannerBuckets.set(numericId, { planId: cached.planId, bucketId: graphId, etag });
2298
- 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);
2299
1918
  }
2300
1919
  /**
2301
- * Updates a bucket (requires cached ETag).
1920
+ * Updates a bucket (U5b-5: fetches a fresh etag immediately before the write).
2302
1921
  */
2303
1922
  async updateBucketAsync(bucketId, updates) {
2304
- const cached = this.idCache.plannerBuckets.get(bucketId);
2305
- if (cached == null)
2306
- throw new Error(`Bucket ID ${bucketId} not found in cache. Try listing buckets first.`);
1923
+ const graphBucketId = this.toGraphId(bucketId, 'plannerBucket');
2307
1924
  const graphUpdates = {};
2308
1925
  if (updates.name != null)
2309
1926
  graphUpdates['name'] = updates.name;
2310
- const result = await this.client.updateBucket(cached.bucketId, graphUpdates, cached.etag);
2311
- const newEtag = result['@odata.etag'] ?? cached.etag;
2312
- 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));
2313
1928
  }
2314
1929
  /**
2315
- * Deletes a bucket (requires cached ETag).
1930
+ * Deletes a bucket (U5b-5: fetches a fresh etag immediately before the write).
2316
1931
  */
2317
1932
  async deleteBucketAsync(bucketId) {
2318
- const cached = this.idCache.plannerBuckets.get(bucketId);
2319
- if (cached == null)
2320
- throw new Error(`Bucket ID ${bucketId} not found in cache. Try listing buckets first.`);
2321
- await this.client.deleteBucket(cached.bucketId, cached.etag);
2322
- 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));
2323
1935
  }
2324
1936
  // ===========================================================================
2325
1937
  // Planner Tasks
2326
1938
  // ===========================================================================
2327
1939
  /**
2328
- * Lists all tasks in a plan.
1940
+ * Lists all tasks in a plan, minting durable pt_ tokens.
2329
1941
  */
2330
1942
  async listPlannerTasksAsync(planId) {
2331
- const cached = await this.resolvePlanId(planId);
2332
- const tasks = await this.client.listPlannerTasks(cached.planId);
1943
+ const graphPlanId = await this.resolvePlanId(planId);
1944
+ const tasks = await this.client.listPlannerTasks(graphPlanId);
2333
1945
  return tasks.map((task) => {
2334
1946
  const graphId = task.id;
2335
- const numericId = hashStringToNumber(graphId);
2336
- const etag = task['@odata.etag'] ?? '';
2337
- this.idCache.plannerTasks.set(numericId, { taskId: graphId, etag });
2338
- // Resolve bucket numeric ID if present
2339
- let bucketNumericId = null;
2340
- if (task.bucketId != null) {
2341
- bucketNumericId = hashStringToNumber(task.bucketId);
2342
- }
2343
1947
  return {
2344
- id: numericId,
1948
+ id: this.mintAlias('plannerTask', graphId),
2345
1949
  title: task.title ?? '',
2346
- bucketId: bucketNumericId,
1950
+ bucketId: task.bucketId != null ? this.mintAlias('plannerBucket', task.bucketId) : null,
2347
1951
  assignees: task.assignments != null ? Object.keys(task.assignments) : [],
2348
1952
  percentComplete: task.percentComplete ?? 0,
2349
1953
  priority: task.priority ?? 5,
@@ -2354,23 +1958,39 @@ export class GraphRepository {
2354
1958
  });
2355
1959
  }
2356
1960
  /**
2357
- * 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.
2358
1986
  */
2359
1987
  async getPlannerTaskAsync(taskId) {
2360
- const cached = this.idCache.plannerTasks.get(taskId);
2361
- if (cached == null)
2362
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2363
- const task = await this.client.getPlannerTask(cached.taskId);
2364
- const etag = task['@odata.etag'] ?? '';
2365
- this.idCache.plannerTasks.set(taskId, { taskId: cached.taskId, etag });
2366
- let bucketNumericId = null;
2367
- if (task.bucketId != null) {
2368
- bucketNumericId = hashStringToNumber(task.bucketId);
2369
- }
1988
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
1989
+ const task = await this.client.getPlannerTask(gTaskId);
2370
1990
  return {
2371
- id: taskId,
1991
+ id: String(taskId),
2372
1992
  title: task.title ?? '',
2373
- bucketId: bucketNumericId,
1993
+ bucketId: task.bucketId != null ? this.mintAlias('plannerBucket', task.bucketId) : null,
2374
1994
  assignees: task.assignments != null ? Object.keys(task.assignments) : [],
2375
1995
  percentComplete: task.percentComplete ?? 0,
2376
1996
  priority: task.priority ?? 5,
@@ -2379,20 +1999,17 @@ export class GraphRepository {
2379
1999
  createdDateTime: task.createdDateTime ?? '',
2380
2000
  conversationThreadId: task.conversationThreadId ?? '',
2381
2001
  orderHint: task.orderHint ?? '',
2382
- etag,
2002
+ etag: this.extractEtag(task),
2383
2003
  };
2384
2004
  }
2385
2005
  /**
2386
2006
  * Creates a new planner task.
2387
2007
  */
2388
2008
  async createPlannerTaskAsync(planId, title, bucketId, assignments, priority, startDate, dueDate) {
2389
- const cachedPlan = await this.resolvePlanId(planId);
2390
- const body = { planId: cachedPlan.planId, title };
2009
+ const graphPlanId = await this.resolvePlanId(planId);
2010
+ const body = { planId: graphPlanId, title };
2391
2011
  if (bucketId != null) {
2392
- const cachedBucket = this.idCache.plannerBuckets.get(bucketId);
2393
- if (cachedBucket == null)
2394
- throw new Error(`Bucket ID ${bucketId} not found in cache. Try listing buckets first.`);
2395
- body.bucketId = cachedBucket.bucketId;
2012
+ body.bucketId = this.toGraphId(bucketId, 'plannerBucket');
2396
2013
  }
2397
2014
  if (assignments != null)
2398
2015
  body.assignments = assignments;
@@ -2403,28 +2020,18 @@ export class GraphRepository {
2403
2020
  if (dueDate != null)
2404
2021
  body.dueDateTime = dueDate;
2405
2022
  const task = await this.client.createPlannerTask(body);
2406
- const graphId = task.id;
2407
- const numericId = hashStringToNumber(graphId);
2408
- const etag = task['@odata.etag'] ?? '';
2409
- this.idCache.plannerTasks.set(numericId, { taskId: graphId, etag });
2410
- return numericId;
2023
+ return this.mintAlias('plannerTask', task.id);
2411
2024
  }
2412
2025
  /**
2413
- * Updates a planner task (requires cached ETag).
2026
+ * Updates a planner task (U5b-5: fetches a fresh etag immediately before the write).
2414
2027
  */
2415
2028
  async updatePlannerTaskAsync(taskId, updates) {
2416
- const cached = this.idCache.plannerTasks.get(taskId);
2417
- if (cached == null)
2418
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2029
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
2419
2030
  const graphUpdates = {};
2420
2031
  if (updates.title != null)
2421
2032
  graphUpdates['title'] = updates.title;
2422
- if (updates.bucketId != null) {
2423
- const cachedBucket = this.idCache.plannerBuckets.get(updates.bucketId);
2424
- if (cachedBucket == null)
2425
- throw new Error(`Bucket ID ${updates.bucketId} not found in cache. Try listing buckets first.`);
2426
- graphUpdates['bucketId'] = cachedBucket.bucketId;
2427
- }
2033
+ if (updates.bucketId != null)
2034
+ graphUpdates['bucketId'] = this.toGraphId(updates.bucketId, 'plannerBucket');
2428
2035
  if (updates.percentComplete != null)
2429
2036
  graphUpdates['percentComplete'] = updates.percentComplete;
2430
2037
  if (updates.priority != null)
@@ -2435,39 +2042,31 @@ export class GraphRepository {
2435
2042
  graphUpdates['dueDateTime'] = updates.dueDate;
2436
2043
  if (updates.assignments != null)
2437
2044
  graphUpdates['assignments'] = updates.assignments;
2438
- const result = await this.client.updatePlannerTask(cached.taskId, graphUpdates, cached.etag);
2439
- const newEtag = result['@odata.etag'] ?? cached.etag;
2440
- 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));
2441
2046
  }
2442
2047
  /**
2443
- * Deletes a planner task (requires cached ETag).
2048
+ * Deletes a planner task (U5b-5: fetches a fresh etag immediately before the write).
2444
2049
  */
2445
2050
  async deletePlannerTaskAsync(taskId) {
2446
- const cached = this.idCache.plannerTasks.get(taskId);
2447
- if (cached == null)
2448
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2449
- await this.client.deletePlannerTask(cached.taskId, cached.etag);
2450
- 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));
2451
2053
  }
2452
2054
  // ===========================================================================
2453
2055
  // Planner Task Details
2454
2056
  // ===========================================================================
2455
2057
  /**
2456
- * 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.
2457
2060
  */
2458
2061
  async getPlannerTaskDetailsAsync(taskId) {
2459
- const cached = this.idCache.plannerTasks.get(taskId);
2460
- if (cached == null)
2461
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2462
- const details = await this.client.getPlannerTaskDetails(cached.taskId);
2463
- const etag = details['@odata.etag'] ?? '';
2464
- this.idCache.plannerTaskDetails.set(taskId, { taskId: cached.taskId, etag });
2062
+ const gTaskId = this.toGraphId(taskId, 'plannerTask');
2063
+ const details = await this.client.getPlannerTaskDetails(gTaskId);
2465
2064
  return {
2466
- id: taskId,
2065
+ id: String(taskId),
2467
2066
  description: details.description ?? '',
2468
2067
  checklist: details.checklist ?? {},
2469
2068
  references: details.references ?? {},
2470
- etag,
2069
+ etag: this.extractEtag(details),
2471
2070
  };
2472
2071
  }
2473
2072
  /**
@@ -2480,17 +2079,15 @@ export class GraphRepository {
2480
2079
  * have `details` set to `undefined`.
2481
2080
  */
2482
2081
  async listPlannerTasksWithDetailsAsync(planId) {
2483
- // Step 1: List all tasks (populates idCache.plannerTasks)
2082
+ // Step 1: List all tasks (mints pt_ tokens, registered in the alias store)
2484
2083
  const tasks = await this.listPlannerTasksAsync(planId);
2485
2084
  if (tasks.length === 0)
2486
2085
  return [];
2487
2086
  // Step 2: Build batch requests for each task's details
2488
2087
  const batchRequests = tasks.map((task) => {
2489
- const cached = this.idCache.plannerTasks.get(task.id);
2490
- // cached is guaranteed to exist since listPlannerTasksAsync just populated it
2491
- const graphTaskId = cached.taskId;
2088
+ const graphTaskId = this.toGraphId(task.id, 'plannerTask');
2492
2089
  return {
2493
- id: String(task.id),
2090
+ id: task.id,
2494
2091
  method: 'GET',
2495
2092
  url: `/planner/tasks/${graphTaskId}/details`,
2496
2093
  };
@@ -2499,15 +2096,10 @@ export class GraphRepository {
2499
2096
  const batchResults = await this.client.batchRequests(batchRequests);
2500
2097
  // Step 4: Merge details into tasks
2501
2098
  return tasks.map((task) => {
2502
- const result = batchResults.get(String(task.id));
2099
+ const result = batchResults.get(task.id);
2503
2100
  if (result != null && result.status >= 200 && result.status < 300) {
2504
2101
  const detailBody = result.body;
2505
2102
  const etag = result.headers?.ETag ?? result.headers?.etag ?? '';
2506
- // Cache the task detail ETag for later updates
2507
- const cachedTask = this.idCache.plannerTasks.get(task.id);
2508
- if (cachedTask != null) {
2509
- this.idCache.plannerTaskDetails.set(task.id, { taskId: cachedTask.taskId, etag });
2510
- }
2511
2103
  return {
2512
2104
  ...task,
2513
2105
  details: {
@@ -2523,15 +2115,12 @@ export class GraphRepository {
2523
2115
  });
2524
2116
  }
2525
2117
  /**
2526
- * 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).
2527
2121
  */
2528
2122
  async updatePlannerTaskDetailsAsync(taskId, updates) {
2529
- const cachedTask = this.idCache.plannerTasks.get(taskId);
2530
- if (cachedTask == null)
2531
- throw new Error(`Task ID ${taskId} not found in cache. Try listing planner tasks first.`);
2532
- const cachedDetails = this.idCache.plannerTaskDetails.get(taskId);
2533
- if (cachedDetails == null)
2534
- 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');
2535
2124
  const graphUpdates = {};
2536
2125
  if (updates.description != null)
2537
2126
  graphUpdates['description'] = updates.description;
@@ -2539,9 +2128,7 @@ export class GraphRepository {
2539
2128
  graphUpdates['checklist'] = updates.checklist;
2540
2129
  if (updates.references != null)
2541
2130
  graphUpdates['references'] = updates.references;
2542
- const result = await this.client.updatePlannerTaskDetails(cachedTask.taskId, graphUpdates, cachedDetails.etag);
2543
- const newEtag = result['@odata.etag'] ?? cachedDetails.etag;
2544
- 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));
2545
2132
  }
2546
2133
  // ===========================================================================
2547
2134
  // Planner Visualization Data
@@ -2566,7 +2153,7 @@ export class GraphRepository {
2566
2153
  tasks: tasks.map(t => ({
2567
2154
  id: t.id,
2568
2155
  title: t.title,
2569
- bucketId: t.bucketId ?? 0,
2156
+ bucketId: t.bucketId ?? '',
2570
2157
  percentComplete: t.percentComplete,
2571
2158
  priority: t.priority,
2572
2159
  startDateTime: t.startDateTime || null,
@@ -2582,10 +2169,8 @@ export class GraphRepository {
2582
2169
  const meetings = await this.client.listOnlineMeetings(limit ?? 20);
2583
2170
  return meetings.map((meeting) => {
2584
2171
  const graphId = meeting.id ?? '';
2585
- const numericId = hashStringToNumber(graphId);
2586
- this.idCache.onlineMeetings.set(numericId, graphId);
2587
2172
  return {
2588
- id: numericId,
2173
+ id: this.mintAlias('onlineMeeting', graphId),
2589
2174
  subject: meeting.subject ?? '',
2590
2175
  startDateTime: meeting.startDateTime ?? '',
2591
2176
  endDateTime: meeting.endDateTime ?? '',
@@ -2595,103 +2180,96 @@ export class GraphRepository {
2595
2180
  }
2596
2181
  async getOnlineMeetingAsync(meetingId) {
2597
2182
  const mapMeeting = (m) => ({
2598
- id: meetingId,
2183
+ id: String(meetingId),
2599
2184
  subject: m.subject ?? '',
2600
2185
  startDateTime: m.startDateTime ?? '',
2601
2186
  endDateTime: m.endDateTime ?? '',
2602
2187
  joinUrl: m.joinWebUrl ?? '',
2603
2188
  participants: m.participants ?? null,
2604
2189
  });
2605
- const graphId = this.idCache.onlineMeetings.get(meetingId);
2606
- if (graphId == null) {
2607
- // Try to refresh cache
2608
- await this.listOnlineMeetingsAsync(100);
2609
- const refreshedId = this.idCache.onlineMeetings.get(meetingId);
2610
- if (refreshedId == null)
2611
- return undefined;
2612
- const meeting = await this.client.getOnlineMeeting(refreshedId);
2613
- 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
+ }
2614
2214
  }
2615
2215
  const meeting = await this.client.getOnlineMeeting(graphId);
2616
2216
  return mapMeeting(meeting);
2617
2217
  }
2618
2218
  async listMeetingRecordingsAsync(meetingId) {
2619
- const graphMeetingId = this.idCache.onlineMeetings.get(meetingId);
2620
- if (graphMeetingId == null)
2621
- throw new Error(`Meeting ID ${meetingId} not found in cache. Try listing online meetings first.`);
2219
+ const graphMeetingId = this.toGraphId(meetingId, 'onlineMeeting');
2622
2220
  const recordings = await this.client.listMeetingRecordings(graphMeetingId);
2623
2221
  return recordings.map((recording) => {
2624
2222
  const graphId = recording.id ?? '';
2625
- const numericId = hashStringToNumber(graphId);
2626
- this.idCache.recordings.set(numericId, { meetingId: graphMeetingId, recordingId: graphId });
2627
2223
  return {
2628
- id: numericId,
2224
+ id: this.mintAliasComposite('recording', { meetingId: graphMeetingId, recordingId: graphId }),
2629
2225
  createdDateTime: recording.createdDateTime ?? '',
2630
2226
  recordingContentUrl: recording.recordingContentUrl ?? '',
2631
2227
  };
2632
2228
  });
2633
2229
  }
2634
2230
  async downloadMeetingRecordingAsync(recordingId, outputPath) {
2635
- const cached = this.idCache.recordings.get(recordingId);
2636
- if (cached == null)
2637
- throw new Error(`Recording ID ${recordingId} not found in cache. Call list_meeting_recordings first.`);
2638
- 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);
2639
2233
  fs.writeFileSync(outputPath, Buffer.from(content));
2640
2234
  return outputPath;
2641
2235
  }
2642
2236
  async listMeetingTranscriptsAsync(meetingId) {
2643
- const graphMeetingId = this.idCache.onlineMeetings.get(meetingId);
2644
- if (graphMeetingId == null)
2645
- throw new Error(`Meeting ID ${meetingId} not found in cache. Try listing online meetings first.`);
2237
+ const graphMeetingId = this.toGraphId(meetingId, 'onlineMeeting');
2646
2238
  const transcripts = await this.client.listMeetingTranscripts(graphMeetingId);
2647
2239
  return transcripts.map((transcript) => {
2648
2240
  const graphId = transcript.id ?? '';
2649
- const numericId = hashStringToNumber(graphId);
2650
- this.idCache.transcripts.set(numericId, { meetingId: graphMeetingId, transcriptId: graphId });
2651
2241
  return {
2652
- id: numericId,
2242
+ id: this.mintAliasComposite('transcript', { meetingId: graphMeetingId, transcriptId: graphId }),
2653
2243
  createdDateTime: transcript.createdDateTime ?? '',
2654
2244
  contentUrl: transcript.contentUrl ?? '',
2655
2245
  };
2656
2246
  });
2657
2247
  }
2658
2248
  async getMeetingTranscriptContentAsync(transcriptId, format) {
2659
- const cached = this.idCache.transcripts.get(transcriptId);
2660
- if (cached == null)
2661
- throw new Error(`Transcript ID ${transcriptId} not found in cache. Call list_meeting_transcripts first.`);
2662
- 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');
2663
2251
  }
2664
2252
  // ===========================================================================
2665
2253
  // Excel Online (Workbook)
2666
2254
  // ===========================================================================
2667
2255
  async listWorksheetsAsync(fileId) {
2668
- const driveItemId = this.idCache.driveItems.get(fileId);
2669
- if (driveItemId == null)
2670
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2256
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2671
2257
  return await this.client.listWorksheets(driveItemId);
2672
2258
  }
2673
2259
  async getWorksheetRangeAsync(fileId, worksheetName, range) {
2674
- const driveItemId = this.idCache.driveItems.get(fileId);
2675
- if (driveItemId == null)
2676
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2260
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2677
2261
  return await this.client.getWorksheetRange(driveItemId, worksheetName, range);
2678
2262
  }
2679
2263
  async getUsedRangeAsync(fileId, worksheetName) {
2680
- const driveItemId = this.idCache.driveItems.get(fileId);
2681
- if (driveItemId == null)
2682
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2264
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2683
2265
  return await this.client.getUsedRange(driveItemId, worksheetName);
2684
2266
  }
2685
2267
  async updateWorksheetRangeAsync(fileId, worksheetName, range, values) {
2686
- const driveItemId = this.idCache.driveItems.get(fileId);
2687
- if (driveItemId == null)
2688
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2268
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2689
2269
  return await this.client.updateWorksheetRange(driveItemId, worksheetName, range, values);
2690
2270
  }
2691
2271
  async getTableDataAsync(fileId, tableName) {
2692
- const driveItemId = this.idCache.driveItems.get(fileId);
2693
- if (driveItemId == null)
2694
- throw new Error(`Drive item ID ${fileId} not found in cache. List OneDrive or SharePoint files first.`);
2272
+ const driveItemId = this.toGraphId(fileId, 'driveItem');
2695
2273
  return await this.client.getTableData(driveItemId, tableName);
2696
2274
  }
2697
2275
  // OneDrive
@@ -2700,19 +2278,15 @@ export class GraphRepository {
2700
2278
  * Lists files/folders in a drive folder (or root).
2701
2279
  */
2702
2280
  async listDriveItemsAsync(folderId) {
2703
- let graphFolderId;
2704
- if (folderId != null) {
2705
- graphFolderId = this.idCache.driveItems.get(folderId);
2706
- if (graphFolderId == null)
2707
- throw new Error(`Drive item ID ${folderId} not found in cache. Try listing drive items first.`);
2708
- }
2281
+ const graphFolderId = folderId != null
2282
+ ? this.toGraphId(folderId, 'driveItem')
2283
+ : undefined;
2709
2284
  const items = await this.client.listDriveItems(graphFolderId);
2285
+ // Rows carry self-encoding dr_ tokens (minted here) — no cache.
2710
2286
  return items.map((item) => {
2711
2287
  const itemId = item.id;
2712
- const numericId = hashStringToNumber(itemId);
2713
- this.idCache.driveItems.set(numericId, itemId);
2714
2288
  return {
2715
- id: numericId,
2289
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2716
2290
  name: item.name ?? '',
2717
2291
  size: item.size ?? 0,
2718
2292
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2728,10 +2302,8 @@ export class GraphRepository {
2728
2302
  const items = await this.client.searchDriveItems(query, limit);
2729
2303
  return items.map((item) => {
2730
2304
  const itemId = item.id;
2731
- const numericId = hashStringToNumber(itemId);
2732
- this.idCache.driveItems.set(numericId, itemId);
2733
2305
  return {
2734
- id: numericId,
2306
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2735
2307
  name: item.name ?? '',
2736
2308
  size: item.size ?? 0,
2737
2309
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2744,15 +2316,13 @@ export class GraphRepository {
2744
2316
  * Gets metadata for a specific drive item.
2745
2317
  */
2746
2318
  async getDriveItemAsync(itemId) {
2747
- const graphId = this.idCache.driveItems.get(itemId);
2748
- if (graphId == null)
2749
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2319
+ const graphId = this.toGraphId(itemId, 'driveItem');
2750
2320
  const item = await this.client.getDriveItem(graphId);
2751
2321
  const fileObj = item.file;
2752
2322
  const createdByObj = item.createdBy;
2753
2323
  const createdByUser = createdByObj?.user;
2754
2324
  return {
2755
- id: itemId,
2325
+ id: mintSelfEncoded('driveItem', graphId),
2756
2326
  name: item.name ?? '',
2757
2327
  size: item.size ?? 0,
2758
2328
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2766,9 +2336,7 @@ export class GraphRepository {
2766
2336
  * Downloads a drive item to a local file.
2767
2337
  */
2768
2338
  async downloadFileAsync(itemId, outputPath) {
2769
- const graphId = this.idCache.driveItems.get(itemId);
2770
- if (graphId == null)
2771
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2339
+ const graphId = this.toGraphId(itemId, 'driveItem');
2772
2340
  const content = await this.client.downloadDriveItem(graphId);
2773
2341
  const buffer = Buffer.from(content);
2774
2342
  fs.writeFileSync(outputPath, buffer);
@@ -2781,9 +2349,7 @@ export class GraphRepository {
2781
2349
  const content = fs.readFileSync(localFilePath);
2782
2350
  const result = await this.client.uploadDriveItem(parentPath, fileName, content);
2783
2351
  const resultId = result.id;
2784
- const numericId = hashStringToNumber(resultId);
2785
- this.idCache.driveItems.set(numericId, resultId);
2786
- return numericId;
2352
+ return mintSelfEncoded('driveItem', resultId);
2787
2353
  }
2788
2354
  /**
2789
2355
  * Lists recently accessed drive items.
@@ -2792,10 +2358,8 @@ export class GraphRepository {
2792
2358
  const items = await this.client.listRecentDriveItems();
2793
2359
  return items.map((item) => {
2794
2360
  const itemId = item.id;
2795
- const numericId = hashStringToNumber(itemId);
2796
- this.idCache.driveItems.set(numericId, itemId);
2797
2361
  return {
2798
- id: numericId,
2362
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2799
2363
  name: item.name ?? '',
2800
2364
  size: item.size ?? 0,
2801
2365
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2811,10 +2375,8 @@ export class GraphRepository {
2811
2375
  const items = await this.client.listSharedWithMe();
2812
2376
  return items.map((item) => {
2813
2377
  const itemId = item.id;
2814
- const numericId = hashStringToNumber(itemId);
2815
- this.idCache.driveItems.set(numericId, itemId);
2816
2378
  return {
2817
- id: numericId,
2379
+ id: itemId.length > 0 ? mintSelfEncoded('driveItem', itemId) : '',
2818
2380
  name: item.name ?? '',
2819
2381
  size: item.size ?? 0,
2820
2382
  lastModified: item.lastModifiedDateTime ?? '',
@@ -2827,9 +2389,7 @@ export class GraphRepository {
2827
2389
  * Creates a sharing link for a drive item.
2828
2390
  */
2829
2391
  async createSharingLinkAsync(itemId, type, scope) {
2830
- const graphId = this.idCache.driveItems.get(itemId);
2831
- if (graphId == null)
2832
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2392
+ const graphId = this.toGraphId(itemId, 'driveItem');
2833
2393
  const result = await this.client.createSharingLink(graphId, type, scope);
2834
2394
  const link = result.link;
2835
2395
  return {
@@ -2842,17 +2402,14 @@ export class GraphRepository {
2842
2402
  * Deletes a drive item.
2843
2403
  */
2844
2404
  async deleteDriveItemAsync(itemId) {
2845
- const graphId = this.idCache.driveItems.get(itemId);
2846
- if (graphId == null)
2847
- throw new Error(`Drive item ID ${itemId} not found in cache. Try listing drive items first.`);
2405
+ const graphId = this.toGraphId(itemId, 'driveItem');
2848
2406
  await this.client.deleteDriveItem(graphId);
2849
- this.idCache.driveItems.delete(itemId);
2850
2407
  }
2851
2408
  // ===========================================================================
2852
2409
  // SharePoint Sites & Document Libraries
2853
2410
  // ===========================================================================
2854
2411
  /**
2855
- * Lists followed SharePoint sites, caching IDs.
2412
+ * Lists followed SharePoint sites, minting durable si_ tokens.
2856
2413
  */
2857
2414
  async listSitesAsync() {
2858
2415
  const sites = await this.client.listFollowedSites();
@@ -2860,10 +2417,8 @@ export class GraphRepository {
2860
2417
  for (const site of sites) {
2861
2418
  const siteId = site.id;
2862
2419
  if (siteId != null) {
2863
- const numericId = hashStringToNumber(siteId);
2864
- this.idCache.sites.set(numericId, siteId);
2865
2420
  result.push({
2866
- id: numericId,
2421
+ id: this.mintAlias('site', siteId),
2867
2422
  name: site.name ?? '',
2868
2423
  webUrl: site.webUrl ?? '',
2869
2424
  displayName: site.displayName ?? '',
@@ -2873,7 +2428,7 @@ export class GraphRepository {
2873
2428
  return result;
2874
2429
  }
2875
2430
  /**
2876
- * Searches SharePoint sites by keyword, caching IDs.
2431
+ * Searches SharePoint sites by keyword, minting durable si_ tokens.
2877
2432
  */
2878
2433
  async searchSitesAsync(query) {
2879
2434
  const sites = await this.client.searchSites(query);
@@ -2881,10 +2436,8 @@ export class GraphRepository {
2881
2436
  for (const site of sites) {
2882
2437
  const siteId = site.id;
2883
2438
  if (siteId != null) {
2884
- const numericId = hashStringToNumber(siteId);
2885
- this.idCache.sites.set(numericId, siteId);
2886
2439
  result.push({
2887
- id: numericId,
2440
+ id: this.mintAlias('site', siteId),
2888
2441
  name: site.name ?? '',
2889
2442
  webUrl: site.webUrl ?? '',
2890
2443
  displayName: site.displayName ?? '',
@@ -2897,12 +2450,10 @@ export class GraphRepository {
2897
2450
  * Gets details for a specific SharePoint site.
2898
2451
  */
2899
2452
  async getSiteAsync(siteId) {
2900
- const graphId = this.idCache.sites.get(siteId);
2901
- if (graphId == null)
2902
- throw new Error(`Site ID ${siteId} not found in cache. Try listing or searching sites first.`);
2453
+ const graphId = this.toGraphId(siteId, 'site');
2903
2454
  const site = await this.client.getSite(graphId);
2904
2455
  return {
2905
- id: siteId,
2456
+ id: String(siteId),
2906
2457
  name: site.name ?? '',
2907
2458
  webUrl: site.webUrl ?? '',
2908
2459
  displayName: site.displayName ?? '',
@@ -2910,21 +2461,17 @@ export class GraphRepository {
2910
2461
  };
2911
2462
  }
2912
2463
  /**
2913
- * Lists document libraries for a SharePoint site, caching IDs.
2464
+ * Lists document libraries for a SharePoint site, minting durable dl_ tokens.
2914
2465
  */
2915
2466
  async listDocumentLibrariesAsync(siteId) {
2916
- const graphSiteId = this.idCache.sites.get(siteId);
2917
- if (graphSiteId == null)
2918
- throw new Error(`Site ID ${siteId} not found in cache. Try listing or searching sites first.`);
2467
+ const graphSiteId = this.toGraphId(siteId, 'site');
2919
2468
  const drives = await this.client.listDocumentLibraries(graphSiteId);
2920
2469
  const result = [];
2921
2470
  for (const drive of drives) {
2922
2471
  const driveId = drive.id;
2923
2472
  if (driveId != null) {
2924
- const numericId = hashStringToNumber(driveId);
2925
- this.idCache.documentLibraries.set(numericId, { siteId: graphSiteId, driveId });
2926
2473
  result.push({
2927
- id: numericId,
2474
+ id: this.mintAliasComposite('documentLibrary', { siteId: graphSiteId, driveId }),
2928
2475
  name: drive.name ?? '',
2929
2476
  webUrl: drive.webUrl ?? '',
2930
2477
  driveType: drive.driveType ?? '',
@@ -2934,28 +2481,21 @@ export class GraphRepository {
2934
2481
  return result;
2935
2482
  }
2936
2483
  /**
2937
- * Lists items in a document library or folder, caching IDs.
2484
+ * Lists items in a document library or folder, minting durable li_ tokens.
2938
2485
  */
2939
2486
  async listLibraryItemsAsync(libraryId, folderId) {
2940
- const libCached = this.idCache.documentLibraries.get(libraryId);
2941
- if (libCached == null)
2942
- throw new Error(`Library ID ${libraryId} not found in cache. Try listing document libraries first.`);
2487
+ const libCached = this.toGraphParts(libraryId, 'documentLibrary', ['siteId', 'driveId']);
2943
2488
  let folderItemId;
2944
2489
  if (folderId != null) {
2945
- const folderCached = this.idCache.libraryDriveItems.get(folderId);
2946
- if (folderCached == null)
2947
- throw new Error(`Folder ID ${folderId} not found in cache. Try listing library items first.`);
2948
- folderItemId = folderCached.itemId;
2490
+ folderItemId = this.toGraphParts(folderId, 'libraryDriveItem', ['driveId', 'itemId']).itemId;
2949
2491
  }
2950
2492
  const items = await this.client.listLibraryItems(libCached.driveId, folderItemId);
2951
2493
  const result = [];
2952
2494
  for (const item of items) {
2953
2495
  const itemGraphId = item.id;
2954
2496
  if (itemGraphId != null) {
2955
- const numericId = hashStringToNumber(itemGraphId);
2956
- this.idCache.libraryDriveItems.set(numericId, { driveId: libCached.driveId, itemId: itemGraphId });
2957
2497
  result.push({
2958
- id: numericId,
2498
+ id: this.mintAliasComposite('libraryDriveItem', { driveId: libCached.driveId, itemId: itemGraphId }),
2959
2499
  name: item.name ?? '',
2960
2500
  size: item.size ?? 0,
2961
2501
  webUrl: item.webUrl ?? '',
@@ -2970,9 +2510,7 @@ export class GraphRepository {
2970
2510
  * Downloads a file from a document library to the specified path.
2971
2511
  */
2972
2512
  async downloadLibraryFileAsync(itemId, outputPath) {
2973
- const cached = this.idCache.libraryDriveItems.get(itemId);
2974
- if (cached == null)
2975
- throw new Error(`Item ID ${itemId} not found in cache. Try listing library items first.`);
2513
+ const cached = this.toGraphParts(itemId, 'libraryDriveItem', ['driveId', 'itemId']);
2976
2514
  const content = await this.client.downloadLibraryFile(cached.driveId, cached.itemId);
2977
2515
  const resolvedPath = path.resolve(outputPath);
2978
2516
  const dir = path.dirname(resolvedPath);
@@ -2981,16 +2519,171 @@ export class GraphRepository {
2981
2519
  return resolvedPath;
2982
2520
  }
2983
2521
  /**
2984
- * 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.
2985
2523
  */
2986
2524
  getFolderGraphId(folderId) {
2987
- return this.idCache.folders.get(folderId);
2525
+ return this.toGraphId(folderId, 'folder');
2988
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';
2989
2682
  }
2990
2683
  /**
2991
2684
  * Creates a Microsoft Graph API repository.
2992
2685
  */
2993
- export function createGraphRepository(deviceCodeCallback) {
2994
- return new GraphRepository(deviceCodeCallback);
2686
+ export function createGraphRepository(deviceCodeCallback, store, accountId) {
2687
+ return new GraphRepository(deviceCodeCallback, store, accountId);
2995
2688
  }
2996
2689
  //# sourceMappingURL=repository.js.map