@rapidmx/mapi-plugin 1.0.0-beta.3 → 1.0.0-beta.5

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 (154) hide show
  1. package/README.md +4 -4
  2. package/dist/lib/BaseMapiEmsmdbRoute.js +229 -37
  3. package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -1
  4. package/dist/lib/MapiSessionManager.js +404 -48
  5. package/dist/lib/MapiSessionManager.js.map +1 -1
  6. package/dist/lib/RegexPatternUtils.js +30 -0
  7. package/dist/lib/RegexPatternUtils.js.map +1 -0
  8. package/dist/lib/RopDispatcher.js +130 -19
  9. package/dist/lib/RopDispatcher.js.map +1 -1
  10. package/dist/lib/codec/AppointmentRecurrence.js +22 -21
  11. package/dist/lib/codec/AppointmentRecurrence.js.map +1 -1
  12. package/dist/lib/codec/BufferCursor.js +41 -12
  13. package/dist/lib/codec/BufferCursor.js.map +1 -1
  14. package/dist/lib/codec/GlobalObjectId.js +26 -20
  15. package/dist/lib/codec/GlobalObjectId.js.map +1 -1
  16. package/dist/lib/codec/MapiTimeZone.js +43 -10
  17. package/dist/lib/codec/MapiTimeZone.js.map +1 -1
  18. package/dist/lib/codec/PropertyValue.js +28 -2
  19. package/dist/lib/codec/PropertyValue.js.map +1 -1
  20. package/dist/lib/codec/RopBuffer.js +15 -3
  21. package/dist/lib/codec/RopBuffer.js.map +1 -1
  22. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +2 -1
  23. package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -1
  24. package/dist/lib/nspi/NspiCodec.js +5 -5
  25. package/dist/lib/nspi/NspiCodec.js.map +1 -1
  26. package/dist/lib/nspi/NspiGetMatchesHandler.js +14 -14
  27. package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -1
  28. package/dist/lib/rop/AddressList.js +109 -0
  29. package/dist/lib/rop/AddressList.js.map +1 -0
  30. package/dist/lib/rop/CalendarEventTarget.js +7 -4
  31. package/dist/lib/rop/CalendarEventTarget.js.map +1 -1
  32. package/dist/lib/rop/ContactTarget.js +2 -6
  33. package/dist/lib/rop/ContactTarget.js.map +1 -1
  34. package/dist/lib/rop/ContentsTable.js +60 -0
  35. package/dist/lib/rop/ContentsTable.js.map +1 -0
  36. package/dist/lib/rop/ExecuteBudget.js +82 -0
  37. package/dist/lib/rop/ExecuteBudget.js.map +1 -0
  38. package/dist/lib/rop/FastTransferStream.js +99 -2
  39. package/dist/lib/rop/FastTransferStream.js.map +1 -1
  40. package/dist/lib/rop/FolderTarget.js +13 -6
  41. package/dist/lib/rop/FolderTarget.js.map +1 -1
  42. package/dist/lib/rop/HandleDataCache.js +412 -0
  43. package/dist/lib/rop/HandleDataCache.js.map +1 -0
  44. package/dist/lib/rop/MeetingMessageClassHandler.js +194 -48
  45. package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -1
  46. package/dist/lib/rop/MessageBodyStream.js +33 -7
  47. package/dist/lib/rop/MessageBodyStream.js.map +1 -1
  48. package/dist/lib/rop/MessageTarget.js +17 -4
  49. package/dist/lib/rop/MessageTarget.js.map +1 -1
  50. package/dist/lib/rop/NamedPropertyRegistry.js +8 -5
  51. package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -1
  52. package/dist/lib/rop/PropertyResolvers.js +3 -1
  53. package/dist/lib/rop/PropertyResolvers.js.map +1 -1
  54. package/dist/lib/rop/RepoPaging.js +54 -0
  55. package/dist/lib/rop/RepoPaging.js.map +1 -0
  56. package/dist/lib/rop/RestapiRules.js +52 -0
  57. package/dist/lib/rop/RestapiRules.js.map +1 -0
  58. package/dist/lib/rop/RopCreateMessageHandler.js +4 -2
  59. package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -1
  60. package/dist/lib/rop/RopDeleteFolderHandler.js +118 -77
  61. package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -1
  62. package/dist/lib/rop/RopDeleteMessagesHandler.js +26 -1
  63. package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -1
  64. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +14 -14
  65. package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -1
  66. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +13 -14
  67. package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -1
  68. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +33 -32
  69. package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -1
  70. package/dist/lib/rop/RopGetContentsTableHandler.js +17 -38
  71. package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -1
  72. package/dist/lib/rop/RopGetHierarchyTableHandler.js +4 -2
  73. package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -1
  74. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +6 -8
  75. package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -1
  76. package/dist/lib/rop/RopHandler.js +5 -1
  77. package/dist/lib/rop/RopHandler.js.map +1 -1
  78. package/dist/lib/rop/RopLogonHandler.js +5 -2
  79. package/dist/lib/rop/RopLogonHandler.js.map +1 -1
  80. package/dist/lib/rop/RopOpenFolderHandler.js +3 -1
  81. package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -1
  82. package/dist/lib/rop/RopOpenMessageHandler.js +3 -1
  83. package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -1
  84. package/dist/lib/rop/RopOpenStreamHandler.js +11 -8
  85. package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -1
  86. package/dist/lib/rop/RopQueryRowsHandler.js +31 -5
  87. package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -1
  88. package/dist/lib/rop/RopReadStreamHandler.js +22 -4
  89. package/dist/lib/rop/RopReadStreamHandler.js.map +1 -1
  90. package/dist/lib/rop/RopReleaseHandler.js +4 -1
  91. package/dist/lib/rop/RopReleaseHandler.js.map +1 -1
  92. package/dist/lib/rop/RopSaveChangesMessageHandler.js +66 -7
  93. package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -1
  94. package/dist/lib/rop/RopSetColumnsHandler.js +6 -8
  95. package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -1
  96. package/dist/lib/rop/RopSetPropertiesHandler.js +23 -12
  97. package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -1
  98. package/dist/lib/rop/RopSubmitMessageHandler.js +148 -120
  99. package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -1
  100. package/dist/lib/rop/RopWriteStreamHandler.js +37 -10
  101. package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -1
  102. package/dist/lib/rop/TaskTarget.js +2 -6
  103. package/dist/lib/rop/TaskTarget.js.map +1 -1
  104. package/dist/lib/rop/TransportSend.js +16 -0
  105. package/dist/lib/rop/TransportSend.js.map +1 -0
  106. package/dist/lib/sql/MapiEmsmdbRouteSQL.js +2 -1
  107. package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -1
  108. package/dist/types/BaseMapiEmsmdbRoute.d.ts +59 -2
  109. package/dist/types/MapiSessionManager.d.ts +218 -63
  110. package/dist/types/RegexPatternUtils.d.ts +14 -0
  111. package/dist/types/RopDispatcher.d.ts +47 -11
  112. package/dist/types/codec/BufferCursor.d.ts +14 -0
  113. package/dist/types/codec/GlobalObjectId.d.ts +9 -5
  114. package/dist/types/codec/PropertyValue.d.ts +10 -0
  115. package/dist/types/codec/RopBuffer.d.ts +7 -0
  116. package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +1 -0
  117. package/dist/types/nspi/NspiGetMatchesHandler.d.ts +2 -0
  118. package/dist/types/rop/AddressList.d.ts +48 -0
  119. package/dist/types/rop/CalendarEventTarget.d.ts +7 -3
  120. package/dist/types/rop/ContactTarget.d.ts +3 -3
  121. package/dist/types/rop/ContentsTable.d.ts +16 -0
  122. package/dist/types/rop/ExecuteBudget.d.ts +48 -0
  123. package/dist/types/rop/FastTransferStream.d.ts +41 -2
  124. package/dist/types/rop/FolderTarget.d.ts +3 -2
  125. package/dist/types/rop/HandleDataCache.d.ts +145 -0
  126. package/dist/types/rop/MeetingMessageClassHandler.d.ts +23 -29
  127. package/dist/types/rop/MessageBodyStream.d.ts +16 -7
  128. package/dist/types/rop/MessageTarget.d.ts +10 -3
  129. package/dist/types/rop/NamedPropertyRegistry.d.ts +6 -2
  130. package/dist/types/rop/PropertyResolvers.d.ts +1 -1
  131. package/dist/types/rop/RepoPaging.d.ts +30 -0
  132. package/dist/types/rop/RestapiRules.d.ts +32 -0
  133. package/dist/types/rop/RopCreateMessageHandler.d.ts +1 -0
  134. package/dist/types/rop/RopDeleteFolderHandler.d.ts +36 -21
  135. package/dist/types/rop/RopDeleteMessagesHandler.d.ts +7 -0
  136. package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +1 -0
  137. package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +1 -0
  138. package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +10 -11
  139. package/dist/types/rop/RopGetContentsTableHandler.d.ts +7 -13
  140. package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +1 -0
  141. package/dist/types/rop/RopHandler.d.ts +40 -1
  142. package/dist/types/rop/RopOpenFolderHandler.d.ts +1 -0
  143. package/dist/types/rop/RopOpenMessageHandler.d.ts +1 -0
  144. package/dist/types/rop/RopOpenStreamHandler.d.ts +1 -0
  145. package/dist/types/rop/RopQueryRowsHandler.d.ts +3 -0
  146. package/dist/types/rop/RopReadStreamHandler.d.ts +3 -1
  147. package/dist/types/rop/RopReleaseHandler.d.ts +1 -0
  148. package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +16 -1
  149. package/dist/types/rop/RopSubmitMessageHandler.d.ts +33 -23
  150. package/dist/types/rop/RopWriteStreamHandler.d.ts +18 -8
  151. package/dist/types/rop/TaskTarget.d.ts +3 -3
  152. package/dist/types/rop/TransportSend.d.ts +10 -0
  153. package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +1 -0
  154. package/package.json +3 -3
