diff --git a/.ably/capabilities.yaml b/.ably/capabilities.yaml index 739b2fe54a..ce13a41b5d 100644 --- a/.ably/capabilities.yaml +++ b/.ably/capabilities.yaml @@ -1,6 +1,6 @@ %YAML 1.2 --- -common-version: 1.2.0 +common-version: 1.2.1 compliance: Agent Identifier: Agents: @@ -46,6 +46,7 @@ compliance: Get Identifier: Incremental Backoff: Lifecycle Control: + OS Connectivity Events: Ping: Recovery: State Events: diff --git a/CHANGELOG.md b/CHANGELOG.md index d042f33f3c..6e6e9efb76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ This contains only the most important and/or user-facing changes; for a full changelog, see the commit history. +## [1.2.42](https://github.com/ably/ably-js/tree/1.2.42) (2023-07-24) + +- Auth: remain connected upon failed authorize unless returning explicit 403 [\#1385](https://github.com/ably/ably-js/pull/1385) +- Make `Utils#inspectError` use `toString` for `Error`-like values [\#1391](https://github.com/ably/ably-js/pull/1391) +- docs: fix description of AuthOptions.token [\#1368](https://github.com/ably/ably-js/pull/1368) + +## [1.2.41](https://github.com/ably/ably-js/tree/1.2.41) (2023-06-29) + +- add `ChannelStateChange.hasBacklog` and return state change to attach promise/callback [\#1347](https://github.com/ably/ably-js/pull/1347) +- fix a bug where host fallback was initially skipped after falling back to the base transport [\#1357](https://github.com/ably/ably-js/pull/1357) + ## [1.2.40](https://github.com/ably/ably-js/tree/1.2.40) (2023-05-26) This release adds a new experimental `channels.getDerived` method which allows you to create custom realtime data feeds by selectively subscribing to receive only part of the data from the channel. See the [announcement post](https://pages.ably.com/subscription-filters-preview) for more information. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c040df3e5..1247b408b2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,12 @@ 11. For nontrivial releases: update the ably-js submodule ref in the realtime repo 12. Update the [Ably Changelog](https://changelog.ably.com/) (via [headwayapp](https://headwayapp.co/)) with these changes (again, you can just copy the notes you added to the CHANGELOG) +## Building the library + +To build the library, simply run `npm run build`. Building the library currently requires NodeJS <= v16. + +Since webpack builds are slow, commands are also available to only build the output for specific platforms (eg `npm run build:node`), see [package.json](./package.json) for the full list of available commands + ## Test suite To run the Mocha tests, simply run the following command: @@ -42,6 +48,14 @@ Or run just one test file npm run test:node -- --file=test/realtime/auth.test.js +Or run just one test + + npm run test:node -- --file=test/rest/status.test.js --grep=test_name_here + +Or run test skipping the build + + npm run test:node:skip-build -- --file=test/rest/status.test.js --grep=test_name_here + ### Debugging the mocha tests locally with a debugger Run the following command to launch tests with the debugger enabled. The tests will block until you attach a debugger. @@ -64,6 +78,12 @@ Run the following command to start a local Mocha test runner web server Open your browser to [http://localhost:3000](http://localhost:3000). If you are using a remote browser, refer to https://docs.saucelabs.com/reference/sauce-connect/ for instructions on setting up a local tunnel to your Mocha runner web server. +### Formatting/linting files + +Run the following command to fix linting/formatting issues + + npm run format + ### Testing environment variables for Node.js All tests are run against the sandbox environment by default. However, the following environment variables can be set before running the Karma server to change the environment the tests are run against. diff --git a/ably.d.ts b/ably.d.ts index a276fade46..c34628f611 100644 --- a/ably.d.ts +++ b/ably.d.ts @@ -636,7 +636,7 @@ declare namespace Types { queryTime?: boolean; /** - * An authenticated token. This can either be a {@link TokenDetails} object, a {@link TokenRequest} object, or token string (obtained from the `token` property of a {@link TokenDetails} component of an Ably {@link TokenRequest} response, or a JSON Web Token satisfying [the Ably requirements for JWTs](https://ably.com/docs/core-features/authentication#ably-jwt)). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Read more about [Token authentication](https://ably.com/docs/core-features/authentication#token-authentication). + * An authenticated token. This can either be a {@link TokenDetails} object or token string (obtained from the `token` property of a {@link TokenDetails} component of an Ably {@link TokenRequest} response, or a JSON Web Token satisfying [the Ably requirements for JWTs](https://ably.com/docs/core-features/authentication#ably-jwt)). This option is mostly useful for testing: since tokens are short-lived, in production you almost always want to use an authentication method that enables the client library to renew the token automatically when the previous one expires, such as `authUrl` or `authCallback`. Read more about [Token authentication](https://ably.com/docs/core-features/authentication#token-authentication). */ token?: TokenDetails | string; @@ -1124,6 +1124,10 @@ declare namespace Types { * Indicates whether message continuity on this channel is preserved, see [Nonfatal channel errors](https://ably.com/docs/realtime/channels#nonfatal-errors) for more info. */ resumed: boolean; + /** + * Indicates whether the client can expect a backlog of messages from a rewind or resume. + */ + hasBacklog?: boolean; } /** @@ -1396,6 +1400,154 @@ declare namespace Types { unit?: StatsIntervalGranularity; } + /** + * Contains information about the results of a batch operation. + */ + interface BatchResult { + /** + * The number of successful operations in the request. + */ + successCount: number; + /** + * The number of unsuccessful operations in the request. + */ + failureCount: number; + /** + * An array of results for the batch operation. + */ + results: T[]; + } + + /** + * Describes the messages that should be published by a batch publish operation, and the channels to which they should be published. + */ + interface BatchPublishSpec { + /** + * The names of the channels to publish the `messages` to. + */ + channels: string[]; + /** + * An array of {@link Message} objects. + */ + messages: Message[]; + } + + /** + * Contains information about the result of successful publishes to a channel requested by a single {@link Types.BatchPublishSpec}. + */ + interface BatchPublishSuccessResult { + /** + * The name of the channel the message(s) was published to. + */ + channel: string; + /** + * A unique ID prefixed to the {@link Message.id} of each published message. + */ + messageId: string; + } + + /** + * Contains information about the result of unsuccessful publishes to a channel requested by a single {@link Types.BatchPublishSpec}. + */ + interface BatchPublishFailureResult { + /** + * The name of the channel the message(s) failed to be published to. + */ + channel: string; + /** + * Describes the reason for which the message(s) failed to publish to the channel as an {@link ErrorInfo} object. + */ + error: ErrorInfo; + } + + /** + * Contains information about the result of a successful batch presence request for a single channel. + */ + interface BatchPresenceSuccessResult { + /** + * The channel name the presence state was retrieved for. + */ + channel: string; + /** + * An array of {@link PresenceMessage}s describing members present on the channel. + */ + presence: PresenceMessage[]; + } + + /** + * Contains information about the result of an unsuccessful batch presence request for a single channel. + */ + interface BatchPresenceFailureResult { + /** + * The channel name the presence state failed to be retrieved for. + */ + channel: string; + /** + * Describes the reason for which presence state could not be retrieved for the channel as an {@link ErrorInfo} object. + */ + error: ErrorInfo; + } + + /** + * The `TokenRevocationOptions` interface describes the additional options accepted by {@link Auth.revokeTokens}. + */ + interface TokenRevocationOptions { + /** + * A Unix timestamp in milliseconds where only tokens issued before this time are revoked. The default is the current time. Requests with an `issuedBefore` in the future, or more than an hour in the past, will be rejected. + */ + issuedBefore?: number; + /** + * If true, permits a token renewal cycle to take place without needing established connections to be dropped, by postponing enforcement to 30 seconds in the future, and sending any existing connections a hint to obtain (and upgrade the connection to use) a new token. The default is `false`, meaning that the effect is near-immediate. + */ + allowReauthMargin?: boolean; + } + + /** + * Describes which tokens should be affected by a token revocation request. + */ + interface TokenRevocationTargetSpecifier { + /** + * The type of token revocation target specifier. Valid values include `clientId`, `revocationKey` and `channel`. + */ + type: string; + /** + * The value of the token revocation target specifier. + */ + value: string; + } + + /** + * Contains information about the result of a successful token revocation request for a single target specifier. + */ + interface TokenRevocationSuccessResult { + /** + * The target specifier. + */ + target: string; + /** + * The time at which the token revocation will take effect, as a Unix timestamp in milliseconds. + */ + appliesAt: number; + /** + * A Unix timestamp in milliseconds. Only tokens issued earlier than this time will be revoked. + */ + issuedBefore: number; + } + + /** + * Contains information about the result of an unsuccessful token revocation request for a single target specifier. + */ + interface TokenRevocationFailureResult { + /** + * The target specifier. + */ + target: string; + /** + * Describes the reason for which token revocation failed for the given `target` as an {@link ErrorInfo} object. + */ + error: ErrorInfo; + } + // Common Listeners /** * A callback which returns only a single argument, used for {@link RealtimeChannel} subscriptions. @@ -1600,6 +1752,30 @@ declare namespace Types { * @returns A promise which, upon success, will be fulfilled with the time as milliseconds since the Unix epoch. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. */ time(): Promise; + + /** + * Publishes a {@link Types.BatchPublishSpec} object to one or more channels, up to a maximum of 100 channels. + * + * @param spec - A {@link Types.BatchPublishSpec} object. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch publish for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish(spec: BatchPublishSpec): Promise>; + /** + * Publishes one or more {@link Types.BatchPublishSpec} objects to one or more channels, up to a maximum of 100 channels. + * + * @param specs - An array of {@link Types.BatchPublishSpec} objects. + * @returns A promise which, upon success, will be fulfilled with an array of {@link Types.BatchResult} objects containing information about the result of the batch publish for each requested channel for each provided {@link Types.BatchPublishSpec}. This array is in the same order as the provided {@link Types.BatchPublishSpec} array. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish( + specs: BatchPublishSpec[] + ): Promise[]>; + /** + * Retrieves the presence state for one or more channels, up to a maximum of 100 channels. Presence state includes the `clientId` of members and their current {@link Types.PresenceAction}. + * + * @param channels - An array of one or more channel names, up to a maximum of 100 channels. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch presence request for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPresence(channels: string[]): Promise[]>; /** * A {@link Types.Push} object. */ @@ -1691,6 +1867,29 @@ declare namespace Types { * @returns A promise which, upon success, will be fulfilled with the time as milliseconds since the Unix epoch. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. */ time(): Promise; + /** + * Publishes a {@link Types.BatchPublishSpec} object to one or more channels, up to a maximum of 100 channels. + * + * @param spec - A {@link Types.BatchPublishSpec} object. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch publish for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish(spec: BatchPublishSpec): Promise>; + /** + * Publishes one or more {@link Types.BatchPublishSpec} objects to one or more channels, up to a maximum of 100 channels. + * + * @param specs - An array of {@link Types.BatchPublishSpec} objects. + * @returns A promise which, upon success, will be fulfilled with an array of {@link Types.BatchResult} objects containing information about the result of the batch publish for each requested channel for each provided {@link Types.BatchPublishSpec}. This array is in the same order as the provided {@link Types.BatchPublishSpec} array. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPublish( + specs: BatchPublishSpec[] + ): Promise[]>; + /** + * Retrieves the presence state for one or more channels, up to a maximum of 100 channels. Presence state includes the `clientId` of members and their current {@link Types.PresenceAction}. + * + * @param channels - An array of one or more channel names, up to a maximum of 100 channels. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} object containing information about the result of the batch presence request for each requested channel. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + batchPresence(channels: string[]): Promise[]>; /** * A {@link Types.Push} object. */ @@ -1730,6 +1929,17 @@ declare namespace Types { * @returns A promise which, upon success, will be fulfilled with a {@link TokenDetails} object. Upon failure, the promise will be rejected with an {@link ErrorInfo} object which explains the error. */ requestToken(TokenParams?: TokenParams, authOptions?: AuthOptions): Promise; + /** + * Revokes the tokens specified by the provided array of {@link TokenRevocationTargetSpecifier}s. Only tokens issued by an API key that had revocable tokens enabled before the token was issued can be revoked. See the [token revocation docs](https://ably.com/docs/core-features/authentication#token-revocation) for more information. + * + * @param specifiers - An array of {@link TokenRevocationTargetSpecifier} objects. + * @param options - A set of options which are used to modify the revocation request. + * @returns A promise which, upon success, will be fulfilled with a {@link Types.BatchResult} containing information about the result of the token revocation request for each provided [`TokenRevocationTargetSpecifier`]{@link TokenRevocationTargetSpecifier}. Upon failure, the promise will be rejected with an {@link Types.ErrorInfo} object which explains the error. + */ + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + options?: TokenRevocationOptions + ): Promise>; } /** @@ -2004,9 +2214,9 @@ declare namespace Types { /** * Attach to this channel ensuring the channel is created in the Ably system and all messages published on the channel are received by any channel listeners registered using {@link RealtimeChannel.subscribe | `subscribe()`}. Any resulting channel state change will be emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. As a convenience, `attach()` is called implicitly if {@link RealtimeChannel.subscribe | `subscribe()`} for the channel is called, or {@link RealtimePresence.enter | `enter()`} or {@link RealtimePresence.subscribe | `subscribe()`} are called on the {@link RealtimePresence} object for this channel. * - * @returns A promise which resolves upon success of the operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon success, if the channel became attached will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be fulfilled with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - attach(): Promise; + attach(): Promise; /** * Detach from this channel. Any resulting channel state change is emitted to any listeners registered using the {@link EventEmitter.on | `on()`} or {@link EventEmitter.once | `once()`} methods. Once all clients globally have detached from the channel, the channel will be released in the Ably service within two minutes. * @@ -2032,32 +2242,32 @@ declare namespace Types { * * @param event - The event name. * @param listener - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannel.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(event: string, listener?: messageCallback): Promise; + subscribe(event: string, listener?: messageCallback): Promise; /** * Registers a listener for messages on this channel for multiple event name values. * * @param events - An array of event names. * @param listener - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannel.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(events: Array, listener?: messageCallback): Promise; + subscribe(events: Array, listener?: messageCallback): Promise; /** * Registers a listener for messages on this channel that match the supplied filter. * * @param filter - A {@link MessageFilter}. * @param listener - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannel.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(filter: MessageFilter, listener?: messageCallback): Promise; + subscribe(filter: MessageFilter, listener?: messageCallback): Promise; /** * Registers a listener for messages on this channel. The caller supplies a listener function, which is called each time one or more messages arrives on the channel. * * @param callback - An event listener function. - * @returns A promise which resolves upon success of the channel {@link RealtimeChannel.attach | `attach()`} operation and rejects with an {@link ErrorInfo} object upon its failure. + * @returns A promise which, upon successful attachment to the channel, will be fulfilled with a {@link ChannelStateChange} object. If the channel was already attached the promise will be resolved with `null`. Upon failure, the promise will be rejected with an {@link ErrorInfo} object. */ - subscribe(callback: messageCallback): Promise; + subscribe(callback: messageCallback): Promise; /** * Publishes a single message to the channel with the given event name and payload. When publish is called with this client library, it won't attempt to implicitly attach to the channel, so long as [transient publishing](https://ably.com/docs/realtime/channels#transient-publish) is available in the library. Otherwise, the client will implicitly attach. * diff --git a/package-lock.json b/package-lock.json index 8a6f6b3d33..0882f0105f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ably", - "version": "1.2.40", + "version": "1.2.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "ably", - "version": "1.2.40", + "version": "1.2.42", "license": "Apache-2.0", "dependencies": { "@ably/msgpack-js": "^0.4.0", @@ -21,10 +21,10 @@ "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "async": "ably-forks/async#requirejs", - "aws-sdk": "^2.1075.0", + "aws-sdk": "^2.1413.0", "chai": "^4.2.0", "copy-webpack-plugin": "^11.0.0", - "cors": "~2.7", + "cors": "^2.8.5", "esbuild": "^0.18.10", "esbuild-plugin-umd-wrapper": "^1.0.7", "eslint": "^7.13.0", @@ -32,11 +32,9 @@ "eslint-plugin-security": "^1.4.0", "express": "^4.17.1", "glob": "~4.4", - "google-closure-compiler": "^20180610.0.1", - "grunt": "^1.4.1", + "grunt": "^1.6.1", "grunt-bump": "^0.3.1", "grunt-cli": "~1.2.0", - "grunt-closure-tools": "^1.0.0", "grunt-contrib-concat": "~0.5", "grunt-shell": "~1.1", "grunt-webpack": "^5.0.0", @@ -1427,9 +1425,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1286.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1286.0.tgz", - "integrity": "sha512-CvkCD1+NSk2MPOutD2hEPhXDET/79w/gd9a359QWb9Ja0Fd4vVFXPkhlm1DTGzuwqFKGinpCMxDP4md7QPsVvw==", + "version": "2.1414.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1414.0.tgz", + "integrity": "sha512-WhqTWiTZRUxWITvUG5VMPYGdCLNAm4zOTDIiotbErR9x+uDExk2CAGbXE8HH11+tD8PhZVXyukymSiG+7rJMMg==", "dev": true, "dependencies": { "buffer": "4.9.2", @@ -1441,7 +1439,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" }, "engines": { "node": ">= 10.0.0" @@ -1877,24 +1875,6 @@ "node": ">=6" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -1917,23 +1897,6 @@ "mimic-response": "^1.0.0" } }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2133,22 +2096,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, "node_modules/cors": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.7.2.tgz", - "integrity": "sha1-IThd6//yTCI6EGBbgjEUUpmaHLs=", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "dependencies": { + "object-assign": "^4", "vary": "^1" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.10" } }, "node_modules/cross-spawn": { @@ -2166,9 +2124,9 @@ } }, "node_modules/dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true, "engines": { "node": "*" @@ -3392,78 +3350,6 @@ "node": ">= 4" } }, - "node_modules/google-closure-compiler": { - "version": "20180610.0.2", - "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20180610.0.2.tgz", - "integrity": "sha1-Eggy9gzK2ZXo5HxedRIRLVTGro8=", - "dev": true, - "dependencies": { - "chalk": "^1.0.0", - "vinyl": "^2.0.1", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "bin": { - "google-closure-compiler": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/google-closure-compiler/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/google-closure-compiler/node_modules/ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/google-closure-compiler/node_modules/chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "dependencies": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/google-closure-compiler/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/google-closure-compiler/node_modules/supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3522,32 +3408,30 @@ } }, "node_modules/grunt": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", - "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", "dev": true, "dependencies": { - "dateformat": "~3.0.3", + "dateformat": "~4.6.2", "eventemitter2": "~0.4.13", "exit": "~0.1.2", - "findup-sync": "~0.3.0", + "findup-sync": "~5.0.0", "glob": "~7.1.6", "grunt-cli": "~1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", + "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" + "nopt": "~3.0.6" }, "bin": { "grunt": "bin/grunt" }, "engines": { - "node": ">=8" + "node": ">=16" } }, "node_modules/grunt-bump": { @@ -3592,19 +3476,6 @@ "node": ">=0.10.0" } }, - "node_modules/grunt-closure-tools": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-closure-tools/-/grunt-closure-tools-1.0.0.tgz", - "integrity": "sha1-+pdty8JrZSYgq1pYkYsZnxbpyZ0=", - "dev": true, - "dependencies": { - "grunt": "^1.0.1", - "task-closure-tools": "^0.1.10" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/grunt-contrib-concat": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-0.5.1.tgz", @@ -3853,6 +3724,45 @@ "webpack": "^4.0.0 || ^5.0.0" } }, + "node_modules/grunt/node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt/node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt/node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/grunt/node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -3914,31 +3824,50 @@ "node": ">=0.10.0" } }, - "node_modules/grunt/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "node_modules/grunt/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">=10" + "node": ">=0.10.0" } }, - "node_modules/grunt/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/grunt/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/grunt/node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "dependencies": { - "glob": "^7.1.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" }, - "bin": { - "rimraf": "bin.js" + "engines": { + "node": ">=8.6" + } + }, + "node_modules/grunt/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8.0" } }, "node_modules/gzip-size": { @@ -5233,6 +5162,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -5551,9 +5489,9 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "engines": { "node": ">=8.6" @@ -5678,12 +5616,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -5813,27 +5745,6 @@ "node": ">= 0.8" } }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -5871,21 +5782,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6096,7 +5992,7 @@ "node_modules/sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, "node_modules/schema-utils": { @@ -6391,15 +6287,6 @@ "node": ">=6" } }, - "node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-explorer": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/source-map-explorer/-/source-map-explorer-2.5.2.tgz", @@ -6638,21 +6525,6 @@ "node": ">= 0.8" } }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, "node_modules/string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -6769,15 +6641,6 @@ "node": ">=6" } }, - "node_modules/task-closure-tools": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/task-closure-tools/-/task-closure-tools-0.1.10.tgz", - "integrity": "sha1-2bHs+A7jfi2tIkRbJiAseN0VU3s=", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -7007,6 +6870,19 @@ "node": ">=10.13.0" } }, + "node_modules/tsconfig-paths-webpack-plugin/node_modules/enhanced-resolve": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", + "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tsconfig-paths/node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7268,32 +7144,6 @@ "node": ">= 0.8" } }, - "node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", - "dev": true, - "dependencies": { - "source-map": "^0.5.1" - } - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -7685,19 +7535,22 @@ } }, "node_modules/xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" } }, "node_modules/xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, "engines": { "node": ">=4.0" @@ -8785,9 +8638,9 @@ "dev": true }, "aws-sdk": { - "version": "2.1286.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1286.0.tgz", - "integrity": "sha512-CvkCD1+NSk2MPOutD2hEPhXDET/79w/gd9a359QWb9Ja0Fd4vVFXPkhlm1DTGzuwqFKGinpCMxDP4md7QPsVvw==", + "version": "2.1414.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1414.0.tgz", + "integrity": "sha512-WhqTWiTZRUxWITvUG5VMPYGdCLNAm4zOTDIiotbErR9x+uDExk2CAGbXE8HH11+tD8PhZVXyukymSiG+7rJMMg==", "dev": true, "requires": { "buffer": "4.9.2", @@ -8799,7 +8652,7 @@ "url": "0.10.3", "util": "^0.12.4", "uuid": "8.0.0", - "xml2js": "0.4.19" + "xml2js": "0.5.0" }, "dependencies": { "events": { @@ -9130,18 +8983,6 @@ } } }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", - "dev": true - }, "clone-deep": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", @@ -9161,23 +9002,6 @@ "mimic-response": "^1.0.0" } }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -9335,18 +9159,13 @@ } } }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, "cors": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.7.2.tgz", - "integrity": "sha1-IThd6//yTCI6EGBbgjEUUpmaHLs=", + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, "requires": { + "object-assign": "^4", "vary": "^1" } }, @@ -9362,9 +9181,9 @@ } }, "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true }, "debug": { @@ -10325,59 +10144,6 @@ } } }, - "google-closure-compiler": { - "version": "20180610.0.2", - "resolved": "https://registry.npmjs.org/google-closure-compiler/-/google-closure-compiler-20180610.0.2.tgz", - "integrity": "sha1-Eggy9gzK2ZXo5HxedRIRLVTGro8=", - "dev": true, - "requires": { - "chalk": "^1.0.0", - "vinyl": "^2.0.1", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } - } - }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -10424,28 +10190,56 @@ "dev": true }, "grunt": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.5.3.tgz", - "integrity": "sha512-mKwmo4X2d8/4c/BmcOETHek675uOqw0RuA/zy12jaspWqvTp4+ZeQF1W+OTpcbncnaBsfbQJ6l0l4j+Sn/GmaQ==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", "dev": true, "requires": { - "dateformat": "~3.0.3", + "dateformat": "~4.6.2", "eventemitter2": "~0.4.13", "exit": "~0.1.2", - "findup-sync": "~0.3.0", + "findup-sync": "~5.0.0", "glob": "~7.1.6", "grunt-cli": "~1.4.3", "grunt-known-options": "~2.0.0", "grunt-legacy-log": "~3.0.0", "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", + "iconv-lite": "~0.6.3", "js-yaml": "~3.14.0", "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" + "nopt": "~3.0.6" }, "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + } + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -10491,19 +10285,38 @@ "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", "dev": true }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, "requires": { - "glob": "^7.1.3" + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" } } } @@ -10537,16 +10350,6 @@ "resolve": "~1.1.0" } }, - "grunt-closure-tools": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-closure-tools/-/grunt-closure-tools-1.0.0.tgz", - "integrity": "sha1-+pdty8JrZSYgq1pYkYsZnxbpyZ0=", - "dev": true, - "requires": { - "grunt": "^1.0.1", - "task-closure-tools": "^0.1.10" - } - }, "grunt-contrib-concat": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-0.5.1.tgz", @@ -11667,6 +11470,12 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -11903,9 +11712,9 @@ "dev": true }, "picomatch": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", - "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pkg-dir": { @@ -11983,12 +11792,6 @@ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, "progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -12073,29 +11876,6 @@ "unpipe": "1.0.0" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, "rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -12123,18 +11903,6 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true - }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -12281,7 +12049,7 @@ "sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "dev": true }, "schema-utils": { @@ -12524,12 +12292,6 @@ "is-fullwidth-code-point": "^2.0.0" } }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - }, "source-map-explorer": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/source-map-explorer/-/source-map-explorer-2.5.2.tgz", @@ -12720,23 +12482,6 @@ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } - } - }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -12819,12 +12564,6 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true }, - "task-closure-tools": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/task-closure-tools/-/task-closure-tools-0.1.10.tgz", - "integrity": "sha1-2bHs+A7jfi2tIkRbJiAseN0VU3s=", - "dev": true - }, "temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -12992,6 +12731,18 @@ "chalk": "^4.1.0", "enhanced-resolve": "^5.7.0", "tsconfig-paths": "^4.1.2" + }, + "dependencies": { + "enhanced-resolve": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz", + "integrity": "sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + } } }, "tslib": { @@ -13166,29 +12917,6 @@ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", "dev": true }, - "vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - }, - "vinyl-sourcemaps-apply": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz", - "integrity": "sha1-q2VJ1h0XLCsbh75cUI0jnI74dwU=", - "dev": true, - "requires": { - "source-map": "^0.5.1" - } - }, "vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -13474,19 +13202,19 @@ } }, "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "requires": { "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" + "xmlbuilder": "~11.0.0" } }, "xmlbuilder": { - "version": "9.0.7", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true }, "y18n": { diff --git a/package.json b/package.json index 4c62961d7d..9cfe9025bd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ably", "description": "Realtime client library for Ably, the realtime messaging service", - "version": "1.2.40", + "version": "1.2.42", "license": "Apache-2.0", "bugs": { "url": "https://github.com/ably/ably-js/issues", @@ -33,10 +33,10 @@ "@typescript-eslint/eslint-plugin": "^5.14.0", "@typescript-eslint/parser": "^5.14.0", "async": "ably-forks/async#requirejs", - "aws-sdk": "^2.1075.0", + "aws-sdk": "^2.1413.0", "chai": "^4.2.0", "copy-webpack-plugin": "^11.0.0", - "cors": "~2.7", + "cors": "^2.8.5", "esbuild": "^0.18.10", "esbuild-plugin-umd-wrapper": "^1.0.7", "eslint": "^7.13.0", @@ -44,7 +44,7 @@ "eslint-plugin-security": "^1.4.0", "express": "^4.17.1", "glob": "~4.4", - "grunt": "^1.4.1", + "grunt": "^1.6.1", "grunt-bump": "^0.3.1", "grunt-cli": "~1.2.0", "grunt-contrib-concat": "~0.5", @@ -82,6 +82,7 @@ "grunt": "grunt", "test": "grunt test", "test:node": "grunt test:node", + "test:node:skip-build": "grunt mocha", "test:webserver": "grunt test:webserver", "test:playwright": "node test/support/runPlaywrightTests.js", "concat": "grunt concat", diff --git a/src/common/lib/client/auth.ts b/src/common/lib/client/auth.ts index 794ea837e1..3425547636 100644 --- a/src/common/lib/client/auth.ts +++ b/src/common/lib/client/auth.ts @@ -9,7 +9,16 @@ import Rest from './rest'; import Realtime from './realtime'; import ClientOptions from '../../types/ClientOptions'; import HttpMethods from '../../constants/HttpMethods'; +import HttpStatusCodes from 'common/constants/HttpStatusCodes'; import Platform from '../../platform'; +import Resource from './resource'; + +type BatchResult = API.Types.BatchResult; +type TokenRevocationTargetSpecifier = API.Types.TokenRevocationTargetSpecifier; +type TokenRevocationOptions = API.Types.TokenRevocationOptions; +type TokenRevocationSuccessResult = API.Types.TokenRevocationSuccessResult; +type TokenRevocationFailureResult = API.Types.TokenRevocationFailureResult; +type TokenRevocationResult = BatchResult; const MAX_TOKEN_LENGTH = Math.pow(2, 17); function noop() {} @@ -273,10 +282,10 @@ class Auth { _authOptions, (err: ErrorInfo, tokenDetails: API.Types.TokenDetails) => { if (err) { - if ((this.client as Realtime).connection) { - /* We interpret RSA4d as including requests made by a client lib to - * authenticate triggered by an explicit authorize() or an AUTH received from - * ably, not just connect-sequence-triggered token fetches */ + if ((this.client as Realtime).connection && err.statusCode === HttpStatusCodes.Forbidden) { + /* Per RSA4d & RSA4d1, if the auth server explicitly repudiates our right to + * stay connecticed by returning a 403, we actively disconnect the connection + * even though we may well still have time left in the old token. */ (this.client as Realtime).connection.connectionManager.actOnErrorFromAuthorize(err); } callback?.(err); @@ -1027,6 +1036,67 @@ class Auth { static isTokenErr(error: IPartialErrorInfo) { return error.code && error.code >= 40140 && error.code < 40150; } + + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + options?: TokenRevocationOptions + ): Promise; + revokeTokens( + specifiers: TokenRevocationTargetSpecifier[], + optionsOrCallbackArg?: TokenRevocationOptions | StandardCallback, + callbackArg?: StandardCallback + ): void | Promise { + if (useTokenAuth(this.client.options)) { + throw new ErrorInfo('Cannot revoke tokens when using token auth', 40162, 401); + } + + const keyName = this.client.options.keyName!; + + let resolvedOptions: TokenRevocationOptions; + + if (typeof optionsOrCallbackArg === 'function') { + callbackArg = optionsOrCallbackArg; + resolvedOptions = {}; + } else { + resolvedOptions = optionsOrCallbackArg ?? {}; + } + + if (callbackArg === undefined) { + return Utils.promisify(this, 'revokeTokens', [specifiers, resolvedOptions]); + } + + const callback = callbackArg; + + const requestBodyDTO = { + targets: specifiers.map((specifier) => `${specifier.type}:${specifier.value}`), + ...resolvedOptions, + }; + + const format = this.client.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Utils.defaultPostHeaders(this.client.options, { format }); + + if (this.client.options.headers) Utils.mixin(headers, this.client.options.headers); + + const requestBody = Utils.encodeBody(requestBodyDTO, format); + Resource.post( + this.client, + `/keys/${keyName}/revokeTokens`, + requestBody, + headers, + { newBatchResponse: 'true' }, + null, + (err, body, headers, unpacked) => { + if (err) { + callback(err); + return; + } + + const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as TokenRevocationResult; + + callback(null, batchResult); + } + ); + } } export default Auth; diff --git a/src/common/lib/client/channelstatechange.ts b/src/common/lib/client/channelstatechange.ts index b778210032..09cf0a2c6f 100644 --- a/src/common/lib/client/channelstatechange.ts +++ b/src/common/lib/client/channelstatechange.ts @@ -5,11 +5,21 @@ class ChannelStateChange { current: string; resumed?: boolean; reason?: string | Error | ErrorInfo; + hasBacklog?: boolean; - constructor(previous: string, current: string, resumed?: boolean, reason?: string | Error | ErrorInfo | null) { + constructor( + previous: string, + current: string, + resumed?: boolean, + hasBacklog?: boolean, + reason?: string | Error | ErrorInfo | null + ) { this.previous = previous; this.current = current; - if (current === 'attached') this.resumed = resumed; + if (current === 'attached') { + this.resumed = resumed; + this.hasBacklog = hasBacklog; + } if (reason) this.reason = reason; } } diff --git a/src/common/lib/client/paginatedresource.ts b/src/common/lib/client/paginatedresource.ts index 416c59fae6..0deff1dace 100644 --- a/src/common/lib/client/paginatedresource.ts +++ b/src/common/lib/client/paginatedresource.ts @@ -5,7 +5,7 @@ import ErrorInfo, { IPartialErrorInfo } from '../types/errorinfo'; import { PaginatedResultCallback } from '../../types/utils'; import Rest from './rest'; -export type BodyHandler = (body: unknown, headers: Record, packed?: boolean) => Promise; +export type BodyHandler = (body: unknown, headers: Record, unpacked?: boolean) => Promise; function getRelParams(linkUrl: string) { const urlMatch = linkUrl.match(/^\.\/(\w+)\?(.*)$/); diff --git a/src/common/lib/client/realtimechannel.ts b/src/common/lib/client/realtimechannel.ts index 0c55e901da..2d3b568a5a 100644 --- a/src/common/lib/client/realtimechannel.ts +++ b/src/common/lib/client/realtimechannel.ts @@ -12,7 +12,7 @@ import ConnectionErrors from '../transport/connectionerrors'; import * as API from '../../../../ably'; import ConnectionManager from '../transport/connectionmanager'; import ConnectionStateChange from './connectionstatechange'; -import { ErrCallback, PaginatedResultCallback } from '../../types/utils'; +import { ErrCallback, PaginatedResultCallback, StandardCallback } from '../../types/utils'; import Realtime from './realtime'; interface RealtimeHistoryParams { @@ -266,19 +266,23 @@ class RealtimeChannel extends Channel { } } - attach(callback?: ErrCallback): void | Promise { + attach(callback?: StandardCallback): void | Promise { if (!callback) { return Utils.promisify(this, 'attach', arguments); } if (this.state === 'attached') { - callback(); + callback(null, null); return; } this._attach(false, null, callback); } - _attach(forceReattach: boolean, attachReason: ErrorInfo | null, callback?: ErrCallback): void { + _attach( + forceReattach: boolean, + attachReason: ErrorInfo | null, + callback?: StandardCallback + ): void { if (!callback) { callback = function (err?: ErrorInfo | null) { if (err) { @@ -300,7 +304,7 @@ class RealtimeChannel extends Channel { this.once(function (this: { event: string }, stateChange: ChannelStateChange) { switch (this.event) { case 'attached': - callback?.(); + callback?.(null, stateChange); break; case 'detached': case 'suspended': @@ -394,7 +398,7 @@ class RealtimeChannel extends Channel { this.sendMessage(msg, callback || noop); } - subscribe(...args: unknown[] /* [event], listener, [callback] */): void | Promise { + subscribe(...args: unknown[] /* [event], listener, [callback] */): void | Promise { const [event, listener, callback] = RealtimeChannel.processListenerArgs(args); if (!callback) { @@ -588,12 +592,13 @@ class RealtimeChannel extends Channel { this.modes = (modesFromFlags && Utils.allToLowerCase(modesFromFlags)) || undefined; const resumed = message.hasFlag('RESUMED'); const hasPresence = message.hasFlag('HAS_PRESENCE'); + const hasBacklog = message.hasFlag('HAS_BACKLOG'); if (this.state === 'attached') { if (!resumed) { /* On a loss of continuity, the presence set needs to be re-synced */ this.presence.onAttached(hasPresence); } - const change = new ChannelStateChange(this.state, this.state, resumed, message.error); + const change = new ChannelStateChange(this.state, this.state, resumed, hasBacklog, message.error); this._allChannelChanges.emit('update', change); if (!resumed || this.channelOptions.updateOnAttached) { this.emit('update', change); @@ -602,7 +607,7 @@ class RealtimeChannel extends Channel { /* RTL5i: re-send DETACH and remain in the 'detaching' state */ this.checkPendingState(); } else { - this.notifyState('attached', message.error, resumed, hasPresence); + this.notifyState('attached', message.error, resumed, hasPresence, hasBacklog); } break; } @@ -767,7 +772,8 @@ class RealtimeChannel extends Channel { state: API.Types.ChannelState, reason?: ErrorInfo | null, resumed?: boolean, - hasPresence?: boolean + hasPresence?: boolean, + hasBacklog?: boolean ): void { Logger.logAction( Logger.LOG_MICRO, @@ -793,7 +799,7 @@ class RealtimeChannel extends Channel { if (reason) { this.errorReason = reason; } - const change = new ChannelStateChange(this.state, state, resumed, reason); + const change = new ChannelStateChange(this.state, state, resumed, hasBacklog, reason); const logLevel = state === 'failed' ? Logger.LOG_ERROR : Logger.LOG_MAJOR; Logger.logAction( logLevel, @@ -906,10 +912,7 @@ class RealtimeChannel extends Channel { if (this.retryTimer) return; this.retryCount++; - const retryDelay = - this.realtime.options.timeouts.channelRetryTimeout * - Utils.getJitterCoefficient() * - Utils.getBackoffCoefficient(this.retryCount); + const retryDelay = Utils.getRetryTime(this.realtime.options.timeouts.channelRetryTimeout, this.retryCount); this.retryTimer = setTimeout(() => { /* If connection is not connected, just leave in suspended, a reattach diff --git a/src/common/lib/client/realtimepresence.ts b/src/common/lib/client/realtimepresence.ts index 1d2f60be06..e81f257e26 100644 --- a/src/common/lib/client/realtimepresence.ts +++ b/src/common/lib/client/realtimepresence.ts @@ -463,7 +463,7 @@ class RealtimePresence extends Presence { const msg = 'Presence auto-re-enter failed: ' + err.toString(); const wrappedErr = new ErrorInfo(msg, 91004, 400); Logger.logAction(Logger.LOG_ERROR, 'RealtimePresence._ensureMyMembersPresent()', msg); - const change = new ChannelStateChange(this.channel.state, this.channel.state, true, wrappedErr); + const change = new ChannelStateChange(this.channel.state, this.channel.state, true, false, wrappedErr); this.channel.emit('update', change); } }; diff --git a/src/common/lib/client/rest.ts b/src/common/lib/client/rest.ts index d6e57416e2..f70d9ef8fa 100644 --- a/src/common/lib/client/rest.ts +++ b/src/common/lib/client/rest.ts @@ -12,10 +12,21 @@ import { ChannelOptions } from '../../types/channel'; import { PaginatedResultCallback, StandardCallback } from '../../types/utils'; import { ErrnoException, IHttp, RequestParams } from '../../types/http'; import ClientOptions, { NormalisedClientOptions } from '../../types/ClientOptions'; +import * as API from '../../../../ably'; import Platform from '../../platform'; import Message from '../types/message'; import PresenceMessage from '../types/presencemessage'; +import Resource from './resource'; + +type BatchResult = API.Types.BatchResult; +type BatchPublishSpec = API.Types.BatchPublishSpec; +type BatchPublishSuccessResult = API.Types.BatchPublishSuccessResult; +type BatchPublishFailureResult = API.Types.BatchPublishFailureResult; +type BatchPublishResult = BatchResult; +type BatchPresenceSuccessResult = API.Types.BatchPresenceSuccessResult; +type BatchPresenceFailureResult = API.Types.BatchPresenceFailureResult; +type BatchPresenceResult = BatchResult; const noop = function () {}; class Rest { @@ -219,6 +230,93 @@ class Rest { } } + batchPublish( + specOrSpecs: T + ): Promise; + batchPublish( + specOrSpecs: T, + callback?: StandardCallback + ): void | Promise { + if (callback === undefined) { + return Utils.promisify(this, 'batchPublish', [specOrSpecs]); + } + + let requestBodyDTO: BatchPublishSpec[]; + let singleSpecMode: boolean; + if (Utils.isArray(specOrSpecs)) { + requestBodyDTO = specOrSpecs; + singleSpecMode = false; + } else { + requestBodyDTO = [specOrSpecs]; + singleSpecMode = true; + } + + const format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Utils.defaultPostHeaders(this.options, { format }); + + if (this.options.headers) Utils.mixin(headers, this.options.headers); + + const requestBody = Utils.encodeBody(requestBodyDTO, format); + Resource.post( + this, + '/messages', + requestBody, + headers, + { newBatchResponse: 'true' }, + null, + (err, body, headers, unpacked) => { + if (err) { + callback(err); + return; + } + + const batchResults = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPublishResult[]; + + // I don't love the below type assertions for `callback` but not sure how to avoid them + if (singleSpecMode) { + (callback as StandardCallback)(null, batchResults[0]); + } else { + (callback as StandardCallback)(null, batchResults); + } + } + ); + } + + batchPresence(channels: string[]): Promise; + batchPresence( + channels: string[], + callback?: StandardCallback + ): void | Promise { + if (callback === undefined) { + return Utils.promisify(this, 'batchPresence', [channels]); + } + + const format = this.options.useBinaryProtocol ? Utils.Format.msgpack : Utils.Format.json, + headers = Utils.defaultPostHeaders(this.options, { format }); + + if (this.options.headers) Utils.mixin(headers, this.options.headers); + + const channelsParam = channels.join(','); + + Resource.get( + this, + '/presence', + headers, + { newBatchResponse: 'true', channels: channelsParam }, + null, + (err, body, headers, unpacked) => { + if (err) { + callback(err); + return; + } + + const batchResult = (unpacked ? body : Utils.decodeBody(body, format)) as BatchPresenceResult; + + callback(null, batchResult); + } + ); + } + setLog(logOptions: LoggerOptions): void { Logger.setLog(logOptions.level, logOptions.handler); } diff --git a/src/common/lib/transport/connectionmanager.ts b/src/common/lib/transport/connectionmanager.ts index 92c87d1a0c..3cfd7cae38 100644 --- a/src/common/lib/transport/connectionmanager.ts +++ b/src/common/lib/transport/connectionmanager.ts @@ -1326,10 +1326,7 @@ class ConnectionManager extends EventEmitter { let retryDelay = newState.retryDelay; if (newState.state === 'disconnected') { this.disconnectedRetryCount++; - retryDelay = - (newState.retryDelay as number) * - Utils.getBackoffCoefficient(this.disconnectedRetryCount) * - Utils.getJitterCoefficient(); + retryDelay = Utils.getRetryTime(newState.retryDelay as number, this.disconnectedRetryCount); } const change = new ConnectionStateChange( @@ -1533,19 +1530,19 @@ class ConnectionManager extends EventEmitter { } else if (state == this.states.connected.state) { this.upgradeIfNeeded(transportParams); } else if (this.transports.length > 1 && this.getTransportPreference()) { - this.connectPreference(transportParams); + this.connectPreference(transportParams, connectCount); } else { this.connectBase(transportParams, connectCount); } } - connectPreference(transportParams: TransportParams): void { + connectPreference(transportParams: TransportParams, connectCount?: number): void { const preference = this.getTransportPreference(); let preferenceTimeoutExpired = false; if (!Utils.arrIn(this.transports, preference)) { this.unpersistTransportPreference(); - this.connectImpl(transportParams); + this.connectImpl(transportParams, connectCount); } Logger.logAction( @@ -1568,7 +1565,7 @@ class ConnectionManager extends EventEmitter { /* Be quite agressive about clearing the stored preference if ever it doesn't work */ this.unpersistTransportPreference(); } - this.connectImpl(transportParams); + this.connectImpl(transportParams, connectCount); }, this.options.timeouts.preferenceConnectTimeout); /* For connectPreference, just use the main host. If host fallback is needed, do it in connectBase. @@ -1586,7 +1583,7 @@ class ConnectionManager extends EventEmitter { } else if (!transport && !fatal) { /* Preference failed in a transport-specific way. Try more */ this.unpersistTransportPreference(); - this.connectImpl(transportParams); + this.connectImpl(transportParams, connectCount); } /* If suceeded, or failed fatally, nothing to do */ }); @@ -2116,9 +2113,10 @@ class ConnectionManager extends EventEmitter { } } - /* This method is only used during connection attempts, so implements RSA4c1, - * RSA4c2, and RSA4d. In particular, it is not invoked for - * serverside-triggered reauths or manual reauths, so RSA4c3 does not apply */ + /* This method is only used during connection attempts, so implements RSA4c1, RSA4c2, + * and RSA4d. It is generally not invoked for serverside-triggered reauths or manual + * reauths, so RSA4c3 does not apply, except (per per RSA4d1) in the case that the auth + * server returns 403. */ actOnErrorFromAuthorize(err: ErrorInfo): void { if (err.code === 40171) { /* No way to reauth */ diff --git a/src/common/lib/util/utils.ts b/src/common/lib/util/utils.ts index 303d961d13..0ba3cc27d7 100644 --- a/src/common/lib/util/utils.ts +++ b/src/common/lib/util/utils.ts @@ -424,8 +424,8 @@ export function inspectError(err: unknown): string { (err as ErrorInfo)?.constructor?.name === 'ErrorInfo' || (err as PartialErrorInfo)?.constructor?.name === 'PartialErrorInfo' ) - return Platform.Config.inspect(err); - return (err as Error).toString(); + return (err as Error).toString(); + return Platform.Config.inspect(err); } export function inspectBody(body: unknown): string { @@ -522,14 +522,28 @@ export function allToUpperCase(arr: Array): Array { }); } -export function getBackoffCoefficient(n: number) { - return Math.min((n + 2) / 3, 2); +export function getBackoffCoefficient(count: number) { + return Math.min((count + 2) / 3, 2); } export function getJitterCoefficient() { return 1 - Math.random() * 0.2; } +/** + * + * @param initialTimeout initial timeout value + * @param retryAttempt integer indicating retryAttempt + * @returns RetryTimeout value for given timeout and retryAttempt. + * If x is the value generated then, + * Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout, + * Lower bound = 0.8 * Upper bound, + * Lower bound < x < Upper bound + */ +export function getRetryTime(initialTimeout: number, retryAttempt: number) { + return initialTimeout * getBackoffCoefficient(retryAttempt) * getJitterCoefficient(); +} + export function getGlobalObject() { if (typeof global !== 'undefined') { return global; diff --git a/src/common/types/ClientOptions.ts b/src/common/types/ClientOptions.ts index 1eeac34905..98c96f046d 100644 --- a/src/common/types/ClientOptions.ts +++ b/src/common/types/ClientOptions.ts @@ -9,7 +9,7 @@ export type RestAgentOptions = { export default interface ClientOptions extends API.Types.ClientOptions { restAgentOptions?: RestAgentOptions; pushFullWait?: boolean; - agents?: string[]; + agents?: Record; } /** diff --git a/src/common/types/http.d.ts b/src/common/types/http.d.ts index bd3b33fa59..c3f454d475 100644 --- a/src/common/types/http.d.ts +++ b/src/common/types/http.d.ts @@ -8,7 +8,7 @@ export type RequestCallback = ( error?: ErrnoException | IPartialErrorInfo | null, body?: unknown, headers?: IncomingHttpHeaders, - packed?: boolean, + unpacked?: boolean, statusCode?: number ) => void; export type RequestParams = Record | null; diff --git a/src/platform/nodejs/lib/util/http.ts b/src/platform/nodejs/lib/util/http.ts index 0b2c3307d2..a6e757f37e 100644 --- a/src/platform/nodejs/lib/util/http.ts +++ b/src/platform/nodejs/lib/util/http.ts @@ -261,7 +261,7 @@ const Http: typeof IHttp = class { err?: ErrnoException | ErrorInfo | null, responseText?: unknown, headers?: any, - packed?: boolean, + unpacked?: boolean, statusCode?: number ) { if (!err && !connectivityUrlIsDefault) { diff --git a/src/platform/web/lib/transport/fetchrequest.ts b/src/platform/web/lib/transport/fetchrequest.ts index 360b3f5a5b..9af6592628 100644 --- a/src/platform/web/lib/transport/fetchrequest.ts +++ b/src/platform/web/lib/transport/fetchrequest.ts @@ -63,7 +63,7 @@ export default function fetchRequest( prom = res.text(); } prom.then((body) => { - const packed = !!contentType && contentType.indexOf('application/x-msgpack') === -1; + const unpacked = !!contentType && contentType.indexOf('application/x-msgpack') === -1; if (!res.ok) { const err = getAblyError(body, res.headers) || @@ -72,9 +72,9 @@ export default function fetchRequest( null, res.status ); - callback(err, body, res.headers, packed, res.status); + callback(err, body, res.headers, unpacked, res.status); } else { - callback(null, body, res.headers, packed, res.status); + callback(null, body, res.headers, unpacked, res.status); } }); }) diff --git a/src/platform/web/lib/util/http.ts b/src/platform/web/lib/util/http.ts index ab74454b07..cad0a06185 100644 --- a/src/platform/web/lib/util/http.ts +++ b/src/platform/web/lib/util/http.ts @@ -99,7 +99,7 @@ const Http: typeof IHttp = class { err?: ErrorInfo | ErrnoException | null, responseText?: unknown, headers?: any, - packed?: boolean, + unpacked?: boolean, statusCode?: number ) { let result = false; diff --git a/test/common/modules/shared_helper.js b/test/common/modules/shared_helper.js index 0790322075..fdf2a82587 100644 --- a/test/common/modules/shared_helper.js +++ b/test/common/modules/shared_helper.js @@ -218,6 +218,10 @@ define([ return res; }; + function randomString() { + return Math.random().toString().slice(2); + } + return (module.exports = { setupApp: testAppModule.setup, tearDownApp: testAppModule.tearDown, @@ -249,5 +253,6 @@ define([ arrFind: arrFind, arrFilter: arrFilter, whenPromiseSettles: whenPromiseSettles, + randomString: randomString, }); }); diff --git a/test/realtime/channel.test.js b/test/realtime/channel.test.js index aee99ab380..677be65e22 100644 --- a/test/realtime/channel.test.js +++ b/test/realtime/channel.test.js @@ -10,11 +10,7 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async var createPM = Ably.Realtime.ProtocolMessage.fromDeserialized; var testOnAllTransports = helper.testOnAllTransports; var whenPromiseSettles = helper.whenPromiseSettles; - - /* Helpers */ - function randomString() { - return Math.random().toString().slice(2); - } + var randomString = helper.randomString; function checkCanSubscribe(channel, testChannel) { return function (callback) { @@ -1530,5 +1526,120 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.subscribe(subscriber); }); + + it('attach_returns_state_change', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'attach_returns_state_chnage'; + var channel = realtime.channels.get(channelName); + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.current).to.equal('attached'); + expect(stateChange.previous).to.equal('attaching'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + + // for an already-attached channel, null is returned + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange).to.equal(null); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + }); + + it('subscribe_returns_state_change', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'subscribe_returns_state_chnage'; + var channel = realtime.channels.get(channelName); + channel.subscribe( + function () {}, // message listener + // attach callback + function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.current).to.equal('attached'); + expect(stateChange.previous).to.equal('attaching'); + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + } + ); + }); + + it('rewind_has_backlog_0', function (done) { + var realtime = helper.AblyRealtime(); + var channelName = 'rewind_has_backlog_0'; + var channelOpts = { params: { rewind: '1' } }; + var channel = realtime.channels.get(channelName, channelOpts); + + // attach with rewind but no channel history - hasBacklog should be false + channel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(!stateChange.hasBacklog).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + + it('rewind_has_backlog_1', function (done) { + var realtime = helper.AblyRealtime(); + var rest = helper.AblyRest(); + var channelName = 'rewind_has_backlog_1'; + var channelOpts = { params: { rewind: '1' } }; + var rtChannel = realtime.channels.get(channelName, channelOpts); + var restChannel = rest.channels.get(channelName); + + // attach with rewind after publishing - hasBacklog should be true + restChannel.publish('foo', 'bar', function (err) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + rtChannel.attach(function (err, stateChange) { + if (err) { + closeAndFinish(done, realtime, err); + return; + } + + try { + expect(stateChange.hasBacklog).to.be.ok; + } catch (err) { + closeAndFinish(done, realtime, err); + return; + } + closeAndFinish(done, realtime); + }); + }); + }); }); }); diff --git a/test/realtime/failure.test.js b/test/realtime/failure.test.js index 6e2339434c..61256cdce3 100644 --- a/test/realtime/failure.test.js +++ b/test/realtime/failure.test.js @@ -186,6 +186,11 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async } }); + function checkIsBetween(value, min, max) { + expect(value).to.be.above(min); + expect(value).to.be.below(max); + } + utils.arrForEach(availableTransports, function (transport) { it('disconnected_backoff_' + transport, function (done) { var disconnectedRetryTimeout = 150; @@ -197,16 +202,25 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async }); var retryCount = 0; + var retryTimeouts = []; realtime.connection.on(function (stateChange) { if (stateChange.previous === 'connecting' && stateChange.current === 'disconnected') { if (retryCount > 4) { + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + checkIsBetween(retryTimeouts[0], 120, 150); + checkIsBetween(retryTimeouts[1], 160, 200); + checkIsBetween(retryTimeouts[2], 200, 250); + + for (var i = 3; i < retryTimeouts.length; i++) { + checkIsBetween(retryTimeouts[i], 240, 300); + } closeAndFinish(done, realtime); return; } try { - expect(stateChange.retryIn).to.be.below(disconnectedRetryTimeout + Math.min(retryCount, 3) * 50); - expect(stateChange.retryIn).to.be.above(0.8 * (disconnectedRetryTimeout + Math.min(retryCount, 3) * 50)); + retryTimeouts.push(stateChange.retryIn); retryCount += 1; } catch (err) { closeAndFinish(done, realtime, err); @@ -380,6 +394,8 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async await originalProcessMessage(message); }; + var retryTimeouts = []; + realtime.connection.on('connected', function () { realtime.options.timeouts.realtimeRequestTimeout = 1; whenPromiseSettles(channel.attach(), function (err) { @@ -388,19 +404,23 @@ define(['ably', 'shared_helper', 'async', 'chai'], function (Ably, helper, async channel.on(function (stateChange) { if (stateChange.current === 'suspended') { if (retryCount > 4) { + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + // Additional 10 is a calculationDelayTimeout + checkIsBetween(retryTimeouts[0], 120, 150 + 10); + checkIsBetween(retryTimeouts[1], 160, 200 + 10); + checkIsBetween(retryTimeouts[2], 200, 250 + 10); + + for (var i = 3; i < retryTimeouts.length; i++) { + checkIsBetween(retryTimeouts[i], 240, 300 + 10); + } closeAndFinish(done, realtime); return; } var elapsedSinceSuspneded = performance.now() - lastSuspended; lastSuspended = performance.now(); try { - // JS timers don't work precisely and realtimeRequestTimeout can't be 0 so add 5ms to the max timeout length each retry - expect(elapsedSinceSuspneded).to.be.below( - channelRetryTimeout + Math.min(retryCount, 3) * 50 + 5 * (retryCount + 1) - ); - expect(elapsedSinceSuspneded).to.be.above( - 0.8 * (channelRetryTimeout + Math.min(retryCount, 3) * 50) - ); + retryTimeouts.push(elapsedSinceSuspneded); retryCount += 1; } catch (err) { closeAndFinish(done, realtime, err); diff --git a/test/realtime/utils.test.js b/test/realtime/utils.test.js new file mode 100644 index 0000000000..244e4a3a4a --- /dev/null +++ b/test/realtime/utils.test.js @@ -0,0 +1,43 @@ +'use strict'; + +define(['shared_helper', 'chai'], function (helper, chai) { + var utils = helper.Utils; + var expect = chai.expect; + + // RTB1 + describe('incremental backoff and jitter', function () { + it('should calculate retry timeouts using incremental backoff and jitter', function () { + var retryAttempts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + var initialTimeout = 15; + + var retryTimeouts = retryAttempts.map((attempt) => utils.getRetryTime(initialTimeout, attempt)); + expect(retryTimeouts.filter((timeout) => timeout >= 30).length).to.equal(0); + + function checkIsBetween(value, min, max) { + expect(value).to.be.above(min); + expect(value).to.be.below(max); + } + + // Upper bound = min((retryAttempt + 2) / 3, 2) * initialTimeout + // Lower bound = 0.8 * Upper bound + checkIsBetween(retryTimeouts[0], 12, 15); + checkIsBetween(retryTimeouts[1], 16, 20); + checkIsBetween(retryTimeouts[2], 20, 25); + + for (var i = 3; i < retryTimeouts.length; i++) { + checkIsBetween(retryTimeouts[i], 24, 30); + } + + function calculateBounds(retryAttempt, initialTimeout) { + var upperBound = Math.min((retryAttempt + 2) / 3, 2) * initialTimeout; + var lowerBound = 0.8 * upperBound; + return { lower: lowerBound, upper: upperBound }; + } + + for (var i = 0; i < retryTimeouts.length; i++) { + var bounds = calculateBounds(retryAttempts[i], initialTimeout); + checkIsBetween(retryTimeouts[i], bounds.lower, bounds.upper); + } + }); + }); +}); diff --git a/test/rest/batch.test.js b/test/rest/batch.test.js new file mode 100644 index 0000000000..9c250c54f4 --- /dev/null +++ b/test/rest/batch.test.js @@ -0,0 +1,340 @@ +'use strict'; + +define(['ably', 'shared_helper', 'chai'], function (Ably, helper, chai) { + var expect = chai.expect; + var closeAndFinish = helper.closeAndFinish; + var randomString = helper.randomString; + + describe('rest/batchPublish', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); + + describe('when invoked with an array of specs', function () { + it('performs a batch publish and returns an array of results', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[2].keyStr /* we use this key so that some publishes fail due to capabilities */, + }); + + const specs = [ + { + channels: [ + 'channel0' /* key allows publishing to this channel */, + 'channel3' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message1' }, { data: 'message2' }], + }, + { + channels: [ + 'channel4' /* key allows publishing to this channel */, + 'channel5' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message3' }, { data: 'message4' }], + }, + ]; + + // First, we perform the batch publish request... + const batchResults = await rest.batchPublish(specs); + + expect(batchResults).to.have.lengthOf(specs.length); + + expect(batchResults[0].successCount).to.equal(1); + expect(batchResults[0].failureCount).to.equal(1); + + // Check the results of first BatchPublishSpec + + expect(batchResults[0].results).to.have.lengthOf(2); + + expect(batchResults[0].results[0].channel).to.equal('channel0'); + expect(batchResults[0].results[0].messageId).to.include(':0'); + expect('error' in batchResults[0].results[0]).to.be.false; + + expect(batchResults[0].results[1].channel).to.equal('channel3'); + expect('messageId' in batchResults[0].results[1]).to.be.false; + expect(batchResults[0].results[1].error.statusCode).to.equal(401); + + // Check the results of second BatchPublishSpec + + expect(batchResults[1].results).to.have.lengthOf(2); + + expect(batchResults[1].results[0].channel).to.equal('channel4'); + expect(batchResults[1].results[0].messageId).to.include(':0'); + expect('error' in batchResults[1].results[0]).to.be.false; + + expect(batchResults[1].results[1].channel).to.equal('channel5'); + expect('messageId' in batchResults[1].results[1]).to.be.false; + expect(batchResults[1].results[1].error.statusCode).to.equal(401); + + // ...and now we use channel history to check that the expected messages have been published. + const verificationRest = helper.AblyRest({ promises: true }); + + const channel0 = verificationRest.channels.get('channel0'); + const channel0HistoryPromise = channel0.history({ limit: 2 }); + + const channel4 = verificationRest.channels.get('channel4'); + const channel4HistoryPromise = channel4.history({ limit: 2 }); + + const [channel0History, channel4History] = await Promise.all([channel0HistoryPromise, channel4HistoryPromise]); + + const channel0HistoryData = new Set([channel0History.items[0].data, channel0History.items[1].data]); + expect(channel0HistoryData).to.deep.equal(new Set(['message1', 'message2'])); + + const channel4HistoryData = new Set([channel4History.items[0].data, channel4History.items[1].data]); + expect(channel4HistoryData).to.deep.equal(new Set(['message3', 'message4'])); + }); + }); + + describe('when invoked with a single spec', function () { + it('performs a batch publish and returns a single result', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[2].keyStr /* we use this key so that some publishes fail due to capabilities */, + }); + + const spec = { + channels: [ + 'channel0' /* key allows publishing to this channel */, + 'channel3' /* key does not allow publishing to this channel */, + ], + messages: [{ data: 'message1' }, { data: 'message2' }], + }; + + // First, we perform the batch publish request... + const batchResult = await rest.batchPublish(spec); + + expect(batchResult.successCount).to.equal(1); + expect(batchResult.failureCount).to.equal(1); + + expect(batchResult.results).to.have.lengthOf(2); + + expect(batchResult.results[0].channel).to.equal('channel0'); + expect(batchResult.results[0].messageId).to.include(':0'); + expect('error' in batchResult.results[0]).to.be.false; + + expect(batchResult.results[1].channel).to.equal('channel3'); + expect('messageId' in batchResult.results[1]).to.be.false; + expect(batchResult.results[1].error.statusCode).to.equal(401); + + // ...and now we use channel history to check that the expected messages have been published. + const verificationRest = helper.AblyRest({ promises: true }); + const channel0 = verificationRest.channels.get('channel0'); + const channel0History = await channel0.history({ limit: 2 }); + + const channel0HistoryData = new Set([channel0History.items[0].data, channel0History.items[1].data]); + expect(channel0HistoryData).to.deep.equal(new Set(['message1', 'message2'])); + }); + }); + }); + + describe('rest/batchPresence', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); + + it('performs a batch presence fetch and returns a result', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[2].keyStr /* we use this key so that some presence fetches fail due to capabilities */, + }); + + const presenceEnterRealtime = helper.AblyRealtime({ + promises: true, + clientId: + 'batchPresenceTest' /* note that the key used here has no capability limitations, so that we can use this instance to enter presence below */, + }); + + const channelNames = [ + 'channel0' /* key does not allow presence on this channel */, + 'channel4' /* key allows presence on this channel */, + ]; + + // First, we enter presence on two channels... + await presenceEnterRealtime.channels.get('channel0').presence.enter(); + await presenceEnterRealtime.channels.get('channel4').presence.enter(); + + // ...and now we perform the batch presence request. + const batchResult = await rest.batchPresence(channelNames); + + expect(batchResult.successCount).to.equal(1); + expect(batchResult.failureCount).to.equal(1); + + // Check that the channel0 presence fetch request fails (due to key’s capabilities, as mentioned above) + + expect(batchResult.results[0].channel).to.equal('channel0'); + expect('presence' in batchResult.results[0]).to.be.false; + expect(batchResult.results[0].error.statusCode).to.equal(401); + + // Check that the channel4 presence fetch request reflects the presence enter performed above + + expect(batchResult.results[1].channel).to.equal('channel4'); + expect(batchResult.results[1].presence).to.have.lengthOf(1); + expect(batchResult.results[1].presence[0].clientId).to.equal('batchPresenceTest'); + expect('error' in batchResult.results[1]).to.be.false; + + await new Promise((resolve, reject) => { + closeAndFinish((err) => { + err ? reject(err) : resolve(); + }, presenceEnterRealtime); + }); + }); + }); + + describe('rest/revokeTokens', function () { + this.timeout(60 * 1000); + + before(function (done) { + helper.setupApp(function (err) { + if (err) { + done(err); + } + done(); + }); + }); + + it('revokes tokens matching the given specifiers', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */, + }); + + const clientId1 = `clientId1-${randomString()}`; + const clientId2 = `clientId2-${randomString()}`; + + // First, we fetch tokens for a couple of different clientIds... + const [clientId1TokenDetails, clientId2TokenDetails] = await Promise.all([ + rest.auth.requestToken({ clientId: clientId1 }), + rest.auth.requestToken({ clientId: clientId2 }), + ]); + + // ...then, we set up Realtime instances that use these tokens and wait for them to become CONNECTED... + const clientId1Realtime = helper.AblyRealtime({ + promises: true, + token: clientId1TokenDetails, + }); + const clientId2Realtime = helper.AblyRealtime({ + promises: true, + token: clientId2TokenDetails, + }); + + await Promise.all([ + clientId1Realtime.connection.once('connected'), + clientId2Realtime.connection.once('connected'), + ]); + + // ...then, we set up listeners that will record the state change when these Realtime instances become DISCONNECTED (we need to set up these listeners here, before performing the revocation request, else we might miss the DISCONNECTED state changes that the token revocation provokes, and end up only seeing the subsequent RSA4a2-induced FAILED state change, which due to https://github.com/ably/ably-js/issues/1409 does not expose the 40141 "token revoked" error code)... + // + // Note: + // + // We use Realtime instances for verifying the side effects of a token revocation, as opposed to, say, trying to perform a REST request, because the nature of the Ably service is that token revocation may take a small delay to become active, and so there's no guarantee that a REST request peformed immediately after a revocation request would fail. See discussion at https://ably-real-time.slack.com/archives/C030C5YLY/p1690322740850269?thread_ts=1690315022.372729&cid=C030C5YLY. + const clientId1RealtimeDisconnectedStateChangePromise = clientId1Realtime.connection.once('disconnected'); + const clientId2RealtimeDisconnectedStateChangePromise = clientId2Realtime.connection.once('disconnected'); + + // ...then, we revoke all tokens for these clientIds... + + const specifiers = [ + { type: 'clientId', value: clientId1 }, + { type: 'clientId', value: clientId2 }, + { type: 'invalidType', value: 'abc' }, // we include an invalid specifier type to provoke a non-zero failureCount + ]; + + const result = await rest.auth.revokeTokens(specifiers); + + // ...and check the response from the revocation request... + expect(result.successCount).to.equal(2); + expect(result.failureCount).to.equal(1); + expect(result.results).to.have.lengthOf(3); + + expect(result.results[0].target).to.equal(`clientId:${clientId1}`); + expect(typeof result.results[0].issuedBefore).to.equal('number'); + expect(typeof result.results[0].appliesAt).to.equal('number'); + expect('error' in result.results[0]).to.be.false; + + expect(result.results[1].target).to.equal(`clientId:${clientId2}`); + expect(typeof result.results[1].issuedBefore).to.equal('number'); + expect(typeof result.results[1].appliesAt).to.equal('number'); + expect('error' in result.results[1]).to.be.false; + + expect(result.results[2].target).to.equal('invalidType:abc'); + expect(result.results[2].error.statusCode).to.equal(400); + + // ...and then, we check that the Realtime instances transition to the DISCONNECTED state due to a "token revoked" (40141) error. + const [clientId1RealtimeDisconnectedStateChange, clientId2RealtimeDisconnectedStateChange] = await Promise.all([ + clientId1RealtimeDisconnectedStateChangePromise, + clientId2RealtimeDisconnectedStateChangePromise, + ]); + + expect(clientId1RealtimeDisconnectedStateChange.reason.code).to.equal(40141 /* token revoked */); + expect(clientId2RealtimeDisconnectedStateChange.reason.code).to.equal(40141 /* token revoked */); + + await Promise.all( + [clientId1Realtime, clientId2Realtime].map((realtime) => { + new Promise((resolve, reject) => { + closeAndFinish((err) => { + err ? reject(err) : resolve(); + }, realtime); + }); + }) + ); + }); + + it('accepts optional issuedBefore and allowReauthMargin parameters', async function () { + const testApp = helper.getTestApp(); + const rest = helper.AblyRest({ + promises: true, + key: testApp.keys[4].keyStr /* this key has revocableTokens enabled */, + }); + + const clientId = `clientId-${randomString()}`; + + const serverTimeAtStartOfTest = await rest.time(); + const issuedBefore = serverTimeAtStartOfTest - 20 * 60 * 1000; // i.e. ~20 minutes ago (arbitrarily chosen) + + const result = await rest.auth.revokeTokens([{ type: 'clientId', value: clientId }], { + issuedBefore, + allowReauthMargin: true, + }); + + expect(result.results[0].issuedBefore).to.equal(issuedBefore); + + // Verify the expected side effect of allowReauthMargin, which is to delay the revocation by 30 seconds + const serverTimeThirtySecondsAfterStartOfTest = serverTimeAtStartOfTest + 30 * 1000; + expect(result.results[0].appliesAt).to.be.greaterThan(serverTimeThirtySecondsAfterStartOfTest); + }); + + it('throws an error when using token auth', function () { + const rest = helper.AblyRest({ + useTokenAuth: true, + }); + + let verifiedError = false; + try { + rest.auth.revokeTokens([{ type: 'clientId', value: 'clientId1' }], function () {}); + } catch (err) { + expect(err.statusCode).to.equal(401); + expect(err.code).to.equal(40162); + verifiedError = true; + } + + expect(verifiedError).to.be.true; + }); + }); +}); diff --git a/test/rest/status.test.js b/test/rest/status.test.js index 45e4ee144f..55c33a8b59 100644 --- a/test/rest/status.test.js +++ b/test/rest/status.test.js @@ -4,6 +4,7 @@ define(['shared_helper', 'chai'], function (helper, chai) { var rest; var utils = helper.Utils; var expect = chai.expect; + var restTestOnJsonMsgpack = helper.restTestOnJsonMsgpack; // RSL8 describe('rest/status', function () { @@ -20,7 +21,7 @@ define(['shared_helper', 'chai'], function (helper, chai) { }); }); - it('status0', async function () { + restTestOnJsonMsgpack('status0', async function (rest) { var channel = rest.channels.get('status0'); var channelDetails = await channel.status(); expect(channelDetails.channelId).to.equal('status0'); diff --git a/test/support/browser_file_list.js b/test/support/browser_file_list.js index ad830a97cc..4d0fa8522d 100644 --- a/test/support/browser_file_list.js +++ b/test/support/browser_file_list.js @@ -64,4 +64,6 @@ window.__testFiles__.files = { 'test/browser/connection.test.js': true, 'test/browser/simple.test.js': true, 'test/browser/http.test.js': true, + 'test/rest/status.test.js': true, + 'test/rest/batch.test.js': true, }; diff --git a/test/support/playwrightSetup.js b/test/support/playwrightSetup.js index 8ab4a8a99b..8ade4fe779 100644 --- a/test/support/playwrightSetup.js +++ b/test/support/playwrightSetup.js @@ -6,6 +6,7 @@ class CustomEventReporter extends Mocha.reporters.HTML { constructor(runner) { super(runner); this.indents = 0; + this.failedTests = []; runner .on(EVENT_SUITE_BEGIN, (suite) => { @@ -22,9 +23,13 @@ class CustomEventReporter extends Mocha.reporters.HTML { }) .on(EVENT_TEST_FAIL, (test, err) => { this.logToNodeConsole(`${failSymbol}: ${test.title} - error: ${err.message}`); + this.failedTests.push(test.title); }) .once(EVENT_RUN_END, () => { this.indents = 0; + if (this.failedTests.length > 0) { + this.logToNodeConsole('\nfailed tests: \n' + this.failedTests.map((x) => ' - ' + x).join('\n') + '\n'); + } runner.stats && window.dispatchEvent( new CustomEvent('testResult', {