@rapidmx/activesync 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/LICENSE +382 -0
  2. package/README.md +57 -0
  3. package/dist/lib/BaseDeviceSyncStateRoute.js +90 -0
  4. package/dist/lib/BaseDeviceSyncStateRoute.js.map +1 -0
  5. package/dist/lib/BaseEasRoute.js +213 -0
  6. package/dist/lib/BaseEasRoute.js.map +1 -0
  7. package/dist/lib/CompactDateTime.js +43 -0
  8. package/dist/lib/CompactDateTime.js.map +1 -0
  9. package/dist/lib/EasCommandHandler.js +2 -0
  10. package/dist/lib/EasCommandHandler.js.map +1 -0
  11. package/dist/lib/EasSyncKeyUtils.js +119 -0
  12. package/dist/lib/EasSyncKeyUtils.js.map +1 -0
  13. package/dist/lib/adapters/CalendarSyncAdapter.js +255 -0
  14. package/dist/lib/adapters/CalendarSyncAdapter.js.map +1 -0
  15. package/dist/lib/adapters/ContactsSyncAdapter.js +164 -0
  16. package/dist/lib/adapters/ContactsSyncAdapter.js.map +1 -0
  17. package/dist/lib/adapters/EasCollectionSyncAdapter.js +2 -0
  18. package/dist/lib/adapters/EasCollectionSyncAdapter.js.map +1 -0
  19. package/dist/lib/adapters/EmailSyncAdapter.js +230 -0
  20. package/dist/lib/adapters/EmailSyncAdapter.js.map +1 -0
  21. package/dist/lib/adapters/TasksSyncAdapter.js +100 -0
  22. package/dist/lib/adapters/TasksSyncAdapter.js.map +1 -0
  23. package/dist/lib/codec/WbxmlCodePages.js +706 -0
  24. package/dist/lib/codec/WbxmlCodePages.js.map +1 -0
  25. package/dist/lib/codec/WbxmlDecoder.js +154 -0
  26. package/dist/lib/codec/WbxmlDecoder.js.map +1 -0
  27. package/dist/lib/codec/WbxmlElement.js +31 -0
  28. package/dist/lib/codec/WbxmlElement.js.map +1 -0
  29. package/dist/lib/codec/WbxmlEncoder.js +88 -0
  30. package/dist/lib/codec/WbxmlEncoder.js.map +1 -0
  31. package/dist/lib/commands/ComposeMailCommand.js +187 -0
  32. package/dist/lib/commands/ComposeMailCommand.js.map +1 -0
  33. package/dist/lib/commands/FolderSyncCommand.js +150 -0
  34. package/dist/lib/commands/FolderSyncCommand.js.map +1 -0
  35. package/dist/lib/commands/GetItemEstimateCommand.js +149 -0
  36. package/dist/lib/commands/GetItemEstimateCommand.js.map +1 -0
  37. package/dist/lib/commands/ItemOperationsCommand.js +337 -0
  38. package/dist/lib/commands/ItemOperationsCommand.js.map +1 -0
  39. package/dist/lib/commands/MeetingResponseCommand.js +120 -0
  40. package/dist/lib/commands/MeetingResponseCommand.js.map +1 -0
  41. package/dist/lib/commands/MoveItemsCommand.js +103 -0
  42. package/dist/lib/commands/MoveItemsCommand.js.map +1 -0
  43. package/dist/lib/commands/PingCommand.js +145 -0
  44. package/dist/lib/commands/PingCommand.js.map +1 -0
  45. package/dist/lib/commands/ProvisionCommand.js +164 -0
  46. package/dist/lib/commands/ProvisionCommand.js.map +1 -0
  47. package/dist/lib/commands/ResolveRecipientsCommand.js +124 -0
  48. package/dist/lib/commands/ResolveRecipientsCommand.js.map +1 -0
  49. package/dist/lib/commands/SearchCommand.js +137 -0
  50. package/dist/lib/commands/SearchCommand.js.map +1 -0
  51. package/dist/lib/commands/SendMailCommand.js +18 -0
  52. package/dist/lib/commands/SendMailCommand.js.map +1 -0
  53. package/dist/lib/commands/SettingsCommand.js +138 -0
  54. package/dist/lib/commands/SettingsCommand.js.map +1 -0
  55. package/dist/lib/commands/SmartForwardCommand.js +22 -0
  56. package/dist/lib/commands/SmartForwardCommand.js.map +1 -0
  57. package/dist/lib/commands/SmartReplyCommand.js +22 -0
  58. package/dist/lib/commands/SmartReplyCommand.js.map +1 -0
  59. package/dist/lib/commands/SyncCommand.js +406 -0
  60. package/dist/lib/commands/SyncCommand.js.map +1 -0
  61. package/dist/lib/commands/mongo/FolderSyncCommandMongo.js +16 -0
  62. package/dist/lib/commands/mongo/FolderSyncCommandMongo.js.map +1 -0
  63. package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js +21 -0
  64. package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js.map +1 -0
  65. package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js +18 -0
  66. package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js.map +1 -0
  67. package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js +17 -0
  68. package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js.map +1 -0
  69. package/dist/lib/commands/mongo/MoveItemsCommandMongo.js +17 -0
  70. package/dist/lib/commands/mongo/MoveItemsCommandMongo.js.map +1 -0
  71. package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js +20 -0
  72. package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js.map +1 -0
  73. package/dist/lib/commands/mongo/SearchCommandMongo.js +20 -0
  74. package/dist/lib/commands/mongo/SearchCommandMongo.js.map +1 -0
  75. package/dist/lib/commands/mongo/SendMailCommandMongo.js +17 -0
  76. package/dist/lib/commands/mongo/SendMailCommandMongo.js.map +1 -0
  77. package/dist/lib/commands/mongo/SettingsCommandMongo.js +16 -0
  78. package/dist/lib/commands/mongo/SettingsCommandMongo.js.map +1 -0
  79. package/dist/lib/commands/mongo/SmartForwardCommandMongo.js +17 -0
  80. package/dist/lib/commands/mongo/SmartForwardCommandMongo.js.map +1 -0
  81. package/dist/lib/commands/mongo/SmartReplyCommandMongo.js +17 -0
  82. package/dist/lib/commands/mongo/SmartReplyCommandMongo.js.map +1 -0
  83. package/dist/lib/commands/mongo/SyncCommandMongo.js +26 -0
  84. package/dist/lib/commands/mongo/SyncCommandMongo.js.map +1 -0
  85. package/dist/lib/commands/mongo/index.js +14 -0
  86. package/dist/lib/commands/mongo/index.js.map +1 -0
  87. package/dist/lib/commands/sql/FolderSyncCommandSQL.js +16 -0
  88. package/dist/lib/commands/sql/FolderSyncCommandSQL.js.map +1 -0
  89. package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js +21 -0
  90. package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js.map +1 -0
  91. package/dist/lib/commands/sql/ItemOperationsCommandSQL.js +18 -0
  92. package/dist/lib/commands/sql/ItemOperationsCommandSQL.js.map +1 -0
  93. package/dist/lib/commands/sql/MeetingResponseCommandSQL.js +17 -0
  94. package/dist/lib/commands/sql/MeetingResponseCommandSQL.js.map +1 -0
  95. package/dist/lib/commands/sql/MoveItemsCommandSQL.js +17 -0
  96. package/dist/lib/commands/sql/MoveItemsCommandSQL.js.map +1 -0
  97. package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js +20 -0
  98. package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js.map +1 -0
  99. package/dist/lib/commands/sql/SearchCommandSQL.js +20 -0
  100. package/dist/lib/commands/sql/SearchCommandSQL.js.map +1 -0
  101. package/dist/lib/commands/sql/SendMailCommandSQL.js +17 -0
  102. package/dist/lib/commands/sql/SendMailCommandSQL.js.map +1 -0
  103. package/dist/lib/commands/sql/SettingsCommandSQL.js +16 -0
  104. package/dist/lib/commands/sql/SettingsCommandSQL.js.map +1 -0
  105. package/dist/lib/commands/sql/SmartForwardCommandSQL.js +17 -0
  106. package/dist/lib/commands/sql/SmartForwardCommandSQL.js.map +1 -0
  107. package/dist/lib/commands/sql/SmartReplyCommandSQL.js +17 -0
  108. package/dist/lib/commands/sql/SmartReplyCommandSQL.js.map +1 -0
  109. package/dist/lib/commands/sql/SyncCommandSQL.js +26 -0
  110. package/dist/lib/commands/sql/SyncCommandSQL.js.map +1 -0
  111. package/dist/lib/commands/sql/index.js +14 -0
  112. package/dist/lib/commands/sql/index.js.map +1 -0
  113. package/dist/lib/index.js +56 -0
  114. package/dist/lib/index.js.map +1 -0
  115. package/dist/lib/mongo/DeviceSyncStateRouteMongo.js +16 -0
  116. package/dist/lib/mongo/DeviceSyncStateRouteMongo.js.map +1 -0
  117. package/dist/lib/mongo/EasRouteMongo.js +51 -0
  118. package/dist/lib/mongo/EasRouteMongo.js.map +1 -0
  119. package/dist/lib/mongo/index.js +8 -0
  120. package/dist/lib/mongo/index.js.map +1 -0
  121. package/dist/lib/mongo.js +6 -0
  122. package/dist/lib/mongo.js.map +1 -0
  123. package/dist/lib/sql/DeviceSyncStateRouteSQL.js +16 -0
  124. package/dist/lib/sql/DeviceSyncStateRouteSQL.js.map +1 -0
  125. package/dist/lib/sql/EasRouteSQL.js +50 -0
  126. package/dist/lib/sql/EasRouteSQL.js.map +1 -0
  127. package/dist/lib/sql/index.js +8 -0
  128. package/dist/lib/sql/index.js.map +1 -0
  129. package/dist/lib/sql.js +6 -0
  130. package/dist/lib/sql.js.map +1 -0
  131. package/dist/types/BaseDeviceSyncStateRoute.d.ts +34 -0
  132. package/dist/types/BaseEasRoute.d.ts +64 -0
  133. package/dist/types/CompactDateTime.d.ts +22 -0
  134. package/dist/types/EasCommandHandler.d.ts +59 -0
  135. package/dist/types/EasSyncKeyUtils.d.ts +89 -0
  136. package/dist/types/adapters/CalendarSyncAdapter.d.ts +52 -0
  137. package/dist/types/adapters/ContactsSyncAdapter.d.ts +33 -0
  138. package/dist/types/adapters/EasCollectionSyncAdapter.d.ts +52 -0
  139. package/dist/types/adapters/EmailSyncAdapter.d.ts +53 -0
  140. package/dist/types/adapters/TasksSyncAdapter.d.ts +32 -0
  141. package/dist/types/codec/WbxmlCodePages.d.ts +45 -0
  142. package/dist/types/codec/WbxmlDecoder.d.ts +33 -0
  143. package/dist/types/codec/WbxmlElement.d.ts +43 -0
  144. package/dist/types/codec/WbxmlEncoder.d.ts +20 -0
  145. package/dist/types/commands/ComposeMailCommand.d.ts +50 -0
  146. package/dist/types/commands/FolderSyncCommand.d.ts +25 -0
  147. package/dist/types/commands/GetItemEstimateCommand.d.ts +43 -0
  148. package/dist/types/commands/ItemOperationsCommand.d.ts +72 -0
  149. package/dist/types/commands/MeetingResponseCommand.d.ts +32 -0
  150. package/dist/types/commands/MoveItemsCommand.d.ts +29 -0
  151. package/dist/types/commands/PingCommand.d.ts +33 -0
  152. package/dist/types/commands/ProvisionCommand.d.ts +54 -0
  153. package/dist/types/commands/ResolveRecipientsCommand.d.ts +34 -0
  154. package/dist/types/commands/SearchCommand.d.ts +39 -0
  155. package/dist/types/commands/SendMailCommand.d.ts +10 -0
  156. package/dist/types/commands/SettingsCommand.d.ts +37 -0
  157. package/dist/types/commands/SmartForwardCommand.d.ts +17 -0
  158. package/dist/types/commands/SmartReplyCommand.d.ts +17 -0
  159. package/dist/types/commands/SyncCommand.d.ts +100 -0
  160. package/dist/types/commands/mongo/FolderSyncCommandMongo.d.ts +8 -0
  161. package/dist/types/commands/mongo/GetItemEstimateCommandMongo.d.ts +7 -0
  162. package/dist/types/commands/mongo/ItemOperationsCommandMongo.d.ts +9 -0
  163. package/dist/types/commands/mongo/MeetingResponseCommandMongo.d.ts +8 -0
  164. package/dist/types/commands/mongo/MoveItemsCommandMongo.d.ts +8 -0
  165. package/dist/types/commands/mongo/ResolveRecipientsCommandMongo.d.ts +8 -0
  166. package/dist/types/commands/mongo/SearchCommandMongo.d.ts +8 -0
  167. package/dist/types/commands/mongo/SendMailCommandMongo.d.ts +8 -0
  168. package/dist/types/commands/mongo/SettingsCommandMongo.d.ts +7 -0
  169. package/dist/types/commands/mongo/SmartForwardCommandMongo.d.ts +8 -0
  170. package/dist/types/commands/mongo/SmartReplyCommandMongo.d.ts +8 -0
  171. package/dist/types/commands/mongo/SyncCommandMongo.d.ts +8 -0
  172. package/dist/types/commands/mongo/index.d.ts +9 -0
  173. package/dist/types/commands/sql/FolderSyncCommandSQL.d.ts +8 -0
  174. package/dist/types/commands/sql/GetItemEstimateCommandSQL.d.ts +7 -0
  175. package/dist/types/commands/sql/ItemOperationsCommandSQL.d.ts +9 -0
  176. package/dist/types/commands/sql/MeetingResponseCommandSQL.d.ts +8 -0
  177. package/dist/types/commands/sql/MoveItemsCommandSQL.d.ts +8 -0
  178. package/dist/types/commands/sql/ResolveRecipientsCommandSQL.d.ts +8 -0
  179. package/dist/types/commands/sql/SearchCommandSQL.d.ts +8 -0
  180. package/dist/types/commands/sql/SendMailCommandSQL.d.ts +8 -0
  181. package/dist/types/commands/sql/SettingsCommandSQL.d.ts +7 -0
  182. package/dist/types/commands/sql/SmartForwardCommandSQL.d.ts +8 -0
  183. package/dist/types/commands/sql/SmartReplyCommandSQL.d.ts +8 -0
  184. package/dist/types/commands/sql/SyncCommandSQL.d.ts +8 -0
  185. package/dist/types/commands/sql/index.d.ts +9 -0
  186. package/dist/types/index.d.ts +51 -0
  187. package/dist/types/mongo/DeviceSyncStateRouteMongo.d.ts +8 -0
  188. package/dist/types/mongo/EasRouteMongo.d.ts +14 -0
  189. package/dist/types/mongo/index.d.ts +3 -0
  190. package/dist/types/mongo.d.ts +1 -0
  191. package/dist/types/sql/DeviceSyncStateRouteSQL.d.ts +8 -0
  192. package/dist/types/sql/EasRouteSQL.d.ts +13 -0
  193. package/dist/types/sql/index.d.ts +3 -0
  194. package/dist/types/sql.d.ts +1 -0
  195. package/package.json +94 -0
