@rapidmx/mapi-plugin 1.0.0-beta.3 → 1.0.0-beta.4
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.
- package/README.md +4 -4
- package/dist/lib/BaseMapiEmsmdbRoute.js +211 -36
- package/dist/lib/BaseMapiEmsmdbRoute.js.map +1 -1
- package/dist/lib/MapiSessionManager.js +404 -48
- package/dist/lib/MapiSessionManager.js.map +1 -1
- package/dist/lib/RegexPatternUtils.js +30 -0
- package/dist/lib/RegexPatternUtils.js.map +1 -0
- package/dist/lib/RopDispatcher.js +130 -19
- package/dist/lib/RopDispatcher.js.map +1 -1
- package/dist/lib/codec/AppointmentRecurrence.js +22 -21
- package/dist/lib/codec/AppointmentRecurrence.js.map +1 -1
- package/dist/lib/codec/BufferCursor.js +41 -12
- package/dist/lib/codec/BufferCursor.js.map +1 -1
- package/dist/lib/codec/GlobalObjectId.js +26 -20
- package/dist/lib/codec/GlobalObjectId.js.map +1 -1
- package/dist/lib/codec/MapiTimeZone.js +43 -10
- package/dist/lib/codec/MapiTimeZone.js.map +1 -1
- package/dist/lib/codec/PropertyValue.js +28 -2
- package/dist/lib/codec/PropertyValue.js.map +1 -1
- package/dist/lib/codec/RopBuffer.js +15 -3
- package/dist/lib/codec/RopBuffer.js.map +1 -1
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js +2 -1
- package/dist/lib/mongo/MapiEmsmdbRouteMongo.js.map +1 -1
- package/dist/lib/nspi/NspiCodec.js +5 -5
- package/dist/lib/nspi/NspiCodec.js.map +1 -1
- package/dist/lib/nspi/NspiGetMatchesHandler.js +14 -14
- package/dist/lib/nspi/NspiGetMatchesHandler.js.map +1 -1
- package/dist/lib/rop/AddressList.js +109 -0
- package/dist/lib/rop/AddressList.js.map +1 -0
- package/dist/lib/rop/CalendarEventTarget.js +7 -4
- package/dist/lib/rop/CalendarEventTarget.js.map +1 -1
- package/dist/lib/rop/ContactTarget.js +2 -6
- package/dist/lib/rop/ContactTarget.js.map +1 -1
- package/dist/lib/rop/ContentsTable.js +60 -0
- package/dist/lib/rop/ContentsTable.js.map +1 -0
- package/dist/lib/rop/ExecuteBudget.js +82 -0
- package/dist/lib/rop/ExecuteBudget.js.map +1 -0
- package/dist/lib/rop/FastTransferStream.js +99 -2
- package/dist/lib/rop/FastTransferStream.js.map +1 -1
- package/dist/lib/rop/FolderTarget.js +13 -6
- package/dist/lib/rop/FolderTarget.js.map +1 -1
- package/dist/lib/rop/HandleDataCache.js +412 -0
- package/dist/lib/rop/HandleDataCache.js.map +1 -0
- package/dist/lib/rop/MeetingMessageClassHandler.js +194 -48
- package/dist/lib/rop/MeetingMessageClassHandler.js.map +1 -1
- package/dist/lib/rop/MessageBodyStream.js +33 -7
- package/dist/lib/rop/MessageBodyStream.js.map +1 -1
- package/dist/lib/rop/MessageTarget.js +17 -4
- package/dist/lib/rop/MessageTarget.js.map +1 -1
- package/dist/lib/rop/NamedPropertyRegistry.js +8 -5
- package/dist/lib/rop/NamedPropertyRegistry.js.map +1 -1
- package/dist/lib/rop/PropertyResolvers.js +3 -1
- package/dist/lib/rop/PropertyResolvers.js.map +1 -1
- package/dist/lib/rop/RepoPaging.js +54 -0
- package/dist/lib/rop/RepoPaging.js.map +1 -0
- package/dist/lib/rop/RestapiRules.js +52 -0
- package/dist/lib/rop/RestapiRules.js.map +1 -0
- package/dist/lib/rop/RopCreateMessageHandler.js +4 -2
- package/dist/lib/rop/RopCreateMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopDeleteFolderHandler.js +112 -77
- package/dist/lib/rop/RopDeleteFolderHandler.js.map +1 -1
- package/dist/lib/rop/RopDeleteMessagesHandler.js +17 -1
- package/dist/lib/rop/RopDeleteMessagesHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js +14 -14
- package/dist/lib/rop/RopFastTransferSourceCopyPropertiesHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js +13 -14
- package/dist/lib/rop/RopFastTransferSourceCopyToHandler.js.map +1 -1
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js +33 -32
- package/dist/lib/rop/RopFastTransferSourceGetBufferHandler.js.map +1 -1
- package/dist/lib/rop/RopGetContentsTableHandler.js +17 -38
- package/dist/lib/rop/RopGetContentsTableHandler.js.map +1 -1
- package/dist/lib/rop/RopGetHierarchyTableHandler.js +4 -2
- package/dist/lib/rop/RopGetHierarchyTableHandler.js.map +1 -1
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js +6 -8
- package/dist/lib/rop/RopGetPropertiesSpecificHandler.js.map +1 -1
- package/dist/lib/rop/RopHandler.js +5 -1
- package/dist/lib/rop/RopHandler.js.map +1 -1
- package/dist/lib/rop/RopLogonHandler.js +5 -2
- package/dist/lib/rop/RopLogonHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenFolderHandler.js +3 -1
- package/dist/lib/rop/RopOpenFolderHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenMessageHandler.js +3 -1
- package/dist/lib/rop/RopOpenMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopOpenStreamHandler.js +11 -8
- package/dist/lib/rop/RopOpenStreamHandler.js.map +1 -1
- package/dist/lib/rop/RopQueryRowsHandler.js +31 -5
- package/dist/lib/rop/RopQueryRowsHandler.js.map +1 -1
- package/dist/lib/rop/RopReadStreamHandler.js +22 -4
- package/dist/lib/rop/RopReadStreamHandler.js.map +1 -1
- package/dist/lib/rop/RopReleaseHandler.js +4 -1
- package/dist/lib/rop/RopReleaseHandler.js.map +1 -1
- package/dist/lib/rop/RopSaveChangesMessageHandler.js +66 -7
- package/dist/lib/rop/RopSaveChangesMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopSetColumnsHandler.js +6 -8
- package/dist/lib/rop/RopSetColumnsHandler.js.map +1 -1
- package/dist/lib/rop/RopSetPropertiesHandler.js +23 -12
- package/dist/lib/rop/RopSetPropertiesHandler.js.map +1 -1
- package/dist/lib/rop/RopSubmitMessageHandler.js +144 -120
- package/dist/lib/rop/RopSubmitMessageHandler.js.map +1 -1
- package/dist/lib/rop/RopWriteStreamHandler.js +37 -10
- package/dist/lib/rop/RopWriteStreamHandler.js.map +1 -1
- package/dist/lib/rop/TaskTarget.js +2 -6
- package/dist/lib/rop/TaskTarget.js.map +1 -1
- package/dist/lib/rop/TransportSend.js +16 -0
- package/dist/lib/rop/TransportSend.js.map +1 -0
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js +2 -1
- package/dist/lib/sql/MapiEmsmdbRouteSQL.js.map +1 -1
- package/dist/types/BaseMapiEmsmdbRoute.d.ts +52 -2
- package/dist/types/MapiSessionManager.d.ts +218 -63
- package/dist/types/RegexPatternUtils.d.ts +14 -0
- package/dist/types/RopDispatcher.d.ts +47 -11
- package/dist/types/codec/BufferCursor.d.ts +14 -0
- package/dist/types/codec/GlobalObjectId.d.ts +9 -5
- package/dist/types/codec/PropertyValue.d.ts +10 -0
- package/dist/types/codec/RopBuffer.d.ts +7 -0
- package/dist/types/mongo/MapiEmsmdbRouteMongo.d.ts +1 -0
- package/dist/types/nspi/NspiGetMatchesHandler.d.ts +2 -0
- package/dist/types/rop/AddressList.d.ts +48 -0
- package/dist/types/rop/CalendarEventTarget.d.ts +7 -3
- package/dist/types/rop/ContactTarget.d.ts +3 -3
- package/dist/types/rop/ContentsTable.d.ts +16 -0
- package/dist/types/rop/ExecuteBudget.d.ts +48 -0
- package/dist/types/rop/FastTransferStream.d.ts +41 -2
- package/dist/types/rop/FolderTarget.d.ts +3 -2
- package/dist/types/rop/HandleDataCache.d.ts +145 -0
- package/dist/types/rop/MeetingMessageClassHandler.d.ts +23 -29
- package/dist/types/rop/MessageBodyStream.d.ts +16 -7
- package/dist/types/rop/MessageTarget.d.ts +10 -3
- package/dist/types/rop/NamedPropertyRegistry.d.ts +6 -2
- package/dist/types/rop/PropertyResolvers.d.ts +1 -1
- package/dist/types/rop/RepoPaging.d.ts +30 -0
- package/dist/types/rop/RestapiRules.d.ts +32 -0
- package/dist/types/rop/RopCreateMessageHandler.d.ts +1 -0
- package/dist/types/rop/RopDeleteFolderHandler.d.ts +36 -21
- package/dist/types/rop/RopDeleteMessagesHandler.d.ts +7 -0
- package/dist/types/rop/RopFastTransferSourceCopyPropertiesHandler.d.ts +1 -0
- package/dist/types/rop/RopFastTransferSourceCopyToHandler.d.ts +1 -0
- package/dist/types/rop/RopFastTransferSourceGetBufferHandler.d.ts +10 -11
- package/dist/types/rop/RopGetContentsTableHandler.d.ts +7 -13
- package/dist/types/rop/RopGetHierarchyTableHandler.d.ts +1 -0
- package/dist/types/rop/RopHandler.d.ts +25 -1
- package/dist/types/rop/RopOpenFolderHandler.d.ts +1 -0
- package/dist/types/rop/RopOpenMessageHandler.d.ts +1 -0
- package/dist/types/rop/RopOpenStreamHandler.d.ts +1 -0
- package/dist/types/rop/RopQueryRowsHandler.d.ts +3 -0
- package/dist/types/rop/RopReadStreamHandler.d.ts +3 -1
- package/dist/types/rop/RopReleaseHandler.d.ts +1 -0
- package/dist/types/rop/RopSaveChangesMessageHandler.d.ts +16 -1
- package/dist/types/rop/RopSubmitMessageHandler.d.ts +33 -23
- package/dist/types/rop/RopWriteStreamHandler.d.ts +18 -8
- package/dist/types/rop/TaskTarget.d.ts +3 -3
- package/dist/types/rop/TransportSend.d.ts +10 -0
- package/dist/types/sql/MapiEmsmdbRouteSQL.d.ts +1 -0
- package/package.json +3 -3
|
@@ -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;
|
|
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. */
|
|
@@ -61,6 +79,7 @@ export declare abstract class BaseMapiEmsmdbRoute<M extends Mailbox> {
|
|
|
61
79
|
private scanPipeline?;
|
|
62
80
|
private mailTransport?;
|
|
63
81
|
private logger;
|
|
82
|
+
private config?;
|
|
64
83
|
init(): Promise<void>;
|
|
65
84
|
dispatch(req: HttpRequest, res: HttpResponse, user?: JWTUser): Promise<void>;
|
|
66
85
|
/**
|
|
@@ -71,12 +90,43 @@ export declare abstract class BaseMapiEmsmdbRoute<M extends Mailbox> {
|
|
|
71
90
|
*/
|
|
72
91
|
private handleConnect;
|
|
73
92
|
/**
|
|
74
|
-
* `Execute` decodes the outer envelope (`Flags`/`RopBufferSize`/`RopBuffer`/...) and the inner ROP
|
|
75
|
-
* framing (`RopBuffer.ts`), dispatches every contained ROP via `RopDispatcher`, and re-encodes the
|
|
93
|
+
* `Execute` decodes the outer envelope (`Flags`/`RopBufferSize`/`RopBuffer`/`MaxRopOut`/...) and the inner ROP
|
|
94
|
+
* buffer framing (`RopBuffer.ts`), dispatches every contained ROP via `RopDispatcher`, and re-encodes the
|
|
76
95
|
* collected responses - preserving the incoming `handleTable` unchanged (this pragmatic subset never
|
|
77
96
|
* allocates/frees table-wide handle slots at the framing level; individual `RopHandler`s manage their own
|
|
78
97
|
* entries within `session.handles` instead).
|
|
98
|
+
*
|
|
99
|
+
* - **One request at a time per session.** Only the caller's own session (whose mailbox is still the caller's) is
|
|
100
|
+
* locked. The lock is taken before any ROP runs and renewed every `SESSION_LOCK_RENEW_MS` while the request runs,
|
|
101
|
+
* so a slow request keeps it; an overlapping request gets `X-ResponseCode` 15 (Invalid Sequence) without any side
|
|
102
|
+
* effects. The save stays compare-and-set. Each `Execute` also refreshes the session's place in the per-user cap.
|
|
103
|
+
* - **Retries.** While a request with an `X-RequestId` runs, an in-progress marker for that id is kept (renewed with
|
|
104
|
+
* the lock); a retry of it is answered with code 15 instead of running again. Its response then replaces the
|
|
105
|
+
* marker, and a request repeating the last id (a client retrying after a lost response) gets that response again
|
|
106
|
+
* instead of running its ROPs twice. A request that ends without a stored response drops its marker.
|
|
107
|
+
* `MapiSequence` isn't validated - the lock and the replay cache cover what it guards against here.
|
|
108
|
+
* - **Output size.** ROP responses are held to the client's `MaxRopOut`, less the `RopSize` field and handle
|
|
109
|
+
* table, and to what a 16-bit `RopSize` can describe (see `dispatchRops`). Room for a `RopBufferTooSmall` covering
|
|
110
|
+
* the rest of the request is kept free as ROPs run, so a response that doesn't fit is always answered that way (a
|
|
111
|
+
* normal, stored response). Only a `MaxRopOut` too small for a `RopBufferTooSmall` of the whole request answers
|
|
112
|
+
* `ecBufferTooSmall` in the body `ErrorCode`, before any ROP runs, so nothing is stored and a retry changes nothing.
|
|
113
|
+
* - **Released handles.** Once the session is saved, the FastTransfer streams and write-stream chunks of handles
|
|
114
|
+
* the request released are deleted from the shared store; `Disconnect` and session eviction delete a session's
|
|
115
|
+
* whole set.
|
|
116
|
+
* - **Malformed ROPs.** A ROP that can't be decoded answers 400; the ROPs before it already ran, so the session is
|
|
117
|
+
* still saved first.
|
|
79
118
|
*/
|
|
80
119
|
private handleExecute;
|
|
120
|
+
/** Runs an `Execute` under its session's lock. `running.requestId` is set while this request's in-progress marker is
|
|
121
|
+
* stored, and cleared once its response is stored instead. */
|
|
122
|
+
private executeLocked;
|
|
123
|
+
/** Writes an `Execute` failure body (no ROP buffer) with the given `X-ResponseCode` and `ErrorCode`. */
|
|
124
|
+
private sendExecuteFailure;
|
|
125
|
+
/**
|
|
126
|
+
* Loads the session named by the `MapiContext` cookie, but only if the authenticated caller is the user who
|
|
127
|
+
* `Connect`ed it. The cookie value alone is a bearer token for someone else's mailbox otherwise, so a session
|
|
128
|
+
* belonging to another user is treated exactly like one that doesn't exist.
|
|
129
|
+
*/
|
|
130
|
+
private loadOwnSession;
|
|
81
131
|
private handleDisconnect;
|
|
82
132
|
}
|
|
@@ -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.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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).
|
|
21
|
-
*
|
|
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)
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
54
|
-
* `uid`) rather than this library's own `BaseEntity
|
|
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
|
|
58
|
-
*
|
|
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
|
|
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.
|
|
82
|
-
*
|
|
83
|
-
* `
|
|
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
|
|
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
|
|
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
|
|
112
|
-
*
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
* `
|
|
117
|
-
*
|
|
118
|
-
*
|
|
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
|
-
*
|
|
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
|
|
124
|
-
private
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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;
|
|
@@ -50,6 +50,16 @@ export type PropertyValueData = number | bigint | boolean | string | Buffer | Da
|
|
|
50
50
|
* the low half. On the wire (little-endian), that means `PropertyType` is read/written first.
|
|
51
51
|
*/
|
|
52
52
|
export declare function readPropertyTag(reader: BufferReader): PropertyTag;
|
|
53
|
+
/** The most `PropertyTag`s accepted in one request's tag array (`RopSetColumns`, `RopGetPropertiesSpecific`,
|
|
54
|
+
* the FastTransfer source ROPs, NSPI `Columns`). Every tag multiplies the per-row resolution work and, for
|
|
55
|
+
* `RopSetColumns`, the stored session state; a real client asks for far fewer. */
|
|
56
|
+
export declare const MAX_PROPERTY_TAG_COUNT = 256;
|
|
57
|
+
/**
|
|
58
|
+
* Reads `count` consecutive `PropertyTag`s. When `count` exceeds `MAX_PROPERTY_TAG_COUNT`, the tags' bytes are
|
|
59
|
+
* skipped (still bounds-checked, so a count larger than the buffer throws) and `undefined` is returned, leaving
|
|
60
|
+
* the reader positioned after the array so the caller can send an error response and keep parsing later ROPs.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readPropertyTagArray(reader: BufferReader, count: number): PropertyTag[] | undefined;
|
|
53
63
|
export declare function writePropertyTag(writer: BufferWriter, tag: PropertyTag): void;
|
|
54
64
|
/** `TypedPropertyValue` structure (`[MS-OXCDATA]` §2.11.4): `PropertyType` (2 bytes) followed by the value
|
|
55
65
|
* itself, encoded per `readPropertyValue`/`writePropertyValue` below. */
|