@@ -8,7 +8,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
9
9
  // SPDX-License-Identifier: MPL-2.0
10
10
  ///////////////////////////////////////////////////////////////////////////////
11
- import { CalendarEventSQL, ContactSQL, FolderSQL, LabelSQL, MailboxSQL, MessageSQL, TaskSQL } from "@rapidmx/restapi/sql";
11
+ import { AuditLogEntrySQL, CalendarEventSQL, ContactSQL, FolderSQL, LabelSQL, MailboxSQL, MessageSQL, TaskSQL } from "@rapidmx/restapi/sql";
12
12
  import { BaseMapiEmsmdbRoute } from "../BaseMapiEmsmdbRoute.js";
13
13
  import { RopLogonHandler } from "../rop/RopLogonHandler.js";
14
14
  import { RopReleaseHandler } from "../rop/RopReleaseHandler.js";
@@ -51,6 +51,7 @@ let MapiEmsmdbRouteSQL = class MapiEmsmdbRouteSQL extends BaseMapiEmsmdbRoute {
51
51
  this.contactClass = ContactSQL;
52
52
  this.taskClass = TaskSQL;
53
53
  this.labelClass = LabelSQL;
54
+ this.auditLogClass = AuditLogEntrySQL;
54
55
  this.ropHandlerClasses = [
55
56
  RopLogonHandler,
56
57
  RopReleaseHandler,
@@ -1 +1 @@
1
- {"version":3,"file":"MapiEmsmdbRouteSQL.js","sourceRoot":"","sources":["../../../src/sql/MapiEmsmdbRouteSQL.ts"],"names":[],"mappings":";;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC1H,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AACpF,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,+BAA+B,EAAE,MAAM,2CAA2C,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,iCAAiC,EAAE,MAAM,6CAA6C,CAAC;AAChG,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,kCAAkC,EAAE,MAAM,8CAA8C,CAAC;AAClG,OAAO,EAAE,0CAA0C,EAAE,MAAM,sDAAsD,CAAC;AAClH,OAAO,EAAE,qCAAqC,EAAE,MAAM,iDAAiD,CAAC;AACxG,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC;AAElC;;;;;;GAMG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,mBAA+B;IAAhE;;QACO,iBAAY,GAAQ,UAAU,CAAC;QAC/B,gBAAW,GAAQ,SAAS,CAAC;QAC7B,iBAAY,GAAQ,UAAU,CAAC;QAC/B,uBAAkB,GAAQ,gBAAgB,CAAC;QAC3C,iBAAY,GAAQ,UAAU,CAAC;QAC/B,cAAS,GAAQ,OAAO,CAAC;QACzB,eAAU,GAAQ,QAAQ,CAAC;QAC3B,sBAAiB,GAAU;YACjC,eAAe;YACf,iBAAiB;YACjB,oBAAoB;YACpB,2BAA2B;YAC3B,0BAA0B;YAC1B,oBAAoB;YACpB,mBAAmB;YACnB,qBAAqB;YACrB,+BAA+B;YAC/B,oBAAoB;YACpB,oBAAoB;YACpB,uBAAuB;YACvB,uBAAuB;YACvB,qBAAqB;YACrB,4BAA4B;YAC5B,uBAAuB;YACvB,iCAAiC;YACjC,wBAAwB;YACxB,sBAAsB;YACtB,kCAAkC;YAClC,0CAA0C;YAC1C,qCAAqC;SACxC,CAAC;IACN,CAAC;CAAA,CAAA;AAhCY,kBAAkB;IAD9B,KAAK,CAAC,cAAc,CAAC;GACT,kBAAkB,CAgC9B"}
1
+ {"version":3,"file":"MapiEmsmdbRouteSQL.js","sourceRoot":"","sources":["../../../src/sql/MapiEmsmdbRouteSQL.ts"],"names":[],"mappings":";;;;;;AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAC5I,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,2BAA2B,EAAE,MAAM,uCAAuC,CAAC;AACpF,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAC;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,+BAA+B,EAAE,MAAM,2CAA2C,CAAC;AAC5F,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,4BAA4B,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,MAAM,mCAAmC,CAAC;AAC5E,OAAO,EAAE,iCAAiC,EAAE,MAAM,6CAA6C,CAAC;AAChG,OAAO,EAAE,wBAAwB,EAAE,MAAM,oCAAoC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,kCAAkC,EAAE,MAAM,8CAA8C,CAAC;AAClG,OAAO,EAAE,0CAA0C,EAAE,MAAM,sDAAsD,CAAC;AAClH,OAAO,EAAE,qCAAqC,EAAE,MAAM,iDAAiD,CAAC;AACxG,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,CAAC;AAElC;;;;;;GAMG;AAEI,IAAM,kBAAkB,GAAxB,MAAM,kBAAmB,SAAQ,mBAA+B;IAAhE;;QACO,iBAAY,GAAQ,UAAU,CAAC;QAC/B,gBAAW,GAAQ,SAAS,CAAC;QAC7B,iBAAY,GAAQ,UAAU,CAAC;QAC/B,uBAAkB,GAAQ,gBAAgB,CAAC;QAC3C,iBAAY,GAAQ,UAAU,CAAC;QAC/B,cAAS,GAAQ,OAAO,CAAC;QACzB,eAAU,GAAQ,QAAQ,CAAC;QAC3B,kBAAa,GAAQ,gBAAgB,CAAC;QACtC,sBAAiB,GAAU;YACjC,eAAe;YACf,iBAAiB;YACjB,oBAAoB;YACpB,2BAA2B;YAC3B,0BAA0B;YAC1B,oBAAoB;YACpB,mBAAmB;YACnB,qBAAqB;YACrB,+BAA+B;YAC/B,oBAAoB;YACpB,oBAAoB;YACpB,uBAAuB;YACvB,uBAAuB;YACvB,qBAAqB;YACrB,4BAA4B;YAC5B,uBAAuB;YACvB,iCAAiC;YACjC,wBAAwB;YACxB,sBAAsB;YACtB,kCAAkC;YAClC,0CAA0C;YAC1C,qCAAqC;SACxC,CAAC;IACN,CAAC;CAAA,CAAA;AAjCY,kBAAkB;IAD9B,KAAK,CAAC,cAAc,CAAC;GACT,kBAAkB,CAiC9B"}
@@ -1,6 +1,22 @@
1
1
  import { type JWTUser } from "@rapidrest/core";
2
2
  import { HttpRequest, HttpResponse } from "@rapidrest/service-core";
3
+ import { type RopBuffer } from "./codec/RopBuffer.js";
3
4
  import { Mailbox } from "@rapidmx/restapi";
5
+ /** The largest `RopBufferSize` accepted - the `[MS-OXCRPC]` ROP input buffer limit. */
6
+ export declare const MAX_ROP_BUFFER_SIZE = 32767;
7
+ /** A decoded `Execute` request: the ROP buffer framing plus the client's `MaxRopOut`. */
8
+ export interface ExecuteRequest extends RopBuffer {
9
+ /** The most bytes the client accepts in the response's `RopBuffer`. */
10
+ maxRopOut: number;
11
+ }
12
+ /**
13
+ * Decodes an `Execute` request body's `Flags`/`RopBufferSize`/`RopBuffer`/`MaxRopOut` and the ROP buffer framing
14
+ * inside it. Throws a 400 `ApiError` for a malformed body - a `RopBufferSize` over `MAX_ROP_BUFFER_SIZE` or past the
15
+ * end of the body, or a ROP buffer `decodeRopBuffer` rejects - instead of letting a bare `RangeError` surface as a
16
+ * 500. A body that ends before `MaxRopOut` gets the largest response a `RopBuffer` can carry.
17
+ * `AuxiliaryBufferSize`/`AuxiliaryBuffer` are left unread - no auxiliary-payload support in this pragmatic subset.
18
+ */
19
+ export declare function decodeExecuteRequest(rawBody: Buffer): ExecuteRequest;
4
20
  /**
5
21
  * Abstract base for the single fixed EMSMDB endpoint (`POST /mapi/emsmdb` by `[MS-OXCMAPIHTTP]` convention,
6
22
  * though the concrete path is left to the consuming application to mount via `@Route(...)` - see
@@ -43,6 +59,8 @@ export declare abstract class BaseMapiEmsmdbRoute<M extends Mailbox> {
43
59
  protected abstract contactClass: any;
44
60
  protected abstract taskClass: any;
45
61
  protected abstract labelClass: any;
62
+ /** The backend's `AuditLogEntry` class. When set, messages deleted through MAPI are audited like REST deletes. */
63
+ protected auditLogClass?: any;
46
64
  /** ROP handler classes to instantiate (one each) in `@Init`, keyed by their own `ropId`. Empty until a
47
65
  * concrete `RopHandler` lands - every ROP is then simply left unprocessed (see `RopDispatcher`'s own doc
48
66
  * comment), the correct, honest behavior for a transport skeleton with no ROPs implemented yet. */
@@ -60,7 +78,11 @@ export declare abstract class BaseMapiEmsmdbRoute<M extends Mailbox> {
60
78
  private blobStore?;
61
79
  private scanPipeline?;
62
80
  private mailTransport?;
81
+ /** Publishes the live `Folder` counts-update event `refreshFolderCounts()` sends after a ROP handler changes what
82
+ * a folder holds through `context.messageRepo` directly - see `folderCountsContext()`/`RopContext.notifyFolderCounts`. */
83
+ private notificationUtils?;
63
84
  private logger;
85
+ private config?;
64
86
  init(): Promise<void>;
65
87
  dispatch(req: HttpRequest, res: HttpResponse, user?: JWTUser): Promise<void>;
66
88
  /**
@@ -71,12 +93,47 @@ export declare abstract class BaseMapiEmsmdbRoute<M extends Mailbox> {
71
93
  */
72
94
  private handleConnect;
73
95
  /**
74
- * `Execute` decodes the outer envelope (`Flags`/`RopBufferSize`/`RopBuffer`/...) and the inner ROP buffer
75
- * framing (`RopBuffer.ts`), dispatches every contained ROP via `RopDispatcher`, and re-encodes the
96
+ * `Execute` decodes the outer envelope (`Flags`/`RopBufferSize`/`RopBuffer`/`MaxRopOut`/...) and the inner ROP
97
+ * buffer framing (`RopBuffer.ts`), dispatches every contained ROP via `RopDispatcher`, and re-encodes the
76
98
  * collected responses - preserving the incoming `handleTable` unchanged (this pragmatic subset never
77
99
  * allocates/frees table-wide handle slots at the framing level; individual `RopHandler`s manage their own
78
100
  * entries within `session.handles` instead).
101
+ *
102
+ * - **One request at a time per session.** Only the caller's own session (whose mailbox is still the caller's) is
103
+ * locked. The lock is taken before any ROP runs and renewed every `SESSION_LOCK_RENEW_MS` while the request runs,
104
+ * so a slow request keeps it; an overlapping request gets `X-ResponseCode` 15 (Invalid Sequence) without any side
105
+ * effects. The save stays compare-and-set. Each `Execute` also refreshes the session's place in the per-user cap.
106
+ * - **Retries.** While a request with an `X-RequestId` runs, an in-progress marker for that id is kept (renewed with
107
+ * the lock); a retry of it is answered with code 15 instead of running again. Its response then replaces the
108
+ * marker, and a request repeating the last id (a client retrying after a lost response) gets that response again
109
+ * instead of running its ROPs twice. A request that ends without a stored response drops its marker.
110
+ * `MapiSequence` isn't validated - the lock and the replay cache cover what it guards against here.
111
+ * - **Output size.** ROP responses are held to the client's `MaxRopOut`, less the `RopSize` field and handle
112
+ * table, and to what a 16-bit `RopSize` can describe (see `dispatchRops`). Room for a `RopBufferTooSmall` covering
113
+ * the rest of the request is kept free as ROPs run, so a response that doesn't fit is always answered that way (a
114
+ * normal, stored response). Only a `MaxRopOut` too small for a `RopBufferTooSmall` of the whole request answers
115
+ * `ecBufferTooSmall` in the body `ErrorCode`, before any ROP runs, so nothing is stored and a retry changes nothing.
116
+ * - **Released handles.** Once the session is saved, the FastTransfer streams and write-stream chunks of handles
117
+ * the request released are deleted from the shared store; `Disconnect` and session eviction delete a session's
118
+ * whole set.
119
+ * - **Malformed ROPs.** A ROP that can't be decoded answers 400; the ROPs before it already ran, so the session is
120
+ * still saved first.
79
121
  */
80
122
  private handleExecute;
123
+ /** Runs an `Execute` under its session's lock. `running.requestId` is set while this request's in-progress marker is
124
+ * stored, and cleared once its response is stored instead. */
125
+ private executeLocked;
126
+ /** What `refreshFolderCounts()` (`@rapidmx/restapi`) needs to recompute, re-cache and publish a folder's counts -
127
+ * see `RopContext.notifyFolderCounts`'s own doc comment for why a ROP handler must call it after adding, deleting
128
+ * or moving a `Message` row through `context.messageRepo` itself. */
129
+ private folderCountsContext;
130
+ /** Writes an `Execute` failure body (no ROP buffer) with the given `X-ResponseCode` and `ErrorCode`. */
131
+ private sendExecuteFailure;
132
+ /**
133
+ * Loads the session named by the `MapiContext` cookie, but only if the authenticated caller is the user who
134
+ * `Connect`ed it. The cookie value alone is a bearer token for someone else's mailbox otherwise, so a session
135
+ * belonging to another user is treated exactly like one that doesn't exist.
136
+ */
137
+ private loadOwnSession;
81
138
  private handleDisconnect;
82
139
  }
@@ -1,37 +1,60 @@
1
1
  import { SimpleEntity } from "@rapidrest/service-core";
2
+ import { type HandleDataStore } from "./rop/HandleDataCache.js";
3
+ /** A pragmatic idle-session lifetime. The spec leaves session lifetime to server policy, not a fixed wire
4
+ * value - a real client re-establishes (`Connect`) transparently whenever its session context has expired. */
5
+ export declare const SESSION_TTL_SECONDS: number;
6
+ /** The longest a session may live, however active it stays. A client simply `Connect`s again afterwards. */
7
+ export declare const MAX_SESSION_LIFETIME_MS: number;
8
+ /** The most sessions one user may hold at once. `Connect` past this ends that user's least recently used session (each
9
+ * `Execute` refreshes its session's place - see `MapiSessionManager.touch`). */
10
+ export declare const MAX_SESSIONS_PER_USER = 20;
11
+ /** The largest a serialized session may grow. Everything that grows with use is capped on its own (handles by their
12
+ * one-byte index, named properties, MIDs; write streams live outside the session); this is the backstop, and a save
13
+ * past it fails with `MAPI_E_TOO_BIG` rather than storing megabytes every later request has to load. */
14
+ export declare const MAX_SESSION_BYTES: number;
15
+ /** How long an `Execute`'s session lock lives without being renewed. A running `Execute` renews it every
16
+ * `SESSION_LOCK_RENEW_MS`, so only a crashed pod's lock expires. The session save is still compare-and-set, so an
17
+ * expired lock can't corrupt state. */
18
+ export declare const SESSION_LOCK_TTL_MS: number;
19
+ /** How often a running `Execute` renews its session lock and in-progress marker. */
20
+ export declare const SESSION_LOCK_RENEW_MS: number;
2
21
  /** Tags what a ROP-assigned integer handle (the `ServerObjectHandleTable` index space) refers to.
3
22
  * `entityUid` for a `"folder"` handle is one of `session.folderIds`' own value strings (`"virtual:<name>"` or
4
- * `"folder:<uid>"`), not a bare UID - the same format throughout avoids a second parallel encoding. A
5
- * `"table"` handle's `rows`/`columns`/`cursor` hold `RopGetHierarchyTable`/`RopSetColumns`/`RopQueryRows`
6
- * state for that specific table instance: `rows` is the resolved, order-fixed list of entity targets (same
7
- * `"virtual:<name>"`/`"folder:<uid>"` format) this table enumerates, `columns` the `RopSetColumns`-configured
8
- * property list, `cursor` how many rows `RopQueryRows` has already returned. A `"stream"` handle's `entityUid`
9
- * is the `"message:<uid>"` target its content was opened from, `propertyId`/`propertyType` the `PropertyTag`
10
- * `RopOpenStream` opened (this pragmatic subset only ever supports `PidTagBody`/`PtypString`, see
11
- * `MessageBodyStream.ts`), and `streamPosition` how many bytes `RopReadStream` has already returned (a
12
- * read-mode stream) or `writeTargetHandleIndex`/`writeBufferBase64` the accumulated write state (a write-mode
13
- * stream opened `ReadWrite`/`Create` against a `RopCreateMessage` draft's `PidTagBody` - see
14
- * `RopOpenStreamHandler`'s own doc comment).
23
+ * `"folder:<uid>"`), not a bare UID - the same format throughout avoids a second parallel encoding.
24
+ *
25
+ * A `"table"` handle's `columns`/`cursor` hold `RopSetColumns`/`RopQueryRows` state. A hierarchy table carries its
26
+ * resolved child-folder targets in `rows`. A contents table carries no rows at all: `contentsKind` says which repo
27
+ * its folder's items live in and `RopQueryRows` reads just the requested window from the database (see
28
+ * `ContentsTable.ts`), so a large folder never lands in session state.
29
+ *
30
+ * A `"stream"` handle's `entityUid` is the `"message:<uid>"` target its content was opened from,
31
+ * `propertyId`/`propertyType` the `PropertyTag` `RopOpenStream` opened (this pragmatic subset only ever supports
32
+ * `PidTagBody`/`PtypString`, see `MessageBodyStream.ts`), and `streamPosition` how many bytes `RopReadStream` has
33
+ * already returned. A read stream's decoded body lives in `HandleDataCache`, not here. A write stream (opened
34
+ * `ReadWrite`/`Create` against a `RopCreateMessage` draft's `PidTagBody`) records the draft it belongs to in
35
+ * `writeTargetHandleIndex`/`writeTargetGeneration`, and how many bytes have been written in `writeSize`. The bytes
36
+ * themselves are chunks in the `HandleDataStore` (see `RopWriteStreamHandler`), not session state.
15
37
  *
16
38
  * A `"message"` handle from `RopCreateMessage` (a draft not yet `RopSaveChangesMessage`d) has `entityUid: ""`
17
39
  * and instead carries `draftFolderUid` (the folder it will belong to) and `draftProperties` (the small,
18
40
  * well-known set of properties this pragmatic subset's `RopSetProperties` tracks - Subject/DisplayTo/
19
41
  * DisplayCc/DisplayBcc/an inline `PidTagBody`, each coerced to a plain string, keyed by decimal `PropertyId` -
20
- * a string key because a JSON-object key is always a string regardless of how it's written). `writeBufferBase64`
21
- * stores accumulated `RopWriteStream` bytes as base64 rather than a raw `Buffer` for the same reason `Date`
22
- * fields elsewhere in this class are stored as ISO strings: `RedisCache`'s Redis-backed path round-trips
23
- * everything through `JSON.stringify`/`JSON.parse`, which cannot represent a `Buffer` (or a `bigint`, which is
24
- * why no property value is ever stored in its native decoded MAPI type here) losslessly.
42
+ * a string key because a JSON-object key is always a string regardless of how it's written). Buffers are stored
43
+ * as base64 and dates as ISO strings since sessions round-trip through `JSON.stringify`/`JSON.parse`.
25
44
  *
26
- * A `"fastTransfer"` handle (`RopFastTransferSourceCopyTo`/`CopyProperties`'s output handle) carries the
27
- * entire pre-built FastTransfer stream eagerly (`transferBufferBase64`, same base64-for-`RedisCache` reasoning
28
- * as `writeBufferBase64`) - this pragmatic subset builds the whole stream up front rather than truly
29
- * incrementally, since a real client only ever pages it out via `RopFastTransferSourceGetBuffer`'s own
30
- * `transferPosition` cursor into that already-complete buffer, see `FastTransferStream.ts`'s own doc comment. */
45
+ * A `"fastTransfer"` handle (`RopFastTransferSourceCopyTo`/`CopyProperties`'s output handle) keeps only what is
46
+ * needed to rebuild its stream (`transferSourceType`, `transferColumns`/`transferExcludeIds`) plus the paging
47
+ * cursor `transferPosition`; the built stream itself lives in `HandleDataCache`.
48
+ *
49
+ * `generation` is a random nonce per assignment (see `assignHandle`), so data keyed by handle index can tell a handle
50
+ * apart from a later one that reuses the same index - including one assigned by a request whose session changes were
51
+ * never saved, which a per-session counter would hand out again. */
31
52
  export interface MapiObjectHandle {
32
53
  type: "logon" | "folder" | "message" | "table" | "stream" | "fastTransfer";
33
54
  entityUid: string;
55
+ generation?: string;
34
56
  rows?: string[];
57
+ contentsKind?: "message" | "calendarEvent" | "contact" | "task";
35
58
  columns?: {
36
59
  propertyId: number;
37
60
  propertyType: number;
@@ -43,25 +66,32 @@ export interface MapiObjectHandle {
43
66
  draftFolderUid?: string;
44
67
  draftProperties?: Record<string, string>;
45
68
  writeTargetHandleIndex?: number;
46
- writeBufferBase64?: string;
47
- transferBufferBase64?: string;
69
+ writeTargetGeneration?: string;
70
+ writeSize?: number;
71
+ transferSourceType?: "folder" | "message";
72
+ transferColumns?: {
73
+ propertyId: number;
74
+ propertyType: number;
75
+ }[];
76
+ transferExcludeIds?: number[];
48
77
  transferPosition?: number;
78
+ /** Set on a draft message once `RopSubmitMessage` has tried to send it, whatever the outcome. A submitted draft
79
+ * can't be submitted again or changed. */
80
+ submitted?: boolean;
49
81
  }
50
82
  /**
51
83
  * The MAPI/HTTP `Session Context` (`[MS-OXCMAPIHTTP]` §3.1.1.1): everything a `Connect`-established session
52
84
  * needs across subsequent `Execute` requests. Never persisted to a real database - purely an ephemeral,
53
- * TTL-bound cache entry (see `MapiSessionManager` below), so this deliberately extends `SimpleEntity` (just a
54
- * `uid`) rather than this library's own `BaseEntity`, which would add ACL/soft-delete/optimistic-locking
55
- * machinery this object has no use for.
85
+ * TTL-bound store entry (see `MapiSessionManager` below), so this deliberately extends `SimpleEntity` (just a
86
+ * `uid`) rather than this library's own `BaseEntity`.
56
87
  *
57
- * `createdAt` is a plain ISO-8601 string, not a `Date` - `RedisCache`'s Redis-backed path round-trips values
58
- * through `JSON.stringify`/`JSON.parse`, which silently turns a `Date` into a string on the way out without
59
- * reviving it back to a `Date` on the way in; storing it as a string from the start keeps the shape identical
60
- * whether a session happens to be served from the in-memory or the Redis-backed path.
88
+ * `createdAt` is a plain ISO-8601 string, not a `Date`, since sessions round-trip through JSON. `version` is
89
+ * bumped on every successful save and compared on the next one - see `MapiSessionManager.save()`.
61
90
  */
62
91
  export declare class MapiSessionContext extends SimpleEntity {
63
92
  mailboxUid: string;
64
93
  userUid: string;
94
+ version: number;
65
95
  handles: Record<number, MapiObjectHandle>;
66
96
  nextHandleIndex: number;
67
97
  createdAt: string;
@@ -71,60 +101,185 @@ export declare class MapiSessionContext extends SimpleEntity {
71
101
  folderIds: Record<string, string>;
72
102
  /** The reverse of `folderIds` (target -> FID) plus a monotonic counter, maintained alongside it by
73
103
  * `FolderTarget.assignOrGetFid`/`RopLogonHandler` so "does this target already have a FID" is an O(1)
74
- * lookup instead of a linear scan of `folderIds` repeated once per table row - see `assignOrGetFid`'s own
75
- * doc comment for why the scan this replaces was a genuine (not just theoretical) quadratic cost across a
76
- * session's lifetime. Small and purely additive to session state (one more string-keyed map of numbers,
77
- * the same order of magnitude as `folderIds` itself), unlike caching whole resolved rows would be. */
104
+ * lookup instead of a linear scan of `folderIds` repeated once per table row. */
78
105
  folderTargetIds: Record<string, number>;
79
106
  nextFolderId: number;
80
107
  /** This session's MID assignments, keyed by MID (decimal string), valued `"message:<uid>"` - the message
81
- * analog of `folderIds` above. Unlike `folderIds` (pre-populated by `RopLogon`), a MID only ever comes into
82
- * existence lazily, the first time a `RopQueryRows` row exposes a message's `PidTagMid` column (see
83
- * `MessageTarget.assignOrGetMid`), read back by a later `RopOpenMessage`. */
108
+ * analog of `folderIds` above. A MID only ever comes into existence lazily, the first time a `RopQueryRows`
109
+ * row exposes a message's `PidTagMid` column (see `MessageTarget.assignOrGetMid`), read back by a later
110
+ * `RopOpenMessage`. */
84
111
  messageIds: Record<string, string>;
85
- /** The reverse of `messageIds` (target -> MID) plus a monotonic counter - the message analog of
86
- * `folderTargetIds`/`nextFolderId` above, for the identical O(1)-lookup reason. */
112
+ /** The reverse of `messageIds` (target -> MID) plus a monotonic counter. */
87
113
  messageTargetIds: Record<string, number>;
88
114
  nextMessageId: number;
115
+ /** The oldest MID still mapped. Past `MAX_MESSAGE_IDS` mapped MIDs the oldest are dropped - see
116
+ * `MessageTarget.assignOrGetMid`. */
117
+ firstMessageId: number;
89
118
  /** This session's `RopGetPropertyIdsFromNames` mapping table (`[MS-OXCPRPT]` §2.2.12), keyed by a JSON
90
119
  * string encoding of the `{guid, kind, lid|name}` `PropertyName` the numeric ID was assigned to - see
91
- * `NamedPropertyRegistry.ts`'s own doc comment for why a JSON string (rather than a delimiter-joined one)
92
- * is the safest key shape here. A real client resolves every named property (almost every Calendar-specific
93
- * one - `PidLidAppointmentStartWhole`, `PidLidBusyStatus`, `PidLidAppointmentRecur`, ...) through this table
94
- * once per session before ever setting/reading it via `RopSetProperties`/`RopGetPropertiesSpecific`. */
120
+ * `NamedPropertyRegistry.ts`. */
95
121
  namedProperties: Record<string, number>;
96
- /** The reverse of `namedProperties` (numeric ID -> `PropertyName` JSON key) plus a monotonic counter -
97
- * `NamedPropertyRegistry.ts`'s own doc comment covers why both the O(n) forward scan this replaces and the
98
- * `Math.max(...spread)` id-allocation it used are real problems, not just theoretical ones, at real
99
- * mailbox/session scale. */
122
+ /** The reverse of `namedProperties` (numeric ID -> `PropertyName` JSON key) plus a monotonic counter. */
100
123
  namedPropertyIds: Record<number, string>;
101
124
  nextNamedPropertyId: number;
102
- /** `mailboxUid`/`userUid` are always known at construction time (the only call site is
103
- * `MapiSessionManager.create()`, which resolves both up front) - required here rather than optional with
104
- * a same-value fallback, which would just be dead code no real caller ever takes the other branch of. */
105
125
  constructor(other: Partial<SimpleEntity> & {
106
126
  mailboxUid: string;
107
127
  userUid: string;
108
128
  });
109
129
  }
110
130
  /**
111
- * Stores/loads `MapiSessionContext`s, keyed by the opaque session id that also becomes the `MapiContext`
112
- * cookie value. Reuses `@rapidrest/service-core`'s existing generic `RedisCache<T>` (Redis-backed when a
113
- * `cache` datastore is configured, transparent in-memory fallback otherwise) rather than either a bespoke
114
- * Redis client (unlike `PingCommand`'s own throwaway pub/sub client - fine for ephemeral fan-out, not a good
115
- * fit for stateful session data) or the framework's generic cookie/session middleware (built for a whole-app
116
- * `req.session` object, not this protocol's specific handle-table shape). `RedisCache` needs no real
117
- * persisted entity - only a class with a `.name` for its Redis key prefix - so a purely ephemeral,
118
- * never-persisted class like `MapiSessionContext` is a perfectly valid `type` for it.
131
+ * Stores `handle` at `index` with a fresh generation. Whatever previously held that index is released first
132
+ * (see `releaseHandle`), so a stream or cached data belonging to the old handle never attaches to the new one.
133
+ */
134
+ export declare function assignHandle(session: MapiSessionContext, index: number, handle: MapiObjectHandle): MapiObjectHandle;
135
+ /**
136
+ * Releases the handle at `index`: drops its cached data, and releases every write stream opened against it
137
+ * (a stream is meaningless once its draft message is gone, and would otherwise attach to whatever reuses the
138
+ * index next).
139
+ */
140
+ export declare function releaseHandle(session: MapiSessionContext, index: number): void;
141
+ /** The shared-store keys (FastTransfer streams, write-stream chunks) of every handle released on `session` since the
142
+ * last call, for the route to delete once the session's changes are saved. */
143
+ export declare function takeReleasedHandleData(session: MapiSessionContext): string[];
144
+ /** The outcome of `MapiSessionManager.save()`. `"conflict"` means another request saved the session first;
145
+ * `"missing"` that it expired or was ended; `"tooBig"` that it grew past `MAX_SESSION_BYTES` and was not saved. */
146
+ export type SessionSaveResult = "saved" | "conflict" | "missing" | "tooBig";
147
+ /**
148
+ * The storage a `MapiSessionManager` runs on: Redis when a `cache` datastore is configured, otherwise memory.
149
+ *
150
+ * A session is two entries: its JSON (`sessionKey`) and its version (`<sessionKey>.version`). A save compares the
151
+ * version entry only, so Redis never has to parse the session JSON.
152
+ */
153
+ export interface MapiSessionStore {
154
+ get(key: string): Promise<string | undefined>;
155
+ create(key: string, value: string, ttlSeconds: number): Promise<void>;
156
+ compareAndSet(key: string, expectedVersion: number, value: string, ttlSeconds: number): Promise<"saved" | "conflict" | "missing">;
157
+ delete(key: string): Promise<void>;
158
+ /** Atomically records `sessionId` in the user's index (ordered by last use, starting at `createdAtMs`): drops ids
159
+ * whose session is gone, ends the least recently used sessions until fewer than `max` remain, then adds it. Returns
160
+ * the ids it ended. */
161
+ addToUserIndex(indexKey: string, sessionId: string, createdAtMs: number, max: number, ttlSeconds: number): Promise<string[]>;
162
+ /** `SET NX` with a TTL: `true` when the lock was free and is now held with `token`. */
163
+ acquireLock(key: string, token: string, ttlMs: number): Promise<boolean>;
164
+ /** Releases the lock only if it is still held with `token`. */
165
+ releaseLock(key: string, token: string): Promise<void>;
166
+ /** Extends the lock to `ttlMs` from now, only while it is still held with `token`. */
167
+ renewLock(key: string, token: string, ttlMs: number): Promise<boolean>;
168
+ /** Moves `sessionId` to `score` in the user's index when it is still there. */
169
+ touchUserIndex(indexKey: string, sessionId: string, score: number): Promise<void>;
170
+ getValue(key: string): Promise<string | undefined>;
171
+ putValue(key: string, value: string, ttlSeconds: number): Promise<void>;
172
+ }
173
+ /** Creates a session at version 0. KEYS: session, version. ARGV: json, ttl. */
174
+ export declare const CREATE_SCRIPT = "\nredis.call('SETEX', KEYS[1], ARGV[2], ARGV[1])\nredis.call('SETEX', KEYS[2], ARGV[2], '0')\nreturn 1\n";
175
+ /** Replaces the session only when its version entry still equals the expected one, without decoding the session.
176
+ * KEYS: session, version. ARGV: expected version, json, ttl. Returns 1 saved, 0 on a version mismatch, -1 when gone. */
177
+ export declare const COMPARE_AND_SET_SCRIPT = "\nlocal current = redis.call('GET', KEYS[2])\nif not current or redis.call('EXISTS', KEYS[1]) == 0 then return -1 end\nif tonumber(current) ~= tonumber(ARGV[1]) then return 0 end\nredis.call('SETEX', KEYS[1], ARGV[3], ARGV[2])\nredis.call('SETEX', KEYS[2], ARGV[3], tostring(tonumber(ARGV[1]) + 1))\nreturn 1\n";
178
+ /** Adds a session to its user's sorted-set index in one step, so concurrent Connects can't each see room under the
179
+ * cap. KEYS: index. ARGV: session id, created-at score, max, index ttl, session key prefix, session key suffix. The
180
+ * session keys it checks and deletes are built from the ids, so they aren't declared in KEYS (fine on one Redis node,
181
+ * not on a cluster). Returns the ids it ended. */
182
+ export declare const ADD_TO_USER_INDEX_SCRIPT = "\nlocal function sessionKey(id) return ARGV[5] .. id .. ARGV[6] end\nfor _, id in ipairs(redis.call('ZRANGE', KEYS[1], 0, -1)) do\n if redis.call('EXISTS', sessionKey(id)) == 0 then redis.call('ZREM', KEYS[1], id) end\nend\nlocal ended = {}\nwhile redis.call('ZCARD', KEYS[1]) >= tonumber(ARGV[3]) do\n local oldest = redis.call('ZRANGE', KEYS[1], 0, 0)[1]\n redis.call('ZREM', KEYS[1], oldest)\n redis.call('DEL', sessionKey(oldest), sessionKey(oldest) .. '.version')\n table.insert(ended, oldest)\nend\nredis.call('ZADD', KEYS[1], ARGV[2], ARGV[1])\nredis.call('EXPIRE', KEYS[1], ARGV[4])\nreturn ended\n";
183
+ /** KEYS: lock. ARGV: token, ttl ms. Returns 1 when acquired. */
184
+ export declare const ACQUIRE_LOCK_SCRIPT = "\nif redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) then return 1 end\nreturn 0\n";
185
+ /** KEYS: lock. ARGV: token. Deletes the lock only while this token still holds it. */
186
+ export declare const RELEASE_LOCK_SCRIPT = "\nif redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) end\nreturn 1\n";
187
+ /** KEYS: lock. ARGV: token, ttl ms. Returns 1 when the token still held the lock and it was extended. */
188
+ export declare const RENEW_LOCK_SCRIPT = "\nif redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('PEXPIRE', KEYS[1], ARGV[2]) return 1 end\nreturn 0\n";
189
+ /** KEYS: user index. ARGV: score, session id. Updates the score of a member that is still present. */
190
+ export declare const TOUCH_USER_INDEX_SCRIPT = "\nredis.call('ZADD', KEYS[1], 'XX', ARGV[1], ARGV[2])\nreturn 1\n";
191
+ /** A `MapiSessionStore` on a node-redis client. Every read goes to Redis, never a per-process copy, so each
192
+ * replica sees the same session. */
193
+ export declare class RedisMapiSessionStore implements MapiSessionStore {
194
+ private readonly client;
195
+ constructor(client: any);
196
+ get(key: string): Promise<string | undefined>;
197
+ create(key: string, value: string, ttlSeconds: number): Promise<void>;
198
+ compareAndSet(key: string, expectedVersion: number, value: string, ttlSeconds: number): Promise<"saved" | "conflict" | "missing">;
199
+ delete(key: string): Promise<void>;
200
+ addToUserIndex(indexKey: string, sessionId: string, createdAtMs: number, max: number, ttlSeconds: number): Promise<string[]>;
201
+ acquireLock(key: string, token: string, ttlMs: number): Promise<boolean>;
202
+ releaseLock(key: string, token: string): Promise<void>;
203
+ renewLock(key: string, token: string, ttlMs: number): Promise<boolean>;
204
+ touchUserIndex(indexKey: string, sessionId: string, score: number): Promise<void>;
205
+ getValue(key: string): Promise<string | undefined>;
206
+ putValue(key: string, value: string, ttlSeconds: number): Promise<void>;
207
+ }
208
+ /** A single-process `MapiSessionStore`. Values are stored as strings so every load hands out an independent copy,
209
+ * exactly like the Redis store. No method awaits between reading and writing, so each one is atomic. */
210
+ export declare class MemoryMapiSessionStore implements MapiSessionStore {
211
+ private readonly entries;
212
+ private readonly indexes;
213
+ get(key: string): Promise<string | undefined>;
214
+ create(key: string, value: string, ttlSeconds: number): Promise<void>;
215
+ compareAndSet(key: string, expectedVersion: number, value: string, ttlSeconds: number): Promise<"saved" | "conflict" | "missing">;
216
+ delete(key: string): Promise<void>;
217
+ addToUserIndex(indexKey: string, sessionId: string, createdAtMs: number, max: number): Promise<string[]>;
218
+ acquireLock(key: string, token: string, ttlMs: number): Promise<boolean>;
219
+ releaseLock(key: string, token: string): Promise<void>;
220
+ renewLock(key: string, token: string, ttlMs: number): Promise<boolean>;
221
+ touchUserIndex(indexKey: string, sessionId: string, score: number): Promise<void>;
222
+ getValue(key: string): Promise<string | undefined>;
223
+ putValue(key: string, value: string, ttlSeconds: number): Promise<void>;
224
+ private read;
225
+ private write;
226
+ private sweep;
227
+ }
228
+ /** The store key of a session's JSON. */
229
+ export declare function sessionKey(sessionId: string): string;
230
+ /**
231
+ * Stores/loads `MapiSessionContext`s, keyed by the opaque session id that also becomes the `MapiContext` cookie
232
+ * value.
119
233
  *
120
- * @author Jean-Philippe Steinmetz
234
+ * Every load reads the shared store (Redis when a `cache` datastore is configured). An earlier version used
235
+ * `RedisCache`, whose per-process copy served a session without checking Redis, so with several replicas one pod
236
+ * could run ROPs against handles another pod had already changed. Saves are compare-and-set on `version`: when
237
+ * two requests on the same session overlap, the second save reports `"conflict"` instead of overwriting the
238
+ * first request's handles. `Execute` also holds the session's lock (`acquireLock`) while it runs, so an overlapping
239
+ * request is turned away before it has any side effects, and keeps its last response (`storeResponse`) so a retry of
240
+ * the same request is answered again instead of being run twice.
121
241
  */
122
242
  export declare class MapiSessionManager {
123
- private _objectFactory?;
124
- private cache?;
125
- init(): Promise<void>;
243
+ private redisClient?;
244
+ private store?;
245
+ /** Where handlers keep FastTransfer streams and write-stream chunks: in Redis next to the sessions when configured,
246
+ * so any replica can continue a transfer. */
247
+ handleDataStore?: HandleDataStore;
248
+ init(): void;
249
+ /** Creates a session for `userUid`, ending that user's least recently used sessions (and their stored handle data)
250
+ * if they already hold `MAX_SESSIONS_PER_USER`. */
126
251
  create(mailboxUid: string, userUid: string): Promise<MapiSessionContext>;
252
+ /** Records that `session` was just used, so the per-user cap ends the least recently used session rather than the
253
+ * oldest one. */
254
+ touch(session: MapiSessionContext): Promise<void>;
255
+ /** Loads a session straight from the store. A session past `MAX_SESSION_LIFETIME_MS` is ended and treated as
256
+ * missing. */
127
257
  load(sessionId: string): Promise<MapiSessionContext | undefined>;
128
- save(context: MapiSessionContext): Promise<void>;
258
+ /** Saves `context` only if nobody else saved it since it was loaded and it is within `MAX_SESSION_BYTES`,
259
+ * bumping its `version` on success. */
260
+ save(context: MapiSessionContext): Promise<SessionSaveResult>;
261
+ /** Ends a session, deleting its stored handle data too. */
129
262
  destroy(sessionId: string): Promise<void>;
263
+ /** Deletes every FastTransfer stream and write-stream chunk the session stored. Best effort: they expire anyway. */
264
+ private deleteHandleData;
265
+ /** Takes the session's `Execute` lock. Returns the token to release it with, or `undefined` while another
266
+ * request holds it. */
267
+ acquireLock(sessionId: string): Promise<string | undefined>;
268
+ releaseLock(sessionId: string, token: string): Promise<void>;
269
+ /** Extends the session's lock by another `SESSION_LOCK_TTL_MS`. `false` when `token` no longer holds it. */
270
+ renewLock(sessionId: string, token: string): Promise<boolean>;
271
+ /** The response last sent on this session if it answered `requestId`; `"inProgress"` while a request with that id
272
+ * is still running (or ran on a pod that died less than `SESSION_LOCK_TTL_MS` ago); otherwise `undefined`. */
273
+ storedResponse(sessionId: string, requestId: string): Promise<Buffer | "inProgress" | undefined>;
274
+ /** Records that `requestId` is running on this session, for `SESSION_LOCK_TTL_MS` (renewed along with the lock). A
275
+ * retry of it is then answered as busy instead of running a second time. */
276
+ markInProgress(sessionId: string, requestId: string): Promise<void>;
277
+ /** Extends the in-progress marker of `requestId` by another `SESSION_LOCK_TTL_MS`, only while it is still that marker:
278
+ * once the request's response (or anything else) has replaced it, nothing is written. `false` when it wasn't there. */
279
+ renewInProgress(sessionId: string, requestId: string): Promise<boolean>;
280
+ /** Drops the in-progress marker of `requestId` when it is still there (the request ended without storing a
281
+ * response), so a retry runs. Atomic, so it never deletes a response stored meanwhile. */
282
+ clearInProgress(sessionId: string, requestId: string): Promise<void>;
283
+ /** Remembers `body` as this session's answer to `requestId`. Only the latest request is kept. */
284
+ storeResponse(sessionId: string, requestId: string, body: Buffer): Promise<void>;
130
285
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Mirrors `@rapidrest/service-core`'s private `ModelUtils.MAX_PATTERN_LENGTH`: a `regex(...)` query operand
3
+ * longer than this is rejected as a potential ReDoS pattern with `INVALID_REQUEST`. The check runs against the
4
+ * operand AFTER escaping, so a raw client term well under the limit can still exceed it once each regex
5
+ * metacharacter doubles in length.
6
+ */
7
+ export declare const MAX_REGEX_PATTERN_LENGTH = 100;
8
+ /**
9
+ * Escapes `value` for a literal-substring `regex(...)` match, truncating the raw term (never mid-escape or
10
+ * mid-code-point) so the escaped result always fits `MAX_REGEX_PATTERN_LENGTH`. Matching on a prefix of an
11
+ * over-long search term still yields a superset of the full term's matches, which is a far better outcome than
12
+ * the whole command failing.
13
+ */
14
+ export declare function boundedEscapedPattern(value: string, maxLength?: number): string;
@@ -1,17 +1,53 @@
1
1
  import type { RopContext, RopHandler } from "./rop/RopHandler.js";
2
+ /** The most ROPs processed from one `Execute` request. A real client batches far fewer; anything past this is
3
+ * left unprocessed. */
4
+ export declare const MAX_ROPS_PER_EXECUTE = 1024;
5
+ /** The largest `RopsList` a response can carry: `RopSize` is a 16-bit count that includes its own 2 bytes. */
6
+ export declare const MAX_ROPS_LIST_BYTES: number;
7
+ /** `RopBufferTooSmall`'s `RopId` (`[MS-OXCROPS]` §2.2.15.1). */
8
+ export declare const ROP_ID_BUFFER_TOO_SMALL = 255;
9
+ /** `MAPI_E_CALL_FAILED`: a ROP failed while running (a database error, undecodable stored data, ...). */
10
+ export declare const ERROR_CALL_FAILED = 2147500037;
11
+ /** `MAPI_E_TOO_COMPLEX`: the ROP would have gone past this request's `ExecuteBudget`. */
12
+ export declare const ERROR_TOO_COMPLEX = 2147746071;
13
+ /** Thrown by `dispatchRops`, before any ROP runs, when the room for responses can't even hold a `RopBufferTooSmall`
14
+ * carrying the whole request. The route answers the `Execute` with `ecBufferTooSmall` ([MS-OXCRPC]). No ROP has run. */
15
+ export declare class ExecuteBufferTooSmallError extends Error {
16
+ constructor();
17
+ }
18
+ export interface DispatchOptions {
19
+ /** Stop after this many ROPs. Defaults to `MAX_ROPS_PER_EXECUTE`. */
20
+ maxRops?: number;
21
+ /** The room for response ROPs, in bytes. Defaults to `MAX_ROPS_LIST_BYTES`; the route passes what the client's
22
+ * `MaxRopOut` leaves after the `RopSize` field and handle table. */
23
+ maxOutputBytes?: number;
24
+ }
2
25
  /**
3
- * Walks a decoded `RopBuffer`'s `ropsList` blob, dispatching each contained ROP - identified by its own
4
- * leading `RopId` byte - to the matching registered handler in turn, collecting every response into one
5
- * output buffer. Deliberately ROP-agnostic: it knows nothing about any specific ROP's own fields, only how to
6
- * find the *next* one, via the handler's own advanced `reader` position once `handle()` returns - see
7
- * `RopBuffer.ts`'s doc comment for why a fully generic split isn't possible for this wire format.
26
+ * Walks a decoded `RopBuffer`'s `ropsList` blob, dispatching each contained ROP - identified by its own leading
27
+ * `RopId` byte - to the matching registered handler in turn, collecting every response into one output buffer.
28
+ * Deliberately ROP-agnostic: it knows nothing about any specific ROP's own fields, only how to find the *next* one,
29
+ * via the handler's own advanced `reader` position once `handle()` returns - see `RopBuffer.ts`'s doc comment for
30
+ * why a fully generic split isn't possible for this wire format.
8
31
  *
9
- * An unrecognized `RopId` stops processing immediately: the bytes belonging to that ROP (and everything after
10
- * it) can't be skipped without knowing that ROP's own layout, so there is no safe way to resync and continue.
11
- * This is an inherent constraint of the wire format, not a design choice this library is choosing not to
12
- * handle - a real client only ever sends ROPs the server has already told it (via its supported-command
13
- * surface) that it understands.
32
+ * - **Malformed requests.** An unrecognized `RopId`, or a ROP whose bytes run out or can't be sized (a
33
+ * `DecodeError`), throws a `DecodeError`: nothing after it can be located, so the route answers 400.
34
+ * - **A failing ROP.** Any other error thrown by a handler fails just that ROP: its partial output is dropped, a
35
+ * failure response (`RopId`, the handle index it echoes, `ReturnValue` `MAPI_E_CALL_FAILED` or, past the
36
+ * `ExecuteBudget`, `MAPI_E_TOO_COMPLEX`) takes its place, and the following ROPs still run. The handler has
37
+ * already read its whole request by the time it does work that can fail, so the next ROP is still where the
38
+ * reader points.
39
+ * - **Output space.** Room for a `RopBufferTooSmall` covering the next ROP and everything after it (3 bytes plus
40
+ * those request bytes) is always kept free. `context.ropOutputRemaining` tells each handler the room left *after*
41
+ * that reserve (computed from where the reader stands, so once a handler has read its request it excludes the
42
+ * handler's own bytes). A response that doesn't leave the reserve is replaced by `RopBufferTooSmall` (`SizeNeeded`
43
+ * plus the unprocessed request bytes, from this ROP on, for the client to resend) and processing stops; that always
44
+ * fits, because the reserve for this ROP was kept by the one before it. Handlers that change state by how much they
45
+ * return (`RopQueryRows`, `RopReadStream`, `RopFastTransferSourceGetBuffer`) size their response to the room, so they
46
+ * are never replaced after moving a cursor. Only when the request as a whole can't be covered by a
47
+ * `RopBufferTooSmall` is `ExecuteBufferTooSmallError` thrown, before anything runs, and the route answers the
48
+ * `Execute` with `ecBufferTooSmall` ([MS-OXCRPC]). So a failed `Execute` never follows state already changed and
49
+ * saved, which a retry would skip past.
14
50
  *
15
51
  * @author Jean-Philippe Steinmetz
16
52
  */
17
- export declare function dispatchRops(ropsList: Buffer, handlers: Map<number, RopHandler>, context: RopContext): Promise<Buffer>;
53
+ export declare function dispatchRops(ropsList: Buffer, handlers: Map<number, RopHandler>, context: RopContext, options?: DispatchOptions): Promise<Buffer>;
@@ -15,6 +15,14 @@
15
15
  *
16
16
  * @author Jean-Philippe Steinmetz
17
17
  */
18
+ /**
19
+ * A request that can't be decoded: truncated, or carrying a value (such as an unknown property type) whose wire
20
+ * size can't be determined. `RopDispatcher` answers the whole `Execute` with a 400 for one of these, since the bytes
21
+ * of every later ROP can no longer be located.
22
+ */
23
+ export declare class DecodeError extends RangeError {
24
+ constructor(message: string);
25
+ }
18
26
  export declare class BufferReader {
19
27
  private readonly buffer;
20
28
  private offset;
@@ -22,6 +30,8 @@ export declare class BufferReader {
22
30
  get position(): number;
23
31
  get remaining(): number;
24
32
  hasMore(): boolean;
33
+ /** Moves to `position` (backwards or forwards). Throws a `RangeError` for a position outside the buffer. */
34
+ seek(position: number): void;
25
35
  readUInt8(): number;
26
36
  readInt16LE(): number;
27
37
  readUInt16LE(): number;
@@ -62,6 +72,10 @@ export declare class BufferReader {
62
72
  * byte-array accumulator, adapted for multi-byte little-endian fields instead of single bytes. */
63
73
  export declare class BufferWriter {
64
74
  private readonly chunks;
75
+ private byteLength;
76
+ /** The number of bytes written so far. */
77
+ get length(): number;
78
+ private push;
65
79
  writeUInt8(value: number): this;
66
80
  writeInt16LE(value: number): this;
67
81
  writeUInt16LE(value: number): this;
@@ -3,9 +3,13 @@ import { BufferReader } from "./BufferCursor.js";
3
3
  * iCalendar UID (see this file's own doc comment for why that's the form that matters here), using `at` as the
4
4
  * `CreationTime`. */
5
5
  export declare function encodeGlobalObjectId(icalUid: string, at: Date): Buffer;
6
- /** Decodes a `GlobalObjectId` BLOB (read from `reader`'s current position) back into the `icalUid` it was
7
- * built from - unwrapping the `VCALID` form (see this file's own doc comment) when `Data` carries the
8
- * `"vCal-Uid"` marker, falling back to treating `Data` as bare `OutlookID` bytes otherwise. Throws if
9
- * `ByteArrayID` doesn't match the spec's own fixed constant - a real, spec-mandated identity check, not an
10
- * invented restriction. */
6
+ /** Decodes a `GlobalObjectId` BLOB (read from `reader`'s current position) back into the `icalUid` it stands for.
7
+ * A `VCALID` (see this file's own doc comment) yields the UID it wraps. Anything else is a native `OutlookID`,
8
+ * whose `Data` is binary, not text: per `[MS-OXCICAL]`'s UID conversion, the iCalendar UID of such an object is the
9
+ * uppercase hex of the whole `GlobalObjectId` with the instance date (`YH`/`YL`/`M`/`D`, bytes 16-19) zeroed, so
10
+ * that is what is returned (compare it case-insensitively). Throws if `ByteArrayID` doesn't match the spec's own
11
+ * fixed constant - a real, spec-mandated identity check, not an invented restriction. */
11
12
  export declare function decodeGlobalObjectId(reader: BufferReader): string;
13
+ /** The `YYYY-MM-DD` occurrence date a `GlobalObjectId` names (`YH`/`YL`/`M`/`D`), or `undefined` when it names the
14
+ * whole series (a zero year). `globalObjectId` must already have decoded successfully. */
15
+ export declare function globalObjectIdInstanceDate(globalObjectId: Buffer): string | undefined;