@@ -0,0 +1,213 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ ///////////////////////////////////////////////////////////////////////////////
14
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
15
+ // SPDX-License-Identifier: MPL-2.0
16
+ ///////////////////////////////////////////////////////////////////////////////
17
+ import { ApiError, ObjectDecorators } from "@rapidrest/core";
18
+ import { ApiErrorMessages, ApiErrors, RepoUtils, RouteDecorators, } from "@rapidrest/service-core";
19
+ import { WbxmlDecoder } from "./codec/WbxmlDecoder.js";
20
+ import { WbxmlEncoder } from "./codec/WbxmlEncoder.js";
21
+ import { persistDeviceSyncState } from "./EasSyncKeyUtils.js";
22
+ import { resolveCallerMailboxUid } from "@rapidmx/restapi";
23
+ const { Init, Logger } = ObjectDecorators;
24
+ const { Auth, Options, Post, Request, Response, User: AuthUser } = RouteDecorators;
25
+ /** HTTP 449 ("Retry With") is not a standard HTTP status, but is the long-established Exchange ActiveSync
26
+ * convention a real client recognizes as "you must successfully complete `Provision` before this command will
27
+ * be honored" - simpler than constructing a command-specific WBXML error body for every possible command a
28
+ * client might send before it's provisioned. */
29
+ const HTTP_STATUS_RETRY_WITH = 449;
30
+ /** `MS-ASProtocolVersions` value this library actually implements against: confirmed via `[MS-ASHTTP]` that
31
+ * "14.0"/"14.1" are the versions whose `ComposeMail`/`Email2` WBXML code pages cover MIME-based
32
+ * `SendMail`/`SmartForward`/`SmartReply` (what `ComposeMailCommand` actually sends) - not 12.x (which predates
33
+ * MIME-based compose). `16.0`/`16.1` are now included too, now that `SettingsCommand` implements `Oof` (their
34
+ * other headline addition, `RightsManagementInformation`, remains unimplemented, but that alone doesn't gate
35
+ * the version string - `MS-ASProtocolCommands` below is the real capability gate, derived live from
36
+ * `this.handlers`, so a client probing capabilities correctly sees any specific unsupported command regardless
37
+ * of which protocol versions are declared, exactly as it already does for `GetItemEstimate`/`MoveItems`/
38
+ * `ResolveRecipients`). No MS-ASCMD `Sync`/`FolderSync`/`Provision` schema element became newly mandatory
39
+ * between 14.1 and 16.1 outside `Oof`/IRM. Ascending, matching the order Microsoft's own spec lists them in. */
40
+ const MS_AS_PROTOCOL_VERSIONS = "14.0,14.1,16.0,16.1";
41
+ function firstQueryValue(value) {
42
+ return Array.isArray(value) ? value[0] : value;
43
+ }
44
+ /**
45
+ * Abstract base for the single fixed EAS endpoint (`POST /Microsoft-Server-ActiveSync` by MS-ASHTTP
46
+ * convention, though the concrete path is left to the consuming application to mount via `@Route(...)` — see
47
+ * `BaseMailIngestRoute.ts` for the identical undecorated-base-class pattern this follows). Unlike every other
48
+ * route in this library, EAS dispatches on a `Cmd` query parameter against one URL rather than path-based REST
49
+ * routing, so there is exactly one `@Post()` method here, not one per operation.
50
+ *
51
+ * **Auth**: `@Auth(["jwt"])` — the framework's own default strategy, unchanged from every other route in this
52
+ * app. A real native EAS client obtaining that JWT in the first place (rather than the app's own web/API
53
+ * clients, which already have one) requires an OAuth 2.0 Authorization Server capability this library
54
+ * deliberately does not implement itself — see the architecture plan's "Auth" section for the full reasoning
55
+ * behind this choice over a per-request Basic Auth strategy.
56
+ *
57
+ * **Dispatch flow**: resolve the caller's own `Mailbox` (never a client-supplied one — `resolveCallerMailboxUid`,
58
+ * the same helper `BaseSearchRoute` already uses), find-or-create that (mailbox, device) pair's
59
+ * `DeviceSyncState`, enforce the provisioning gate, decode the WBXML request body (if any), dispatch to the
60
+ * matching registered `EasCommandHandler`, bump `lastSyncAt`, and encode the handler's response back to WBXML.
61
+ *
62
+ * **Command handlers** are supplied via `commandHandlerClasses` (empty by default — this class alone is just
63
+ * the transport skeleton; concrete command support, e.g. `ProvisionCommand`/`FolderSyncCommand`, is added
64
+ * incrementally in later work by having a concrete subclass populate this array) and instantiated once each in
65
+ * `@Init` via `ObjectFactory`, so a handler can `@Inject` its own dependencies like any other DI-managed class
66
+ * in this library.
67
+ *
68
+ * **`OPTIONS` protocol discovery**: real EAS clients conventionally probe `OPTIONS` before their first `POST`
69
+ * to read `MS-ASProtocolVersions`/`MS-ASProtocolCommands` and learn what the server supports - see `options()`
70
+ * below. This requires `@rapidrest/service-core` >=1.5.0, whose global CORS middleware consults
71
+ * `IHttpRouter.hasExplicitOptionsRoute()` before its blanket preflight `204` - confirmed live end to end
72
+ * against a real `service-core` 1.5.0 install (`test/routes/{mongo,sql}/EasRoute.test.ts`'s `OPTIONS`
73
+ * describe block), not assumed.
74
+ *
75
+ * `deviceSyncStateClass`/`mailboxClass` are supplied by the Mongo/SQL concrete subclasses, following the exact
76
+ * one-line-per-backend pattern used throughout this library's other routes/jobs.
77
+ *
78
+ * @author Jean-Philippe Steinmetz
79
+ */
80
+ export class BaseEasRoute {
81
+ constructor() {
82
+ /** Command handler classes to instantiate (one each) in `@Init`. Empty until a concrete command lands -
83
+ * every request is then answered with HTTP 501 (see `dispatch()`), which is the correct, honest behavior
84
+ * for a transport skeleton with no commands implemented yet, not a bug to work around. */
85
+ this.commandHandlerClasses = [];
86
+ this.handlers = new Map();
87
+ }
88
+ async init() {
89
+ this.deviceSyncStateRepo = await this._objectFactory.newInstance(RepoUtils, {
90
+ name: this.deviceSyncStateClass.name,
91
+ args: [this.deviceSyncStateClass],
92
+ });
93
+ this.mailboxRepo = await this._objectFactory.newInstance(RepoUtils, {
94
+ name: this.mailboxClass.name,
95
+ args: [this.mailboxClass],
96
+ });
97
+ for (const HandlerClass of this.commandHandlerClasses) {
98
+ const handler = await this._objectFactory.newInstance(HandlerClass);
99
+ this.handlers.set(handler.command, handler);
100
+ }
101
+ }
102
+ /**
103
+ * Answers a real client's pre-flight `MS-ASProtocolVersions`/`MS-ASProtocolCommands` capability probe -
104
+ * see this class's own doc comment for the `service-core` version dependency this needs to actually run.
105
+ * Deliberately unauthenticated (no `@Auth`): this is capability discovery, not mailbox access, and a real
106
+ * Exchange server answers it the same way regardless of credentials. `MS-ASProtocolCommands` is built from
107
+ * `this.handlers`, not a separately-maintained list, so it can never drift out of sync with the commands a
108
+ * concrete subclass actually registered via `commandHandlerClasses`.
109
+ */
110
+ async options(res) {
111
+ res.setHeader("MS-ASProtocolVersions", MS_AS_PROTOCOL_VERSIONS)
112
+ .setHeader("MS-ASProtocolCommands", Array.from(this.handlers.keys()).join(","))
113
+ .status(200)
114
+ .send();
115
+ }
116
+ async dispatch(req, res, user) {
117
+ if (!this.deviceSyncStateRepo || !this.mailboxRepo) {
118
+ throw new ApiError(ApiErrors.INTERNAL_ERROR, 500, ApiErrorMessages.INTERNAL_ERROR);
119
+ }
120
+ if (!user) {
121
+ throw new ApiError(ApiErrors.AUTH_PERMISSION_FAILURE, 403, ApiErrorMessages.AUTH_PERMISSION_FAILURE);
122
+ }
123
+ const cmd = firstQueryValue(req.query["Cmd"]);
124
+ const deviceId = firstQueryValue(req.query["DeviceId"]);
125
+ const deviceType = firstQueryValue(req.query["DeviceType"]) ?? "Unknown";
126
+ const policyKey = firstQueryValue(req.query["PolicyKey"]);
127
+ if (!cmd || !deviceId) {
128
+ throw new ApiError(ApiErrors.INVALID_REQUEST, 400, ApiErrorMessages.INVALID_REQUEST);
129
+ }
130
+ const mailboxUid = await resolveCallerMailboxUid(this.mailboxRepo, user);
131
+ if (!mailboxUid) {
132
+ throw new ApiError(ApiErrors.NOT_FOUND, 404, ApiErrorMessages.NOT_FOUND);
133
+ }
134
+ const deviceSyncState = await this.findOrCreateDeviceSyncState(mailboxUid, deviceId, deviceType);
135
+ // Every command except the two that must work on an unprovisioned device (Provision itself, and
136
+ // Settings - real clients query Settings/DeviceInformation as part of first-run setup, before
137
+ // provisioning completes) requires the device to have already been provisioned.
138
+ if (!deviceSyncState.provisioned && cmd !== "Provision" && cmd !== "Settings") {
139
+ res.status(HTTP_STATUS_RETRY_WITH).send();
140
+ return;
141
+ }
142
+ const handler = this.handlers.get(cmd);
143
+ if (!handler) {
144
+ res.status(501).send();
145
+ return;
146
+ }
147
+ const request = req.rawBody && req.rawBody.length > 0 ? new WbxmlDecoder().decode(req.rawBody) : undefined;
148
+ const response = await handler.handle({
149
+ user,
150
+ mailboxUid,
151
+ deviceId,
152
+ deviceType,
153
+ policyKey,
154
+ deviceSyncState,
155
+ deviceSyncStateRepo: this.deviceSyncStateRepo,
156
+ query: req.query,
157
+ request,
158
+ req,
159
+ });
160
+ await persistDeviceSyncState(deviceSyncState, this.deviceSyncStateRepo, { lastSyncAt: new Date() });
161
+ if (!response) {
162
+ res.status(200).send();
163
+ return;
164
+ }
165
+ const buffer = new WbxmlEncoder().encode(response);
166
+ res.setHeader("Content-Type", "application/vnd.ms-sync.wbxml")
167
+ .setHeader("Content-Length", buffer.length)
168
+ .status(200)
169
+ .send(buffer);
170
+ }
171
+ async findOrCreateDeviceSyncState(mailboxUid, deviceId, deviceType) {
172
+ const existing = await this.deviceSyncStateRepo.find({ mailboxUid, deviceId }, { ignoreACL: true, limit: 1 });
173
+ if (existing[0]) {
174
+ return existing[0];
175
+ }
176
+ const instance = this.deviceSyncStateRepo.instantiateObject({
177
+ mailboxUid,
178
+ deviceId,
179
+ deviceType,
180
+ folderSyncKeys: {},
181
+ provisioned: false,
182
+ });
183
+ return await this.deviceSyncStateRepo.create(instance, { ignoreACL: true });
184
+ }
185
+ }
186
+ __decorate([
187
+ Logger,
188
+ __metadata("design:type", Object)
189
+ ], BaseEasRoute.prototype, "logger", void 0);
190
+ __decorate([
191
+ Init,
192
+ __metadata("design:type", Function),
193
+ __metadata("design:paramtypes", []),
194
+ __metadata("design:returntype", Promise)
195
+ ], BaseEasRoute.prototype, "init", null);
196
+ __decorate([
197
+ Options(),
198
+ __param(0, Response),
199
+ __metadata("design:type", Function),
200
+ __metadata("design:paramtypes", [Object]),
201
+ __metadata("design:returntype", Promise)
202
+ ], BaseEasRoute.prototype, "options", null);
203
+ __decorate([
204
+ Auth(["jwt"]),
205
+ Post(),
206
+ __param(0, Request),
207
+ __param(1, Response),
208
+ __param(2, AuthUser),
209
+ __metadata("design:type", Function),
210
+ __metadata("design:paramtypes", [Object, Object, Object]),
211
+ __metadata("design:returntype", Promise)
212
+ ], BaseEasRoute.prototype, "dispatch", null);
213
+ //# sourceMappingURL=BaseEasRoute.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseEasRoute.js","sourceRoot":"","sources":["../../src/BaseEasRoute.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz. All rights reserved.
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Formats a date as MS-ASDTYPE's "Compact DateTime" (`YYYYMMDDTHHMMSSZ`, always UTC) - the format
7
+ * `Calendar`/`Tasks` timestamp fields (`StartTime`/`EndTime`/`UtcDueDate`/...) use, distinct from the plain
8
+ * `dateTime` type (`YYYY-MM-DDTHH:MM:SS.MSSZ`, i.e. `Date.prototype.toISOString()`) `Email`'s `DateReceived`
9
+ * uses - confirmed against the published MS-ASDTYPE spec (`2.7.2 Compact DateTime` vs `2.7 dateTime Data
10
+ * Type`), not assumed, since sending the wrong one is exactly the kind of silent-until-a-real-device-connects
11
+ * bug this library's WBXML codec work already ran into once with tag casing (`"Mime"` vs `"MIME"`).
12
+ *
13
+ * Accepts `Date | string` and normalizes via `new Date(...)` - a real, discovered-by-testing gap: fields
14
+ * embedded inside a `simple-json` column (e.g. `RecurrenceRule.until`) round-trip through `JSON.stringify`/
15
+ * `JSON.parse` on the SQL backend, which does not preserve `Date` instances, so `until` comes back as a plain
16
+ * ISO string there even though the Mongo backend (native BSON dates) hands back a real `Date` for the exact
17
+ * same field - a caller passing either must work on both backends without knowing which one it's talking to.
18
+ */
19
+ export function toCompactDateTime(date) {
20
+ const d = date instanceof Date ? date : new Date(date);
21
+ const pad = (n) => String(n).padStart(2, "0");
22
+ return (`${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}` +
23
+ `T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`);
24
+ }
25
+ /** Anchored strictly to `toCompactDateTime`'s own output shape (`YYYYMMDDTHHMMSSZ`) - a client sending
26
+ * anything else has sent a malformed item, which the caller (an `EasCollectionSyncAdapter.fromApplicationData`
27
+ * implementation) should treat as `Status 6` rather than silently guessing. */
28
+ const COMPACT_DATE_TIME_PATTERN = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/;
29
+ /**
30
+ * Parses MS-ASDTYPE's "Compact DateTime" (`YYYYMMDDTHHMMSSZ`) back into a `Date` - the reverse of
31
+ * `toCompactDateTime`, needed for `Sync`'s client-originated `Add`/`Change` commands on `Calendar`/`Tasks`.
32
+ * Throws (rather than returning an unvalidated `Date` that would silently carry `NaN`s) for anything not
33
+ * matching the exact expected shape.
34
+ */
35
+ export function fromCompactDateTime(value) {
36
+ const match = COMPACT_DATE_TIME_PATTERN.exec(value);
37
+ if (!match) {
38
+ throw new Error(`Not a valid Compact DateTime value: '${value}'`);
39
+ }
40
+ const [, year, month, day, hour, minute, second] = match;
41
+ return new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second)));
42
+ }
43
+ //# sourceMappingURL=CompactDateTime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CompactDateTime.js","sourceRoot":"","sources":["../../src/CompactDateTime.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,mEAAmE;AACnE,mCAAmC;AACnC,+EAA+E;AAE/E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAmB;IACjD,MAAM,CAAC,GAAG,IAAI,YAAY,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,GAAG,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC9D,OAAO,CACH,GAAG,CAAC,CAAC,cAAc,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,EAAE;QACxE,IAAI,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAC,GAAG,CAChF,CAAC;AACN,CAAC;AAED;;+EAE+E;AAC/E,MAAM,yBAAyB,GAAG,gDAAgD,CAAC;AAEnF;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC7C,MAAM,KAAK,GAAG,yBAAyB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpD,IAAI,CAAC,KAAK,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,wCAAwC,KAAK,GAAG,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,GAAG,KAAK,CAAC;IACzD,OAAO,IAAI,IAAI,CACX,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CACvG,CAAC;AACN,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=EasCommandHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EasCommandHandler.js","sourceRoot":"","sources":["../../src/EasCommandHandler.ts"],"names":[],"mappings":""}
@@ -0,0 +1,119 @@
1
+ /** Formats a `SyncKey` back into its wire string form. */
2
+ export function formatSyncKey(key) {
3
+ return `${key.generation}:${key.watermark.toISOString()}`;
4
+ }
5
+ /** Parses a stored/previously-issued `SyncKey` string. Returns `undefined` for a malformed value - callers
6
+ * only ever parse a key this library itself minted and stored (never a raw, unvalidated client value; see
7
+ * `resolveSyncKey`'s doc comment), so `undefined` here signals corrupted persisted state, not client input. */
8
+ export function parseSyncKey(value) {
9
+ const separator = value.indexOf(":");
10
+ if (separator === -1) {
11
+ return undefined;
12
+ }
13
+ const generation = Number(value.slice(0, separator));
14
+ const watermark = new Date(value.slice(separator + 1));
15
+ if (!Number.isFinite(generation) || Number.isNaN(watermark.getTime())) {
16
+ return undefined;
17
+ }
18
+ return { generation, watermark };
19
+ }
20
+ /**
21
+ * Resolves an incoming client `SyncKey` string against the value this library itself previously issued and
22
+ * stored (`storedValue`, e.g. `DeviceSyncState.folderSyncKeys[folderUid]`) — deliberately a plain string
23
+ * equality check, not a re-parse-and-compare of the client's value, so a client that echoes back anything
24
+ * other than the exact opaque string it was handed is treated as `"invalid"` even if it happens to parse.
25
+ *
26
+ * - `"0"` (or empty/missing) from the client is always `"initial"` regardless of `storedValue` — an EAS
27
+ * client legitimately sends this to (re)start a collection from scratch (first-ever sync, or recovering
28
+ * from an `"invalid"` response elsewhere), and the spec requires the server honor it unconditionally.
29
+ * - Otherwise, a match against `storedValue` is `"valid"`; anything else (including `storedValue` being
30
+ * unset, i.e. the server has no record of ever issuing a key for this scope) is `"invalid"` — forcing the
31
+ * client back to `"0"`, per spec, rather than guessing at recovery.
32
+ */
33
+ export function resolveSyncKey(clientValue, storedValue) {
34
+ if (!clientValue || clientValue === "0") {
35
+ return { kind: "initial" };
36
+ }
37
+ if (storedValue === undefined || clientValue !== storedValue) {
38
+ return { kind: "invalid" };
39
+ }
40
+ const parsed = parseSyncKey(storedValue);
41
+ return parsed ? { kind: "valid", key: parsed } : { kind: "invalid" };
42
+ }
43
+ /**
44
+ * Applies `patch` to `deviceSyncState` and persists it - the one correct way any EAS command handler (or
45
+ * `BaseEasRoute` itself) should ever write to a `DeviceSyncState`, replacing the inline
46
+ * `deviceSyncStateRepo.update(...)` calls `ProvisionCommand`/`FolderSyncCommand`/`SyncCommand` used to each
47
+ * repeat individually.
48
+ *
49
+ * This exists because `RepoUtils.update()` does **not** mutate the `existing` object passed to it - confirmed
50
+ * by reading its implementation directly - it only returns a freshly-fetched instance reflecting the write,
51
+ * leaving the caller's in-memory copy (including its optimistic-concurrency `version`) stale. A single write
52
+ * per request is harmless (nothing re-reads the stale copy), but a *second* write to the same `deviceSyncState`
53
+ * later in the same request - e.g. `BaseEasRoute.dispatch()`'s own trailing `lastSyncAt` update, or a second
54
+ * collection's SyncKey in a multi-collection `Sync` - would build its own patch's `version` off that stale
55
+ * value. `RepoUtils.update()` filters its underlying write by exactly that `version`, so the write silently
56
+ * matches zero rows instead of throwing - a silent no-op, not a visible error. Copying `update()`'s own
57
+ * returned result back onto `deviceSyncState` (last line below) keeps every field, including `version`, current
58
+ * for whatever writes this same request still has left to make.
59
+ */
60
+ export async function persistDeviceSyncState(deviceSyncState, deviceSyncStateRepo, patch) {
61
+ Object.assign(deviceSyncState, patch);
62
+ const updated = await deviceSyncStateRepo.update({ uid: deviceSyncState.uid, version: deviceSyncState.version, ...patch }, deviceSyncState, { ignoreACL: true, skipPush: true });
63
+ Object.assign(deviceSyncState, updated);
64
+ }
65
+ /** A tolerance window for treating a row's `dateCreated`/`dateModified` as "close enough" to be an `Add`
66
+ * rather than a `Change` - the two are set within the same request handler and so are typically only
67
+ * milliseconds apart, never exactly equal down to the microsecond depending on the datastore's clock
68
+ * resolution. */
69
+ const NEWLY_CREATED_TOLERANCE_MS = 1000;
70
+ /**
71
+ * Enumerates `Add`/`Change`/`Delete`s for one scoped collection since `watermark`, for `FolderSyncCommand` and
72
+ * (eventually) `SyncCommand`'s shared cursor mechanism. Requires `T` to be `RecoverableBaseEntity` (soft
73
+ * delete) — see `RecoverableRepoUtils`'s own doc comment for why a plain hard-deleted entity can't support
74
+ * this at all (nothing left to enumerate once a row is actually gone).
75
+ *
76
+ * `RepoUtils.find()` does **not** support an `includeDeleted` option - confirmed by reading its source: unlike
77
+ * `count()`/`findOne()`, it never strips `ModelUtils.buildSearchQuery()`'s default `deleted: false` exclusion
78
+ * back out. Rather than the (harmless-looking but silently no-op) `includeDeleted: true` this function used to
79
+ * pass, deleted rows are fetched via a **second** `find()` call with an explicit, literal `deleted: true` in
80
+ * the query object itself - `buildSearchQuery()`'s exclusion only applies when the caller's query has no
81
+ * `"deleted"` key at all, so supplying one directly (rather than relying on an option `find()` doesn't honor)
82
+ * reliably selects exactly the soft-deleted rows instead. The two result sets are then merged and re-windowed
83
+ * together so `windowSize`/`MoreAvailable` still describe the combined stream, not each half independently.
84
+ */
85
+ export async function computeChanges(repo, scopeField, scopeUid, watermark, windowSize) {
86
+ // Overfetch by one (per stream) so a full window can be distinguished from an exact-fit window without a
87
+ // separate count() round trip - the same trick `ExternalShareExpirationJob`/`MailboxQuotaRecalcJob` already
88
+ // use for their own MoreAvailable-shaped bookkeeping. `limit`/`sort` must be baked into the query object
89
+ // itself (not just `options`) for the SQL backend - see those same jobs' comments on this exact requirement.
90
+ const baseCriteria = { [scopeField]: scopeUid, dateModified: `gt(${watermark.toISOString()})` };
91
+ const findOptions = { ignoreACL: true, limit: windowSize + 1, sort: "dateModified" };
92
+ const [liveRows, deletedRows] = await Promise.all([
93
+ repo.find({ ...baseCriteria, sort: "dateModified", limit: windowSize + 1 }, findOptions),
94
+ repo.find({ ...baseCriteria, deleted: true, sort: "dateModified", limit: windowSize + 1 }, findOptions),
95
+ ]);
96
+ const merged = [...liveRows, ...deletedRows].sort((a, b) => new Date(a.dateModified).getTime() - new Date(b.dateModified).getTime());
97
+ const moreAvailable = merged.length > windowSize;
98
+ const page = moreAvailable ? merged.slice(0, windowSize) : merged;
99
+ const adds = [];
100
+ const changes = [];
101
+ const deletes = [];
102
+ let newWatermark = watermark;
103
+ for (const row of page) {
104
+ const dateModified = new Date(row.dateModified);
105
+ if (dateModified.getTime() > newWatermark.getTime()) {
106
+ newWatermark = dateModified;
107
+ }
108
+ if (row.deleted === true) {
109
+ deletes.push(row);
110
+ }
111
+ else {
112
+ const dateCreated = new Date(row.dateCreated);
113
+ const isNewlyCreated = Math.abs(dateModified.getTime() - dateCreated.getTime()) < NEWLY_CREATED_TOLERANCE_MS;
114
+ (isNewlyCreated ? adds : changes).push(row);
115
+ }
116
+ }
117
+ return { adds, changes, deletes, newWatermark, moreAvailable };
118
+ }
119
+ //# sourceMappingURL=EasSyncKeyUtils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EasSyncKeyUtils.js","sourceRoot":"","sources":["../../src/EasSyncKeyUtils.ts"],"names":[],"mappings":"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"}