@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.
- package/LICENSE +382 -0
- package/README.md +57 -0
- package/dist/lib/BaseDeviceSyncStateRoute.js +90 -0
- package/dist/lib/BaseDeviceSyncStateRoute.js.map +1 -0
- package/dist/lib/BaseEasRoute.js +213 -0
- package/dist/lib/BaseEasRoute.js.map +1 -0
- package/dist/lib/CompactDateTime.js +43 -0
- package/dist/lib/CompactDateTime.js.map +1 -0
- package/dist/lib/EasCommandHandler.js +2 -0
- package/dist/lib/EasCommandHandler.js.map +1 -0
- package/dist/lib/EasSyncKeyUtils.js +119 -0
- package/dist/lib/EasSyncKeyUtils.js.map +1 -0
- package/dist/lib/adapters/CalendarSyncAdapter.js +255 -0
- package/dist/lib/adapters/CalendarSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/ContactsSyncAdapter.js +164 -0
- package/dist/lib/adapters/ContactsSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/EasCollectionSyncAdapter.js +2 -0
- package/dist/lib/adapters/EasCollectionSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/EmailSyncAdapter.js +230 -0
- package/dist/lib/adapters/EmailSyncAdapter.js.map +1 -0
- package/dist/lib/adapters/TasksSyncAdapter.js +100 -0
- package/dist/lib/adapters/TasksSyncAdapter.js.map +1 -0
- package/dist/lib/codec/WbxmlCodePages.js +706 -0
- package/dist/lib/codec/WbxmlCodePages.js.map +1 -0
- package/dist/lib/codec/WbxmlDecoder.js +154 -0
- package/dist/lib/codec/WbxmlDecoder.js.map +1 -0
- package/dist/lib/codec/WbxmlElement.js +31 -0
- package/dist/lib/codec/WbxmlElement.js.map +1 -0
- package/dist/lib/codec/WbxmlEncoder.js +88 -0
- package/dist/lib/codec/WbxmlEncoder.js.map +1 -0
- package/dist/lib/commands/ComposeMailCommand.js +187 -0
- package/dist/lib/commands/ComposeMailCommand.js.map +1 -0
- package/dist/lib/commands/FolderSyncCommand.js +150 -0
- package/dist/lib/commands/FolderSyncCommand.js.map +1 -0
- package/dist/lib/commands/GetItemEstimateCommand.js +149 -0
- package/dist/lib/commands/GetItemEstimateCommand.js.map +1 -0
- package/dist/lib/commands/ItemOperationsCommand.js +337 -0
- package/dist/lib/commands/ItemOperationsCommand.js.map +1 -0
- package/dist/lib/commands/MeetingResponseCommand.js +120 -0
- package/dist/lib/commands/MeetingResponseCommand.js.map +1 -0
- package/dist/lib/commands/MoveItemsCommand.js +103 -0
- package/dist/lib/commands/MoveItemsCommand.js.map +1 -0
- package/dist/lib/commands/PingCommand.js +145 -0
- package/dist/lib/commands/PingCommand.js.map +1 -0
- package/dist/lib/commands/ProvisionCommand.js +164 -0
- package/dist/lib/commands/ProvisionCommand.js.map +1 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js +124 -0
- package/dist/lib/commands/ResolveRecipientsCommand.js.map +1 -0
- package/dist/lib/commands/SearchCommand.js +137 -0
- package/dist/lib/commands/SearchCommand.js.map +1 -0
- package/dist/lib/commands/SendMailCommand.js +18 -0
- package/dist/lib/commands/SendMailCommand.js.map +1 -0
- package/dist/lib/commands/SettingsCommand.js +138 -0
- package/dist/lib/commands/SettingsCommand.js.map +1 -0
- package/dist/lib/commands/SmartForwardCommand.js +22 -0
- package/dist/lib/commands/SmartForwardCommand.js.map +1 -0
- package/dist/lib/commands/SmartReplyCommand.js +22 -0
- package/dist/lib/commands/SmartReplyCommand.js.map +1 -0
- package/dist/lib/commands/SyncCommand.js +406 -0
- package/dist/lib/commands/SyncCommand.js.map +1 -0
- package/dist/lib/commands/mongo/FolderSyncCommandMongo.js +16 -0
- package/dist/lib/commands/mongo/FolderSyncCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js +21 -0
- package/dist/lib/commands/mongo/GetItemEstimateCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js +18 -0
- package/dist/lib/commands/mongo/ItemOperationsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/MeetingResponseCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/MoveItemsCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/MoveItemsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js +20 -0
- package/dist/lib/commands/mongo/ResolveRecipientsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js +20 -0
- package/dist/lib/commands/mongo/SearchCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SendMailCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SendMailCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SettingsCommandMongo.js +16 -0
- package/dist/lib/commands/mongo/SettingsCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SmartForwardCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SmartForwardCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SmartReplyCommandMongo.js +17 -0
- package/dist/lib/commands/mongo/SmartReplyCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/SyncCommandMongo.js +26 -0
- package/dist/lib/commands/mongo/SyncCommandMongo.js.map +1 -0
- package/dist/lib/commands/mongo/index.js +14 -0
- package/dist/lib/commands/mongo/index.js.map +1 -0
- package/dist/lib/commands/sql/FolderSyncCommandSQL.js +16 -0
- package/dist/lib/commands/sql/FolderSyncCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js +21 -0
- package/dist/lib/commands/sql/GetItemEstimateCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/ItemOperationsCommandSQL.js +18 -0
- package/dist/lib/commands/sql/ItemOperationsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/MeetingResponseCommandSQL.js +17 -0
- package/dist/lib/commands/sql/MeetingResponseCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/MoveItemsCommandSQL.js +17 -0
- package/dist/lib/commands/sql/MoveItemsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js +20 -0
- package/dist/lib/commands/sql/ResolveRecipientsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js +20 -0
- package/dist/lib/commands/sql/SearchCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SendMailCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SendMailCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SettingsCommandSQL.js +16 -0
- package/dist/lib/commands/sql/SettingsCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SmartForwardCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SmartForwardCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SmartReplyCommandSQL.js +17 -0
- package/dist/lib/commands/sql/SmartReplyCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/SyncCommandSQL.js +26 -0
- package/dist/lib/commands/sql/SyncCommandSQL.js.map +1 -0
- package/dist/lib/commands/sql/index.js +14 -0
- package/dist/lib/commands/sql/index.js.map +1 -0
- package/dist/lib/index.js +56 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js +16 -0
- package/dist/lib/mongo/DeviceSyncStateRouteMongo.js.map +1 -0
- package/dist/lib/mongo/EasRouteMongo.js +51 -0
- package/dist/lib/mongo/EasRouteMongo.js.map +1 -0
- package/dist/lib/mongo/index.js +8 -0
- package/dist/lib/mongo/index.js.map +1 -0
- package/dist/lib/mongo.js +6 -0
- package/dist/lib/mongo.js.map +1 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js +16 -0
- package/dist/lib/sql/DeviceSyncStateRouteSQL.js.map +1 -0
- package/dist/lib/sql/EasRouteSQL.js +50 -0
- package/dist/lib/sql/EasRouteSQL.js.map +1 -0
- package/dist/lib/sql/index.js +8 -0
- package/dist/lib/sql/index.js.map +1 -0
- package/dist/lib/sql.js +6 -0
- package/dist/lib/sql.js.map +1 -0
- package/dist/types/BaseDeviceSyncStateRoute.d.ts +34 -0
- package/dist/types/BaseEasRoute.d.ts +64 -0
- package/dist/types/CompactDateTime.d.ts +22 -0
- package/dist/types/EasCommandHandler.d.ts +59 -0
- package/dist/types/EasSyncKeyUtils.d.ts +89 -0
- package/dist/types/adapters/CalendarSyncAdapter.d.ts +52 -0
- package/dist/types/adapters/ContactsSyncAdapter.d.ts +33 -0
- package/dist/types/adapters/EasCollectionSyncAdapter.d.ts +52 -0
- package/dist/types/adapters/EmailSyncAdapter.d.ts +53 -0
- package/dist/types/adapters/TasksSyncAdapter.d.ts +32 -0
- package/dist/types/codec/WbxmlCodePages.d.ts +45 -0
- package/dist/types/codec/WbxmlDecoder.d.ts +33 -0
- package/dist/types/codec/WbxmlElement.d.ts +43 -0
- package/dist/types/codec/WbxmlEncoder.d.ts +20 -0
- package/dist/types/commands/ComposeMailCommand.d.ts +50 -0
- package/dist/types/commands/FolderSyncCommand.d.ts +25 -0
- package/dist/types/commands/GetItemEstimateCommand.d.ts +43 -0
- package/dist/types/commands/ItemOperationsCommand.d.ts +72 -0
- package/dist/types/commands/MeetingResponseCommand.d.ts +32 -0
- package/dist/types/commands/MoveItemsCommand.d.ts +29 -0
- package/dist/types/commands/PingCommand.d.ts +33 -0
- package/dist/types/commands/ProvisionCommand.d.ts +54 -0
- package/dist/types/commands/ResolveRecipientsCommand.d.ts +34 -0
- package/dist/types/commands/SearchCommand.d.ts +39 -0
- package/dist/types/commands/SendMailCommand.d.ts +10 -0
- package/dist/types/commands/SettingsCommand.d.ts +37 -0
- package/dist/types/commands/SmartForwardCommand.d.ts +17 -0
- package/dist/types/commands/SmartReplyCommand.d.ts +17 -0
- package/dist/types/commands/SyncCommand.d.ts +100 -0
- package/dist/types/commands/mongo/FolderSyncCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/GetItemEstimateCommandMongo.d.ts +7 -0
- package/dist/types/commands/mongo/ItemOperationsCommandMongo.d.ts +9 -0
- package/dist/types/commands/mongo/MeetingResponseCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/MoveItemsCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/ResolveRecipientsCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SearchCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SendMailCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SettingsCommandMongo.d.ts +7 -0
- package/dist/types/commands/mongo/SmartForwardCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SmartReplyCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/SyncCommandMongo.d.ts +8 -0
- package/dist/types/commands/mongo/index.d.ts +9 -0
- package/dist/types/commands/sql/FolderSyncCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/GetItemEstimateCommandSQL.d.ts +7 -0
- package/dist/types/commands/sql/ItemOperationsCommandSQL.d.ts +9 -0
- package/dist/types/commands/sql/MeetingResponseCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/MoveItemsCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/ResolveRecipientsCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SearchCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SendMailCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SettingsCommandSQL.d.ts +7 -0
- package/dist/types/commands/sql/SmartForwardCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SmartReplyCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/SyncCommandSQL.d.ts +8 -0
- package/dist/types/commands/sql/index.d.ts +9 -0
- package/dist/types/index.d.ts +51 -0
- package/dist/types/mongo/DeviceSyncStateRouteMongo.d.ts +8 -0
- package/dist/types/mongo/EasRouteMongo.d.ts +14 -0
- package/dist/types/mongo/index.d.ts +3 -0
- package/dist/types/mongo.d.ts +1 -0
- package/dist/types/sql/DeviceSyncStateRouteSQL.d.ts +8 -0
- package/dist/types/sql/EasRouteSQL.d.ts +13 -0
- package/dist/types/sql/index.d.ts +3 -0
- package/dist/types/sql.d.ts +1 -0
- 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 @@
|
|
|
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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"}
|