diff --git a/entrypoints.json b/entrypoints.json index 975db81888..6b507911ae 100644 --- a/entrypoints.json +++ b/entrypoints.json @@ -24,6 +24,10 @@ "typings": "./lib/firestore/index.d.ts", "dist": "./lib/firestore/index.js" }, + "firebase-admin/functions": { + "typings": "./lib/functions/index.d.ts", + "dist": "./lib/functions/index.js" + }, "firebase-admin/installations": { "typings": "./lib/installations/index.d.ts", "dist": "./lib/installations/index.js" @@ -55,5 +59,9 @@ "firebase-admin/remote-config": { "typings": "./lib/remote-config/index.d.ts", "dist": "./lib/remote-config/index.js" + }, + "firebase-admin/eventarc": { + "typings": "./lib/eventarc/index.d.ts", + "dist": "./lib/eventarc/index.js" } } diff --git a/etc/firebase-admin.auth.api.md b/etc/firebase-admin.auth.api.md index 16fd6a64e1..2986869ef2 100644 --- a/etc/firebase-admin.auth.api.md +++ b/etc/firebase-admin.auth.api.md @@ -55,6 +55,7 @@ export abstract class BaseAuth { generateEmailVerificationLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; generatePasswordResetLink(email: string, actionCodeSettings?: ActionCodeSettings): Promise; generateSignInWithEmailLink(email: string, actionCodeSettings: ActionCodeSettings): Promise; + generateVerifyAndChangeEmailLink(email: string, newEmail: string, actionCodeSettings?: ActionCodeSettings): Promise; getProviderConfig(providerId: string): Promise; getUser(uid: string): Promise; getUserByEmail(email: string): Promise; diff --git a/etc/firebase-admin.eventarc.api.md b/etc/firebase-admin.eventarc.api.md new file mode 100644 index 0000000000..a070f8b391 --- /dev/null +++ b/etc/firebase-admin.eventarc.api.md @@ -0,0 +1,51 @@ +## API Report File for "firebase-admin.eventarc" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export class Channel { + readonly allowedEventTypes?: string[]; + get eventarc(): Eventarc; + get name(): string; + publish(events: CloudEvent | CloudEvent[]): Promise; +} + +// @public +export interface ChannelOptions { + allowedEventTypes?: string[] | string | undefined; +} + +// @public +export interface CloudEvent { + [key: string]: any; + data?: object | string; + datacontenttype?: string; + id?: string; + source?: string; + specversion?: CloudEventVersion; + subject?: string; + time?: string; + type: string; +} + +// @public +export type CloudEventVersion = '1.0'; + +// @public +export class Eventarc { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + get app(): App; + channel(name: string, options?: ChannelOptions): Channel; + channel(options?: ChannelOptions): Channel; +} + +// @public +export function getEventarc(app?: App): Eventarc; + +``` diff --git a/etc/firebase-admin.functions.api.md b/etc/firebase-admin.functions.api.md new file mode 100644 index 0000000000..2ed05d6f60 --- /dev/null +++ b/etc/firebase-admin.functions.api.md @@ -0,0 +1,56 @@ +## API Report File for "firebase-admin.functions" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +/// + +import { Agent } from 'http'; + +// @public +export interface AbsoluteDelivery { + // @alpha (undocumented) + scheduleDelaySeconds?: never; + scheduleTime?: Date; +} + +// @public +export interface DelayDelivery { + scheduleDelaySeconds?: number; + // @alpha (undocumented) + scheduleTime?: never; +} + +// @public +export type DeliverySchedule = DelayDelivery | AbsoluteDelivery; + +// @public +export class Functions { + // Warning: (ae-forgotten-export) The symbol "App" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly app: App; + taskQueue>(functionName: string, extensionId?: string): TaskQueue; +} + +// @public +export function getFunctions(app?: App): Functions; + +// @public +export type TaskOptions = DeliverySchedule & TaskOptionsExperimental & { + dispatchDeadlineSeconds?: number; +}; + +// @public +export interface TaskOptionsExperimental { + // @beta + uri?: string; +} + +// @public +export class TaskQueue> { + enqueue(data: Args, opts?: TaskOptions): Promise; +} + +``` diff --git a/package-lock.json b/package-lock.json index 88ab154a62..a00b138421 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,63 +1,62 @@ { "name": "firebase-admin", - "version": "10.0.2", + "version": "10.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { - "@ampproject/remapping": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", - "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", - "dev": true, - "requires": { - "@jridgewell/trace-mapping": "^0.3.0" - } - }, - "@babel/code-frame": { - "version": "7.16.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", - "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.16.7" - } - }, "@babel/compat-data": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", - "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz", + "integrity": "sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q==", "dev": true }, "@babel/core": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz", - "integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz", + "integrity": "sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA==", "dev": true, "requires": { - "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.7", - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.8", - "@babel/parser": "^7.17.8", + "@babel/generator": "^7.16.7", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.7", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "semver": "^6.3.0" + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } } }, "@babel/generator": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", - "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", + "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", "dev": true, "requires": { - "@babel/types": "^7.17.0", + "@babel/types": "^7.16.8", "jsesc": "^2.5.1", "source-map": "^0.5.0" }, @@ -71,12 +70,12 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", - "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz", + "integrity": "sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA==", "dev": true, "requires": { - "@babel/compat-data": "^7.17.7", + "@babel/compat-data": "^7.16.4", "@babel/helper-validator-option": "^7.16.7", "browserslist": "^4.17.5", "semver": "^6.3.0" @@ -130,28 +129,28 @@ } }, "@babel/helper-module-transforms": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", - "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz", + "integrity": "sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-simple-access": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", "@babel/helper-validator-identifier": "^7.16.7", "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, "@babel/helper-simple-access": { - "version": "7.17.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", - "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz", + "integrity": "sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g==", "dev": true, "requires": { - "@babel/types": "^7.17.0" + "@babel/types": "^7.16.7" } }, "@babel/helper-split-export-declaration": { @@ -176,20 +175,20 @@ "dev": true }, "@babel/helpers": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz", - "integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz", + "integrity": "sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw==", "dev": true, "requires": { "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" } }, "@babel/highlight": { - "version": "7.16.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", - "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz", + "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.16.7", @@ -256,9 +255,9 @@ } }, "@babel/parser": { - "version": "7.17.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", - "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz", + "integrity": "sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw==", "dev": true }, "@babel/template": { @@ -270,26 +269,46 @@ "@babel/code-frame": "^7.16.7", "@babel/parser": "^7.16.7", "@babel/types": "^7.16.7" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + } } }, "@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.8.tgz", + "integrity": "sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ==", "dev": true, "requires": { "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", + "@babel/generator": "^7.16.8", "@babel/helper-environment-visitor": "^7.16.7", "@babel/helper-function-name": "^7.16.7", "@babel/helper-hoist-variables": "^7.16.7", "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", + "@babel/parser": "^7.16.8", + "@babel/types": "^7.16.8", "debug": "^4.1.0", "globals": "^11.1.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -299,9 +318,9 @@ } }, "@babel/types": { - "version": "7.17.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", - "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz", + "integrity": "sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.16.7", @@ -324,9 +343,9 @@ } }, "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -374,31 +393,32 @@ } }, "@firebase/app": { - "version": "0.7.21", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.21.tgz", - "integrity": "sha512-b1COyw4HwajJ4zQCtL7w+d4GCQDmEaVO957eLLlBwz4QuDlx3eQIirpQhzkkPH17BJFZ6x0qyYEt6Wbhakn0kg==", + "version": "0.7.18", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.7.18.tgz", + "integrity": "sha512-jomDaPaEQEWfFUqvxQw4TYSs2gCT2BN0Ec1//3CdMsc1NcppduS31bxsjhn3KdPbtx4opkaZ2FcA+buHtdw9dw==", "dev": true, "requires": { - "@firebase/component": "0.5.13", + "@firebase/component": "0.5.10", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", + "idb": "3.0.2", "tslib": "^2.1.0" }, "dependencies": { "@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", + "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", "dev": true, "requires": { - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", "tslib": "^2.1.0" } }, "@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", + "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -407,32 +427,32 @@ } }, "@firebase/app-compat": { - "version": "0.1.22", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.22.tgz", - "integrity": "sha512-InzQWdIKXsioZb6Ll/uynvopFbq9k3Qpi3gEUq+f8q0yr8/KQVuH2lIDmN70z11LRKXlsziU49qRwtV9tcEYhA==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.1.19.tgz", + "integrity": "sha512-a0TgAXcjF3htSdi10mRwAks1+73nwbmSMXzjlOQDYJ8t3HE7FvHxfB4hjuwHKfgr3MWZjcarsGKVr7LWhUAE8w==", "dev": true, "requires": { - "@firebase/app": "0.7.21", - "@firebase/component": "0.5.13", + "@firebase/app": "0.7.18", + "@firebase/component": "0.5.10", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", "tslib": "^2.1.0" }, "dependencies": { "@firebase/component": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", - "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", + "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", "dev": true, "requires": { - "@firebase/util": "1.5.2", + "@firebase/util": "1.4.3", "tslib": "^2.1.0" } }, "@firebase/util": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", - "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", + "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -446,14 +466,14 @@ "integrity": "sha512-6fbHQwDv2jp/v6bXhBw2eSRbNBpxHcd1NBF864UksSMVIqIyri9qpJB1Mn6sGZE+bnDsSQBC5j2TbMxYsJQkQg==" }, "@firebase/auth": { - "version": "0.19.11", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.19.11.tgz", - "integrity": "sha512-9bctXmQA5pRhLL03wkbg6ibmhoTMa8QRHm3uDnb+iyMcHTJ5AyILRc5AVPS9FsnpWPDOLiVjtuMC28D6iC+zew==", + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.19.8.tgz", + "integrity": "sha512-pU3U8k70gXDYHjrIDlxnnfPkt6Eq1/61KikF7aps1ny8xmSyeUhbXxUbl2pvX5k7eK8uVQvm4uWFlPNJWMitww==", "dev": true, "requires": { - "@firebase/component": "0.5.12", + "@firebase/component": "0.5.10", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.1", + "@firebase/util": "1.4.3", "node-fetch": "2.6.7", "selenium-webdriver": "4.0.0-rc-1", "tslib": "^2.1.0" @@ -474,15 +494,15 @@ } }, "@firebase/auth-compat": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.11.tgz", - "integrity": "sha512-6C42yXevri3F7H1LS3h524UsQsUlzGuszlIL3YsDuS+WJFqBe8I5AHOEM+Opi/VtIpWaXxPhWsp75TQndaCjKA==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.2.8.tgz", + "integrity": "sha512-6gG8agS3LlSxnyObZ7TR1Ze41cJargpP+rGTuBz0WiOvrFcrMoZUjv+5oA5VvF2GiYVMvAzJImxmgYJhMse+GA==", "dev": true, "requires": { - "@firebase/auth": "0.19.11", + "@firebase/auth": "0.19.8", "@firebase/auth-types": "0.11.0", - "@firebase/component": "0.5.12", - "@firebase/util": "1.5.1", + "@firebase/component": "0.5.10", + "@firebase/util": "1.4.3", "node-fetch": "2.6.7", "selenium-webdriver": "^4.0.0-beta.2", "tslib": "^2.1.0" @@ -500,47 +520,96 @@ "dev": true }, "@firebase/component": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.12.tgz", - "integrity": "sha512-gAKwxo0Ev+rp7Px+Yr71WbcC0CM9Tevhv0g38ORp2p57HqGjY65D3MD+jTKGZl58N/0nmX6MRRKym3bq/3k1gw==", + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.10.tgz", + "integrity": "sha512-mzUpg6rsBbdQJvAdu1rNWabU3O7qdd+B+/ubE1b+pTbBKfw5ySRpRRE6sKcZ/oQuwLh0HHB6FRJHcylmI7jDzA==", + "dev": true, "requires": { - "@firebase/util": "1.5.1", + "@firebase/util": "1.4.3", "tslib": "^2.1.0" } }, "@firebase/database": { - "version": "0.12.7", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.7.tgz", - "integrity": "sha512-HL2NMQ3Ce5YNM2MdEuACHmU9NQEwq2F64R0XK+CReph40skxp+A7TvlJDO5bTAC0s3l3ebgCA9VmxfJu5R6UAA==", + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.12.8.tgz", + "integrity": "sha512-JBQVfFLzfhxlQbl4OU6ov9fdsddkytBQdtSSR49cz48homj38ccltAhK6seum+BI7f28cV2LFHF9672lcN+qxA==", "requires": { "@firebase/auth-interop-types": "0.1.6", - "@firebase/component": "0.5.12", + "@firebase/component": "0.5.13", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.1", + "@firebase/util": "1.5.2", "faye-websocket": "0.11.4", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "requires": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-compat": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.7.tgz", - "integrity": "sha512-T1mleRv2A8wyTV/jUuOdkN9Tl2lz0RGauqGc9nxP3AUzS9m3gIDN7u4CahZSdJlkR6tSU/MEWlfs5Q/oZStqxg==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.1.8.tgz", + "integrity": "sha512-dhXr5CSieBuKNdU96HgeewMQCT9EgOIkfF1GNy+iRrdl7BWLxmlKuvLfK319rmIytSs/vnCzcD9uqyxTeU/A3A==", "requires": { - "@firebase/component": "0.5.12", - "@firebase/database": "0.12.7", - "@firebase/database-types": "0.9.6", + "@firebase/component": "0.5.13", + "@firebase/database": "0.12.8", + "@firebase/database-types": "0.9.7", "@firebase/logger": "0.3.2", - "@firebase/util": "1.5.1", + "@firebase/util": "1.5.2", "tslib": "^2.1.0" + }, + "dependencies": { + "@firebase/component": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.5.13.tgz", + "integrity": "sha512-hxhJtpD8Ppf/VU2Rlos6KFCEV77TGIGD5bJlkPK1+B/WUe0mC6dTjW7KhZtXTc+qRBp9nFHWcsIORnT8liHP9w==", + "requires": { + "@firebase/util": "1.5.2", + "tslib": "^2.1.0" + } + }, + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/database-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.6.tgz", - "integrity": "sha512-E7U28X+FtVtug7EkIkaOXbdP8ghCPno21WWgEiDKsneY28N5WOwccfXqSzHgAAezkR40ht/ZqXlCsUhEpv6JXw==", + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.9.7.tgz", + "integrity": "sha512-EFhgL89Fz6DY3kkB8TzdHvdu8XaqqvzcF2DLVOXEnQ3Ms7L755p5EO42LfxXoJqb9jKFvgLpFmKicyJG25WFWw==", "requires": { "@firebase/app-types": "0.7.0", - "@firebase/util": "1.5.1" + "@firebase/util": "1.5.2" + }, + "dependencies": { + "@firebase/util": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.2.tgz", + "integrity": "sha512-YvBH2UxFcdWG2HdFnhxZptPl2eVFlpOyTH66iDo13JPEYraWzWToZ5AMTtkyRHVmu7sssUpQlU9igy1KET7TOw==", + "requires": { + "tslib": "^2.1.0" + } + } } }, "@firebase/logger": { @@ -552,9 +621,10 @@ } }, "@firebase/util": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.5.1.tgz", - "integrity": "sha512-ojwPg8sKVcoU/kC1QdTrD+eUDyjQkZyiH9tlouXeZdAeDddCYNvHgIeBQhZt62WIcjlNhy1zro/xdV5nUUU38A==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.4.3.tgz", + "integrity": "sha512-gQJl6r0a+MElLQEyU8Dx0kkC2coPj67f/zKZrGR7z7WpLgVanhaCUqEsptwpwoxi9RMFIaebleG+C9xxoARq+Q==", + "dev": true, "requires": { "tslib": "^2.1.0" } @@ -594,9 +664,9 @@ "optional": true }, "@google-cloud/storage": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.2.tgz", - "integrity": "sha512-0saiMeQkDALf9pvDD1rDZGV5aktUbnIWjm9jYiFKlxiMROJhODzaaltvyKg5oY6ujHv7Fzc5pzQVFo3R8vENpw==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-5.19.4.tgz", + "integrity": "sha512-Jz7ugcPHhsEmMVvIxM9uoBsdEbKIYwDkh3u07tifsIymEWs47F4/D6+/Tv/W7kLhznqjyOjVJ/0frtBeIC0lJA==", "optional": true, "requires": { "@google-cloud/paginator": "^3.0.7", @@ -627,9 +697,9 @@ } }, "@grpc/grpc-js": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.10.tgz", - "integrity": "sha512-++oAubX/7rJzlqH0ShyzDENNNDHYrlttdc3NM40KlaVQDcgGqQknuPoavmyTC+oNUDyxPCX5dHceKhfcgN3tiw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.5.2.tgz", + "integrity": "sha512-JlBkWqm2qVtZTg6OQU9g0o9C3jR6Us0TekZlTVCESxq5wUbFUjrW5GijXPDpwLqdmabCRJ0xm9Ayyj+b9T9pow==", "optional": true, "requires": { "@grpc/proto-loader": "^0.6.4", @@ -769,38 +839,16 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, - "@jridgewell/resolve-uri": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", - "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", - "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "@mapbox/node-pre-gyp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz", - "integrity": "sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz", + "integrity": "sha512-CMGKi28CF+qlbXh26hDe6NxCd7amqeAzEqnS6IHeO6LoaKyM/n+Xw3HT1COdq8cuioOdlKdqn/hCmqPUOMOywg==", "dev": true, "requires": { - "detect-libc": "^2.0.0", + "detect-libc": "^1.0.3", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", - "node-fetch": "^2.6.7", + "node-fetch": "^2.6.5", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", @@ -820,17 +868,17 @@ } }, "@microsoft/api-extractor": { - "version": "7.21.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.21.3.tgz", - "integrity": "sha512-ZQwuu5QbJq/TMDGr8NGmt4p/kcceaQAhQYQ4DszwNKDaCi/IhGVtO2zRcjSt8DEI2XD40s/CAOPYyF2C+Y99Ow==", + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.19.4.tgz", + "integrity": "sha512-iehC6YA3DGJvxTUaK7HUtQmP6hAQU07+Q/OR8TG4dVR6KpqCi9UPEVk8AgCvQkiK+6FbVEFQTx0qLuYk4EeuHg==", "dev": true, "requires": { - "@microsoft/api-extractor-model": "7.16.2", - "@microsoft/tsdoc": "0.14.1", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.45.2", - "@rushstack/rig-package": "0.3.9", - "@rushstack/ts-command-line": "4.10.8", + "@microsoft/api-extractor-model": "7.15.3", + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.45.0", + "@rushstack/rig-package": "0.3.7", + "@rushstack/ts-command-line": "4.10.6", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.17.0", @@ -840,15 +888,15 @@ }, "dependencies": { "@microsoft/tsdoc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", - "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, "@rushstack/node-core-library": { - "version": "3.45.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.2.tgz", - "integrity": "sha512-MJKdB6mxOoIkks3htGVCo7aiTzllm2I6Xua+KbTSb0cp7rBp8gTCOF/4d8R4HFMwpRdEdwzKgqMM6k9rAK73iw==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.0.tgz", + "integrity": "sha512-YMuIJl19vQT1+g/OU9mLY6T5ZBT9uDlmeXExDQACpGuxTJW+LHNbk/lRX+eCApQI2eLBlaL4U68r3kZlqwbdmw==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -863,9 +911,9 @@ } }, "@rushstack/ts-command-line": { - "version": "4.10.8", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.8.tgz", - "integrity": "sha512-G7CQYY/m3aZU5fVxbebv35yDeua7sSumrDAB2pJp0d60ZEsxGkUQW8771CeMcGWwSKqT9PxPzKpmIakiWv54sA==", + "version": "4.10.6", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.6.tgz", + "integrity": "sha512-Y3GkUag39sTIlukDg9mUp8MCHrrlJ27POrBNRQGc/uF+VVgX8M7zMzHch5zP6O1QVquWgD7Engdpn2piPYaS/g==", "dev": true, "requires": { "@types/argparse": "1.0.38", @@ -881,18 +929,18 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" } }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz", + "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==", "dev": true }, "validator": { @@ -902,12 +950,12 @@ "dev": true }, "z-schema": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.3.tgz", - "integrity": "sha512-sGvEcBOTNum68x9jCpCVGPFJ6mWnkD0YxOcddDlJHRx3tKdB2q8pCHExMVZo/AV/6geuVJXG7hljDaWG8+5GDw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", "dev": true, "requires": { - "commander": "^2.20.3", + "commander": "^2.7.1", "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", "validator": "^13.7.0" @@ -916,26 +964,26 @@ } }, "@microsoft/api-extractor-model": { - "version": "7.16.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.16.2.tgz", - "integrity": "sha512-hnqKsG89iIiQlLDXasxFw8QR0LwakPVXejNVmlGpFL008R+IExgc1f+tBrUAm1in6Oq76t7Ea0TFhER56Qfhaw==", + "version": "7.15.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.15.3.tgz", + "integrity": "sha512-NkSjolmSI7NGvbdz0Y7kjQfdpD+j9E5CwXTxEyjDqxd10MI7GXV8DnAsQ57GFJcgHKgTjf2aUnYfMJ9w3aMicw==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.14.1", - "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.45.2" + "@microsoft/tsdoc": "0.13.2", + "@microsoft/tsdoc-config": "~0.15.2", + "@rushstack/node-core-library": "3.45.0" }, "dependencies": { "@microsoft/tsdoc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", - "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, "@rushstack/node-core-library": { - "version": "3.45.2", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.2.tgz", - "integrity": "sha512-MJKdB6mxOoIkks3htGVCo7aiTzllm2I6Xua+KbTSb0cp7rBp8gTCOF/4d8R4HFMwpRdEdwzKgqMM6k9rAK73iw==", + "version": "3.45.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.45.0.tgz", + "integrity": "sha512-YMuIJl19vQT1+g/OU9mLY6T5ZBT9uDlmeXExDQACpGuxTJW+LHNbk/lRX+eCApQI2eLBlaL4U68r3kZlqwbdmw==", "dev": true, "requires": { "@types/node": "12.20.24", @@ -956,9 +1004,9 @@ "dev": true }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -971,12 +1019,12 @@ "dev": true }, "z-schema": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.3.tgz", - "integrity": "sha512-sGvEcBOTNum68x9jCpCVGPFJ6mWnkD0YxOcddDlJHRx3tKdB2q8pCHExMVZo/AV/6geuVJXG7hljDaWG8+5GDw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", "dev": true, "requires": { - "commander": "^2.20.3", + "commander": "^2.7.1", "lodash.get": "^4.4.2", "lodash.isequal": "^4.5.0", "validator": "^13.7.0" @@ -991,21 +1039,21 @@ "dev": true }, "@microsoft/tsdoc-config": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.1.tgz", - "integrity": "sha512-2RqkwiD4uN6MLnHFljqBlZIXlt/SaUT6cuogU1w2ARw4nKuuppSmR0+s+NC+7kXBQykd9zzu0P4HtBpZT5zBpQ==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.15.2.tgz", + "integrity": "sha512-mK19b2wJHSdNf8znXSMYVShAHktVr/ib0Ck2FA3lsVBSEhSI/TfXT7DJQkAYgcztTuwazGcg58ZjYdk0hTCVrA==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.14.1", + "@microsoft/tsdoc": "0.13.2", "ajv": "~6.12.6", "jju": "~1.4.0", "resolve": "~1.19.0" }, "dependencies": { "@microsoft/tsdoc": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.1.tgz", - "integrity": "sha512-6Wci+Tp3CgPt/B9B0a3J4s3yMgLNSku6w5TV6mN+61C71UqsRBv2FUibBf3tPGlNxebgPHMEUzKpb1ggE8KCKw==", + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.13.2.tgz", + "integrity": "sha512-WrHvO8PDL8wd8T2+zBGKrMwVL5IyzR3ryWUsl0PXgEV0QHup4mTLi0QcATefGI6Gx9Anu7vthPyyyLpY0EpiQg==", "dev": true }, "resolve": { @@ -1150,9 +1198,9 @@ } }, "@rushstack/rig-package": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.9.tgz", - "integrity": "sha512-z3Oxpfb4n9mGXwseX+ifpkmUf9B8Fy8oieVwg8eFgpCbzllkgOwEiwLKEnRWVQ8owFcd46NCKz+7ICH35CRsAw==", + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.7.tgz", + "integrity": "sha512-pzMsTSeTC8IiZ6EJLr53gGMvhT4oLWH+hxD7907cHyWuIUlEXFtu/2pK25vUQT13nKp5DJCWxXyYoGRk/h6rtA==", "dev": true, "requires": { "resolve": "~1.17.0", @@ -1181,9 +1229,9 @@ } }, "@sinonjs/fake-timers": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", - "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" @@ -1273,15 +1321,15 @@ "dev": true }, "@types/chai": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", - "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.1.tgz", + "integrity": "sha512-/zPMqDkzSZ8t3VtxOa4KPq7uzzW978M9Tvh+j7GHKuo6k6GTLxPJ4J5gE5cjfJ26pnXst0N5Hax8Sr0T2Mi9zQ==", "dev": true }, "@types/chai-as-promised": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz", - "integrity": "sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", "dev": true, "requires": { "@types/chai": "*" @@ -1326,9 +1374,9 @@ } }, "@types/express-unless": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.3.tgz", - "integrity": "sha512-TyPLQaF6w8UlWdv4gj8i46B+INBVzURBNRahCozCSXfsK2VTlL1wNyTlMKw817VHygBtlcl5jfnPadlydr06Yw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.2.tgz", + "integrity": "sha512-Q74UyYRX/zIgl1HSp9tUX2PlG8glkVm+59r7aK4KGKzC5jqKIOX6rrVLRQrzpZUQ84VukHtRoeAuon2nIssHPQ==", "requires": { "@types/express": "*" } @@ -1355,9 +1403,9 @@ } }, "@types/lodash": { - "version": "4.14.181", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz", - "integrity": "sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag==", + "version": "4.14.178", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz", + "integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==", "dev": true }, "@types/long": { @@ -1384,9 +1432,9 @@ "dev": true }, "@types/mocha": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", - "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, "@types/nock": { @@ -1399,9 +1447,9 @@ } }, "@types/node": { - "version": "17.0.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.25.tgz", - "integrity": "sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==" + "version": "17.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz", + "integrity": "sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog==" }, "@types/qs": { "version": "6.9.7", @@ -1445,12 +1493,12 @@ } }, "@types/sinon": { - "version": "10.0.11", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz", - "integrity": "sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", "dev": true, "requires": { - "@types/sinonjs__fake-timers": "*" + "@sinonjs/fake-timers": "^7.1.0" } }, "@types/sinon-chai": { @@ -1463,27 +1511,27 @@ "@types/sinon": "*" } }, - "@types/sinonjs__fake-timers": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", - "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", - "dev": true - }, "@types/tough-cookie": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==", "dev": true }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.20.0.tgz", - "integrity": "sha512-fapGzoxilCn3sBtC6NtXZX6+P/Hef7VDbyfGqTTpzYydwhlkevB+0vE0EnmHPVTVSy68GUncyJ/2PcrFBeCo5Q==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.22.0.tgz", + "integrity": "sha512-YCiy5PUzpAeOPGQ7VSGDEY2NeYUV1B0swde2e0HzokRsHBYjSdF6DZ51OuRZxVPHx0032lXGLvOMls91D8FXlg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.20.0", - "@typescript-eslint/type-utils": "5.20.0", - "@typescript-eslint/utils": "5.20.0", + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/type-utils": "5.22.0", + "@typescript-eslint/utils": "5.22.0", "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", @@ -1493,28 +1541,28 @@ }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz", - "integrity": "sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", + "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/visitor-keys": "5.20.0" + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0" } }, "@typescript-eslint/types": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz", - "integrity": "sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", + "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", "dev": true }, "@typescript-eslint/visitor-keys": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz", - "integrity": "sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", + "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/types": "5.22.0", "eslint-visitor-keys": "^3.0.0" } }, @@ -1530,52 +1578,52 @@ } }, "@typescript-eslint/parser": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.19.0.tgz", - "integrity": "sha512-yhktJjMCJX8BSBczh1F/uY8wGRYrBeyn84kH6oyqdIJwTGKmzX5Qiq49LRQ0Jh0LXnWijEziSo6BRqny8nqLVQ==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.12.0.tgz", + "integrity": "sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.19.0", - "@typescript-eslint/types": "5.19.0", - "@typescript-eslint/typescript-estree": "5.19.0", + "@typescript-eslint/scope-manager": "5.12.0", + "@typescript-eslint/types": "5.12.0", + "@typescript-eslint/typescript-estree": "5.12.0", "debug": "^4.3.2" } }, "@typescript-eslint/scope-manager": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.19.0.tgz", - "integrity": "sha512-Fz+VrjLmwq5fbQn5W7cIJZ066HxLMKvDEmf4eu1tZ8O956aoX45jAuBB76miAECMTODyUxH61AQM7q4/GOMQ5g==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.12.0.tgz", + "integrity": "sha512-GAMobtIJI8FGf1sLlUWNUm2IOkIjvn7laFWyRx7CLrv6nLBI7su+B7lbStqVlK5NdLvHRFiJo2HhiDF7Ki01WQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.19.0", - "@typescript-eslint/visitor-keys": "5.19.0" + "@typescript-eslint/types": "5.12.0", + "@typescript-eslint/visitor-keys": "5.12.0" } }, "@typescript-eslint/type-utils": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.20.0.tgz", - "integrity": "sha512-WxNrCwYB3N/m8ceyoGCgbLmuZwupvzN0rE8NBuwnl7APgjv24ZJIjkNzoFBXPRCGzLNkoU/WfanW0exvp/+3Iw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.22.0.tgz", + "integrity": "sha512-iqfLZIsZhK2OEJ4cQ01xOq3NaCuG5FQRKyHicA3xhZxMgaxQazLUHbH/B2k9y5i7l3+o+B5ND9Mf1AWETeMISA==", "dev": true, "requires": { - "@typescript-eslint/utils": "5.20.0", + "@typescript-eslint/utils": "5.22.0", "debug": "^4.3.2", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.19.0.tgz", - "integrity": "sha512-zR1ithF4Iyq1wLwkDcT+qFnhs8L5VUtjgac212ftiOP/ZZUOCuuF2DeGiZZGQXGoHA50OreZqLH5NjDcDqn34w==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.12.0.tgz", + "integrity": "sha512-JowqbwPf93nvf8fZn5XrPGFBdIK8+yx5UEGs2QFAYFI8IWYfrzz+6zqlurGr2ctShMaJxqwsqmra3WXWjH1nRQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.19.0.tgz", - "integrity": "sha512-dRPuD4ocXdaE1BM/dNR21elSEUPKaWgowCA0bqJ6YbYkvtrPVEvZ+zqcX5a8ECYn3q5iBSSUcBBD42ubaOp0Hw==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.0.tgz", + "integrity": "sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ==", "dev": true, "requires": { - "@typescript-eslint/types": "5.19.0", - "@typescript-eslint/visitor-keys": "5.19.0", + "@typescript-eslint/types": "5.12.0", + "@typescript-eslint/visitor-keys": "5.12.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -1584,9 +1632,9 @@ }, "dependencies": { "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -1595,43 +1643,43 @@ } }, "@typescript-eslint/utils": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.20.0.tgz", - "integrity": "sha512-lHONGJL1LIO12Ujyx8L8xKbwWSkoUKFSO+0wDAqGXiudWB2EO7WEUT+YZLtVbmOmSllAjLb9tpoIPwpRe5Tn6w==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.22.0.tgz", + "integrity": "sha512-HodsGb037iobrWSUMS7QH6Hl1kppikjA1ELiJlNSTYf/UdMEwzgj0WIp+lBNb6WZ3zTwb0tEz51j0Wee3iJ3wQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.20.0", - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/typescript-estree": "5.20.0", + "@typescript-eslint/scope-manager": "5.22.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/typescript-estree": "5.22.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" }, "dependencies": { "@typescript-eslint/scope-manager": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.20.0.tgz", - "integrity": "sha512-h9KtuPZ4D/JuX7rpp1iKg3zOH0WNEa+ZIXwpW/KWmEFDxlA/HSfCMhiyF1HS/drTICjIbpA6OqkAhrP/zkCStg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.22.0.tgz", + "integrity": "sha512-yA9G5NJgV5esANJCO0oF15MkBO20mIskbZ8ijfmlKIvQKg0ynVKfHZ15/nhAJN5m8Jn3X5qkwriQCiUntC9AbA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/visitor-keys": "5.20.0" + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0" } }, "@typescript-eslint/types": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.20.0.tgz", - "integrity": "sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.22.0.tgz", + "integrity": "sha512-T7owcXW4l0v7NTijmjGWwWf/1JqdlWiBzPqzAWhobxft0SiEvMJB56QXmeCQjrPuM8zEfGUKyPQr/L8+cFUBLw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.20.0.tgz", - "integrity": "sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.22.0.tgz", + "integrity": "sha512-EyBEQxvNjg80yinGE2xdhpDYm41so/1kOItl0qrjIiJ1kX/L/L8WWGmJg8ni6eG3DwqmOzDqOhe6763bF92nOw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.20.0", - "@typescript-eslint/visitor-keys": "5.20.0", + "@typescript-eslint/types": "5.22.0", + "@typescript-eslint/visitor-keys": "5.22.0", "debug": "^4.3.2", "globby": "^11.0.4", "is-glob": "^4.0.3", @@ -1640,12 +1688,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.20.0.tgz", - "integrity": "sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.22.0.tgz", + "integrity": "sha512-DbgTqn2Dv5RFWluG88tn0pP6Ex0ROF+dpDO1TNNZdRtLjUr6bdznjA6f/qNqJLjd2PgguAES2Zgxh/JzwzETDg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.20.0", + "@typescript-eslint/types": "5.22.0", "eslint-visitor-keys": "^3.0.0" } }, @@ -1661,12 +1709,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.19.0.tgz", - "integrity": "sha512-Ym7zZoMDZcAKWsULi2s7UMLREdVQdScPQ/fKWMYefarCztWlHPFVJo8racf8R0Gc8FAEJ2eD4of8As1oFtnQlQ==", + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.0.tgz", + "integrity": "sha512-cFwTlgnMV6TgezQynx2c/4/tx9Tufbuo9LPzmWqyRC3QC4qTGkAG1C6pBr0/4I10PAI/FlYunI3vJjIcu+ZHMg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.19.0", + "@typescript-eslint/types": "5.12.0", "eslint-visitor-keys": "^3.0.0" } }, @@ -1740,13 +1788,10 @@ } }, "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" - } + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true }, "ansi-cyan": { "version": "0.1.1", @@ -2357,15 +2402,15 @@ "dev": true }, "browserslist": { - "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz", + "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001317", - "electron-to-chromium": "^1.4.84", + "caniuse-lite": "^1.0.30001286", + "electron-to-chromium": "^1.4.17", "escalade": "^3.1.1", - "node-releases": "^2.0.2", + "node-releases": "^2.0.1", "picocolors": "^1.0.0" } }, @@ -2449,9 +2494,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001323", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001323.tgz", - "integrity": "sha512-e4BF2RlCVELKx8+RmklSEIVub1TWrmdhvA5kEUueummz1XyySW0DVk+3x9HyhU9MuWTa2BhqLgEuEmUwASAdCA==", + "version": "1.0.30001299", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz", + "integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==", "dev": true }, "caseless": { @@ -2461,16 +2506,15 @@ "dev": true }, "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dev": true, "requires": { "assertion-error": "^1.1.0", "check-error": "^1.0.2", "deep-eql": "^3.0.1", "get-func-name": "^2.0.0", - "loupe": "^2.3.1", "pathval": "^1.1.1", "type-detect": "^4.0.5" } @@ -2484,6 +2528,15 @@ "check-error": "^1.0.2" } }, + "chai-exclude": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/chai-exclude/-/chai-exclude-2.1.0.tgz", + "integrity": "sha512-IBnm50Mvl3O1YhPpTgbU8MK0Gw7NHcb18WT2TxGdPKOMtdtZVKLHmQwdvOF7mTlHVQStbXuZKFwkevFtbHjpVg==", + "dev": true, + "requires": { + "fclone": "^1.0.11" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3002,15 +3055,15 @@ } }, "date-and-time": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.3.1.tgz", - "integrity": "sha512-OaIRmSJXifwEN21rMVVDs0Kz8uhJ3wWPYd86atkRiqN54liaMQYEbbrgjZQea75YXOBWL4ZFb3rG/waenw1TEg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-2.1.0.tgz", + "integrity": "sha512-X/b2gM7e8zQ7siiE0DhRLeNMpuCkIqec5Jnx4GMk/HWB71a6e5Lz48mH9/GIS/hpLsBRFZfMF1gjXBkY0vq5oA==", "optional": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "requires": { "ms": "2.1.2" } @@ -3167,9 +3220,9 @@ "dev": true }, "detect-libc": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", - "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "dev": true }, "dicer": { @@ -3254,9 +3307,9 @@ } }, "electron-to-chromium": { - "version": "1.4.103", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz", - "integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg==", + "version": "1.4.44", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.44.tgz", + "integrity": "sha512-tHGWiUUmY7GABK8+DNcr474cnZDTzD8x1736SlDosVH8+/vRJeqfaIBAEHFtMjddz/0T4rKKYsxEc8BwQRdBpw==", "dev": true }, "emoji-regex": { @@ -3288,9 +3341,9 @@ } }, "es-abstract": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.2.tgz", - "integrity": "sha512-gfSBJoZdlL2xRiOCy0g8gLMryhoe1TlimjzU99L/31Z8QEGIhVQI+EWwt5lT+AuU9SnorVupXFqqOGqGfsyO6w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -3299,15 +3352,15 @@ "get-intrinsic": "^1.1.1", "get-symbol-description": "^1.0.0", "has": "^1.0.3", - "has-symbols": "^1.0.3", + "has-symbols": "^1.0.2", "internal-slot": "^1.0.3", "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.2", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.1", "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.0", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -3327,14 +3380,14 @@ } }, "es5-ext": { - "version": "0.10.59", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.59.tgz", - "integrity": "sha512-cOgyhW0tIJyQY1Kfw6Kr0viu9ZlUctVchRMZ7R0HiH3dxTSp5zJDLecwxUqPUrGKMsgBI1wd1FL+d9Jxfi4cLw==", + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", "dev": true, "requires": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" } }, "es6-error": { @@ -3388,12 +3441,12 @@ "dev": true }, "eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", - "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -3663,9 +3716,9 @@ }, "dependencies": { "type": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/type/-/type-2.6.0.tgz", - "integrity": "sha512-eiDBDOmkih5pMbo9OqsqPRGMljLodLcwd5XD5JbtNB0o89xZAwynY9EdCDsJU7LtcVCClu9DvM7/0Ep1hYX3EQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", + "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==", "dev": true } } @@ -3785,9 +3838,9 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { - "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "version": "3.2.10", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.10.tgz", + "integrity": "sha512-s9nFhFnvR63wls6/kM88kQqDhMu0AfdjqouE2l5GVQPbqLgyFjjU5ry/r2yKsJxpb9Py1EYNqieFrmMaX4v++A==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -3832,6 +3885,12 @@ "websocket-driver": ">=0.5.1" } }, + "fclone": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fclone/-/fclone-1.0.11.tgz", + "integrity": "sha1-EOhdo4v+p/xZk0HClu4ddyZu5kA=", + "dev": true + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4037,9 +4096,9 @@ } }, "flatted": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", - "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz", + "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", "dev": true }, "flush-write-stream": { @@ -4546,9 +4605,9 @@ } }, "google-gax": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.30.1.tgz", - "integrity": "sha512-AR00wrunctUqwKQFl15Yq5bo9NuFLnT0zguZYCf8eAqoOUMbxn9V1L0ONCtV4+P9z7sLu+cjtgl+5b4eRZvktg==", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-2.29.4.tgz", + "integrity": "sha512-3o6cByD2fE1yIc6i1gpKMQlJStqlvu8Sa/Ly/HCQ6GPHpltpVfkTT4KVj2YLVa7WTSDoGbsLBDmJ1KfN1uNiRw==", "optional": true, "requires": { "@grpc/grpc-js": "~1.5.0", @@ -4557,11 +4616,11 @@ "abort-controller": "^3.0.0", "duplexify": "^4.0.0", "fast-text-encoding": "^1.0.3", - "google-auth-library": "^7.14.0", + "google-auth-library": "^7.6.1", "is-stream-ended": "^0.1.4", "node-fetch": "^2.6.1", - "object-hash": "^3.0.0", - "proto3-json-serializer": "^0.1.8", + "object-hash": "^2.1.1", + "proto3-json-serializer": "^0.1.7", "protobufjs": "6.11.2", "retry-request": "^4.0.0" } @@ -4580,20 +4639,14 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, "gtoken": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.2.tgz", - "integrity": "sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.1.tgz", + "integrity": "sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==", "optional": true, "requires": { "gaxios": "^4.0.0", - "google-p12-pem": "^3.1.3", + "google-p12-pem": "^3.0.3", "jws": "^4.0.0" } }, @@ -4609,6 +4662,15 @@ "vinyl-fs": "^3.0.0" }, "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -4862,9 +4924,9 @@ "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true }, "has-tostringtag": { @@ -5034,6 +5096,12 @@ "debug": "4" } }, + "idb": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/idb/-/idb-3.0.2.tgz", + "integrity": "sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5200,9 +5268,9 @@ "dev": true }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "dev": true, "requires": { "has": "^1.0.3" @@ -5569,9 +5637,9 @@ } }, "istanbul-reports": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz", - "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz", + "integrity": "sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -5667,10 +5735,13 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } }, "jsonfile": { "version": "4.0.0", @@ -5737,9 +5808,9 @@ } }, "jszip": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.8.0.tgz", - "integrity": "sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", "dev": true, "requires": { "lie": "~3.3.0", @@ -5804,15 +5875,25 @@ } }, "jwks-rsa": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.0.5.tgz", - "integrity": "sha512-fliHfsiBRzEU0nXzSvwnh0hynzGB0WihF+CinKbSRlaqRxbqqKf2xbBPgwc8mzf18/WgwlG8e5eTpfSTBcU4DQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-2.1.0.tgz", + "integrity": "sha512-GKOSDBWWBCiQTzawei6mEdRQvji5gecj8F9JwMt0ZOPnBPSmTjo5CKFvvbhE7jGPkU159Cpi0+OTLuABFcNOQQ==", "requires": { "@types/express-jwt": "0.0.42", - "debug": "^4.3.2", + "debug": "^4.3.4", "jose": "^2.0.5", "limiter": "^1.1.5", "lru-memoizer": "^2.1.4" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + } } }, "jws": { @@ -6085,15 +6166,6 @@ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", "optional": true }, - "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dev": true, - "requires": { - "get-func-name": "^2.0.0" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -6322,13 +6394,13 @@ "dev": true }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" + "braces": "^3.0.1", + "picomatch": "^2.2.3" } }, "mime": { @@ -6338,22 +6410,22 @@ "optional": true }, "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.52.0" + "mime-db": "1.51.0" } }, "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -6412,43 +6484,35 @@ "dev": true }, "mocha": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.2.tgz", - "integrity": "sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.0.0.tgz", + "integrity": "sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", "chokidar": "3.5.3", - "debug": "4.3.3", + "debug": "4.3.4", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", "glob": "7.2.0", - "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", - "minimatch": "4.2.1", + "minimatch": "5.0.1", "ms": "2.1.3", - "nanoid": "3.3.1", + "nanoid": "3.3.3", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", - "which": "2.0.2", - "workerpool": "6.2.0", + "workerpool": "6.2.1", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" }, "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -6471,6 +6535,15 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6488,9 +6561,9 @@ } }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -6540,12 +6613,12 @@ } }, "minimatch": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", - "integrity": "sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" } }, "ms": { @@ -6578,15 +6651,6 @@ "has-flag": "^4.0.0" } }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, "yargs": { "version": "16.2.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", @@ -6653,9 +6717,9 @@ "optional": true }, "nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", + "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true }, "nanomatch": { @@ -6684,9 +6748,9 @@ "dev": true }, "next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", "dev": true }, "nice-try": { @@ -6709,9 +6773,9 @@ } }, "nock": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.4.tgz", - "integrity": "sha512-8GPznwxcPNCH/h8B+XZcKjYPXnUV5clOKCjAqyjsiqA++MpNx9E9+t8YPp0MbThO+KauRo7aZJ1WuIZmOrT2Ug==", + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.2.tgz", + "integrity": "sha512-PcBHuvl9i6zfaJ50A7LS55oU+nFLv8htXIhffJO+FxyfibdZ4jEvd9kTuvkrJireBFIGMZ+oUIRpMK5gU9h//g==", "dev": true, "requires": { "debug": "^4.1.0", @@ -6749,9 +6813,9 @@ } }, "node-releases": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz", + "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==", "dev": true }, "node-version": { @@ -7205,9 +7269,9 @@ } }, "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", "optional": true }, "object-inspect": { @@ -7639,6 +7703,17 @@ "arr-diff": "^4.0.0", "arr-union": "^3.1.0", "extend-shallow": "^3.0.2" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "^0.1.0" + } + } } }, "posix-character-classes": { @@ -7687,9 +7762,9 @@ "dev": true }, "proto3-json-serializer": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.8.tgz", - "integrity": "sha512-ACilkB6s1U1gWnl5jtICpnDai4VCxmI9GFxuEaYdxtDG2oVI3sVFIUsvUZcQbJgtPM6p+zqKbjTKQZp6Y4FpQw==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.7.tgz", + "integrity": "sha512-91Yn0rgRL/diKZABrQIVnOm7k3HttbxfP5nm0xMjHctDbCNqaLkGc6O25bwc5Y7WmpxfUdYfeidbhWoyO1aJfA==", "optional": true, "requires": { "protobufjs": "^6.11.2" @@ -8451,9 +8526,9 @@ } }, "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", + "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, "sinon": { "version": "13.0.2", @@ -8467,6 +8542,17 @@ "diff": "^5.0.0", "nise": "^5.1.1", "supports-color": "^7.2.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } } }, "sinon-chai": { @@ -9128,9 +9214,9 @@ "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" }, "ts-node": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz", - "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.5.0.tgz", + "integrity": "sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw==", "dev": true, "requires": { "@cspotcode/source-map-support": "0.7.0", @@ -9402,8 +9488,7 @@ "uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "optional": true + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" }, "v8-compile-cache": { "version": "2.3.0", @@ -9670,9 +9755,9 @@ "dev": true }, "workerpool": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", - "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", "dev": true }, "wrap-ansi": { @@ -9730,9 +9815,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", - "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", "dev": true, "requires": { "cliui": "^7.0.2", @@ -9745,9 +9830,9 @@ }, "dependencies": { "yargs-parser": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", - "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==", "dev": true } } diff --git a/package.json b/package.json index 2f557f9a95..135c6d5d84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-admin", - "version": "10.1.0", + "version": "10.2.0", "description": "Firebase admin SDK for Node.js", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", @@ -71,12 +71,18 @@ "auth": [ "lib/auth" ], + "eventarc": [ + "lib/eventarc" + ], "database": [ "lib/database" ], "firestore": [ "lib/firestore" ], + "functions": [ + "lib/functions" + ], "installations": [ "lib/installations" ], @@ -121,10 +127,18 @@ "require": "./lib/database/index.js", "import": "./lib/esm/database/index.js" }, + "./eventarc": { + "require": "./lib/eventarc/index.js", + "import": "./lib/esm/eventarc/index.js" + }, "./firestore": { "require": "./lib/firestore/index.js", "import": "./lib/esm/firestore/index.js" }, + "./functions": { + "require": "./lib/functions/index.js", + "import": "./lib/esm/functions/index.js" + }, "./installations": { "require": "./lib/installations/index.js", "import": "./lib/esm/installations/index.js" @@ -159,13 +173,14 @@ } }, "dependencies": { - "@firebase/database-compat": "^0.1.1", - "@firebase/database-types": "^0.9.3", + "@firebase/database-compat": "^0.1.8", + "@firebase/database-types": "^0.9.7", "@types/node": ">=12.12.47", "dicer": "^0.3.0", "jsonwebtoken": "^8.5.1", "jwks-rsa": "^2.0.2", - "node-forge": "^1.3.1" + "node-forge": "^1.3.1", + "uuid": "^8.3.2" }, "optionalDependencies": { "@google-cloud/firestore": "^4.15.1", @@ -190,11 +205,13 @@ "@types/request-promise": "^4.1.41", "@types/sinon": "^10.0.2", "@types/sinon-chai": "^3.0.0", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "bcrypt": "^5.0.0", "chai": "^4.2.0", "chai-as-promised": "^7.0.0", + "chai-exclude": "^2.1.0", "chalk": "^4.1.1", "child-process-promise": "^2.2.1", "del": "^6.0.0", @@ -207,7 +224,7 @@ "http-message-parser": "^0.0.34", "lodash": "^4.17.15", "minimist": "^1.2.6", - "mocha": "^9.1.2", + "mocha": "^10.0.0", "mz": "^2.7.0", "nock": "^13.0.0", "npm-run-all": "^4.1.5", diff --git a/src/app-check/app-check-api-client-internal.ts b/src/app-check/app-check-api-client-internal.ts index e9ba97ab97..aae736cf63 100644 --- a/src/app-check/app-check-api-client-internal.ts +++ b/src/app-check/app-check-api-client-internal.ts @@ -26,7 +26,7 @@ import * as validator from '../utils/validator'; import { AppCheckToken } from './app-check-api' // App Check backend constants -const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1beta/projects/{projectId}/apps/{appId}:exchangeCustomToken'; +const FIREBASE_APP_CHECK_V1_API_URL_FORMAT = 'https://firebaseappcheck.googleapis.com/v1/projects/{projectId}/apps/{appId}:exchangeCustomToken'; const FIREBASE_APP_CHECK_CONFIG_HEADERS = { 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` @@ -144,7 +144,7 @@ export class AppCheckApiClient { * @returns An AppCheckToken instance. */ private toAppCheckToken(resp: HttpResponse): AppCheckToken { - const token = resp.data.attestationToken; + const token = resp.data.token; // `ttl` is a string with the suffix "s" preceded by the number of seconds, // with nanoseconds expressed as fractional seconds. const ttlMillis = this.stringToMilliseconds(resp.data.ttl); diff --git a/src/app-check/token-generator.ts b/src/app-check/token-generator.ts index c2a76c8640..2cfe3ca14c 100644 --- a/src/app-check/token-generator.ts +++ b/src/app-check/token-generator.ts @@ -31,7 +31,7 @@ const ONE_MINUTE_IN_MILLIS = ONE_MINUTE_IN_SECONDS * 1000; const ONE_DAY_IN_MILLIS = 24 * 60 * 60 * 1000; // Audience to use for Firebase App Check Custom tokens -const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService'; /** * Class for generating Firebase App Check tokens. diff --git a/src/app-check/token-verifier.ts b/src/app-check/token-verifier.ts index 313d7945d3..6c6503f05d 100644 --- a/src/app-check/token-verifier.ts +++ b/src/app-check/token-verifier.ts @@ -26,7 +26,7 @@ import { DecodedAppCheckToken } from './app-check-api' import { App } from '../app'; const APP_CHECK_ISSUER = 'https://firebaseappcheck.googleapis.com/'; -const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1beta/jwks'; +const JWKS_URL = 'https://firebaseappcheck.googleapis.com/v1/jwks'; /** * Class for verifying Firebase App Check tokens. diff --git a/src/app/credential-internal.ts b/src/app/credential-internal.ts index 0817236038..28442ba8f7 100644 --- a/src/app/credential-internal.ts +++ b/src/app/credential-internal.ts @@ -33,6 +33,7 @@ const GOOGLE_AUTH_TOKEN_PATH = '/o/oauth2/token'; const GOOGLE_METADATA_SERVICE_HOST = 'metadata.google.internal'; const GOOGLE_METADATA_SERVICE_TOKEN_PATH = '/computeMetadata/v1/instance/service-accounts/default/token'; const GOOGLE_METADATA_SERVICE_PROJECT_ID_PATH = '/computeMetadata/v1/project/project-id'; +const GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH = '/computeMetadata/v1/instance/service-accounts/default/email'; const configDir = (() => { // Windows has a dedicated low-rights location for apps at ~/Application Data @@ -197,6 +198,7 @@ export class ComputeEngineCredential implements Credential { private readonly httpClient = new HttpClient(); private readonly httpAgent?: Agent; private projectId?: string; + private accountId?: string; constructor(httpAgent?: Agent) { this.httpAgent = httpAgent; @@ -226,6 +228,25 @@ export class ComputeEngineCredential implements Credential { }); } + public getServiceAccountEmail(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + + const request = this.buildRequest(GOOGLE_METADATA_SERVICE_ACCOUNT_ID_PATH); + return this.httpClient.send(request) + .then((resp) => { + this.accountId = resp.text!; + return this.accountId; + }) + .catch((err) => { + const detail: string = (err instanceof HttpError) ? getDetailFromResponse(err.response) : err.message; + throw new FirebaseAppError( + AppErrorCodes.INVALID_CREDENTIAL, + `Failed to determine service account email: ${detail}`); + }); + } + private buildRequest(urlPath: string): HttpRequestConfig { return { method: 'GET', diff --git a/src/auth/auth-api-request.ts b/src/auth/auth-api-request.ts index 13018337da..a962a4f719 100644 --- a/src/auth/auth-api-request.ts +++ b/src/auth/auth-api-request.ts @@ -59,7 +59,7 @@ export const RESERVED_CLAIMS = [ /** List of supported email action request types. */ export const EMAIL_ACTION_REQUEST_TYPES = [ - 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', + 'PASSWORD_RESET', 'VERIFY_EMAIL', 'EMAIL_SIGNIN', 'VERIFY_AND_CHANGE_EMAIL', ]; /** Maximum allowed number of characters in the custom claims payload. */ @@ -817,6 +817,11 @@ const FIREBASE_AUTH_GET_OOB_CODE = new ApiSettings('/accounts:sendOobCode', 'POS AuthClientErrorCode.INVALID_EMAIL, ); } + if (typeof request.newEmail !== 'undefined' && !validator.isEmail(request.newEmail)) { + throw new FirebaseAuthError( + AuthClientErrorCode.INVALID_NEW_EMAIL, + ); + } if (EMAIL_ACTION_REQUEST_TYPES.indexOf(request.requestType) === -1) { throw new FirebaseAuthError( AuthClientErrorCode.INVALID_ARGUMENT, @@ -1599,12 +1604,19 @@ export abstract class AbstractAuthRequestHandler { * @param actionCodeSettings - The optional action code setings which defines whether * the link is to be handled by a mobile app and the additional state information to be passed in the * deep link, etc. Required when requestType == 'EMAIL_SIGNIN' + * @param newEmail - The email address the account is being updated to. + * Required only for VERIFY_AND_CHANGE_EMAIL requests. * @returns A promise that resolves with the email action link. */ public getEmailActionLink( requestType: string, email: string, - actionCodeSettings?: ActionCodeSettings): Promise { - let request = { requestType, email, returnOobLink: true }; + actionCodeSettings?: ActionCodeSettings, newEmail?: string): Promise { + let request = { + requestType, + email, + returnOobLink: true, + ...(typeof newEmail !== 'undefined') && { newEmail }, + }; // ActionCodeSettings required for email link sign-in to determine the url where the sign-in will // be completed. if (typeof actionCodeSettings === 'undefined' && requestType === 'EMAIL_SIGNIN') { @@ -1623,6 +1635,14 @@ export abstract class AbstractAuthRequestHandler { return Promise.reject(e); } } + if (requestType === 'VERIFY_AND_CHANGE_EMAIL' && typeof newEmail === 'undefined') { + return Promise.reject( + new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + "`newEmail` is required when `requestType` === 'VERIFY_AND_CHANGE_EMAIL'", + ), + ); + } return this.invokeRequestHandler(this.getAuthUrlBuilder(), FIREBASE_AUTH_GET_OOB_CODE, request) .then((response: any) => { // Return the link. diff --git a/src/auth/base-auth.ts b/src/auth/base-auth.ts index 7119dd2514..6f77e088f8 100644 --- a/src/auth/base-auth.ts +++ b/src/auth/base-auth.ts @@ -834,6 +834,35 @@ export abstract class BaseAuth { return this.authRequestHandler.getEmailActionLink('VERIFY_EMAIL', email, actionCodeSettings); } + /** + * Generates an out-of-band email action link to verify the user's ownership + * of the specified email. The {@link ActionCodeSettings} object provided + * as an argument to this method defines whether the link is to be handled by a + * mobile app or browser along with additional state information to be passed in + * the deep link, etc. + * + * @param email - The current email account. + * @param newEmail - The email address the account is being updated to. + * @param actionCodeSettings - The action + * code settings. If specified, the state/continue URL is set as the + * "continueUrl" parameter in the email verification link. The default email + * verification landing page will use this to display a link to go back to + * the app if it is installed. + * If the actionCodeSettings is not specified, no URL is appended to the + * action URL. + * The state URL provided must belong to a domain that is authorized + * in the console, or an error will be thrown. + * Mobile app redirects are only applicable if the developer configures + * and accepts the Firebase Dynamic Links terms of service. + * The Android package name and iOS bundle ID are respected only if they + * are configured in the same Firebase Auth project. + * @returns A promise that resolves with the generated link. + */ + public generateVerifyAndChangeEmailLink(email: string, newEmail: string, + actionCodeSettings?: ActionCodeSettings): Promise { + return this.authRequestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, newEmail); + } + /** * Generates the out of band email action link to verify the user's ownership * of the specified email. The {@link ActionCodeSettings} object provided diff --git a/src/eventarc/cloudevent.ts b/src/eventarc/cloudevent.ts new file mode 100644 index 0000000000..9fa0749f2e --- /dev/null +++ b/src/eventarc/cloudevent.ts @@ -0,0 +1,95 @@ + +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * A CloudEvent version. + */ +export type CloudEventVersion = '1.0'; + +/** + * A CloudEvent describes event data. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md + */ +export interface CloudEvent { + + /** + * Identifier for the event. If not provided, it is auto-populated with a UUID. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#id + */ + id?: string; + + /** + * Identifies the context in which an event happened. If not provided, the value of `EVENTARC_CLOUD_EVENT_SOURCE` + * environment variable is used and if that is not set, a validation error is thrown. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#source-1 + */ + source?: string; + + /** + * The version of the CloudEvents specification which the event uses. If not provided, is set to `1.0` -- + * the only supported value. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#specversion + */ + specversion?: CloudEventVersion; + + /** + * Type of the event. Should be prefixed with a reverse-DNS name (`com.my-org.v1.something.happended`). + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#type + */ + type: string; + + /** + * Subject (context) of the event in the context of the event producer. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#subject + */ + subject?: string; + + /** + * MIME type of the data being sent with the event in the `data` field. Only `application/json` and `text/plain` + * are currently supported. If not specified, it is automatically inferred from the type of provided data. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#datacontenttype + */ + datacontenttype?: string; + + /** + * Timestamp of the event. Must be in ISO time format. If not specified, current time (at the moment of publishing) + * is used. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#time + */ + time?: string; + + /** + * Data payload of the event. Objects are stringified with JSON and strings are be passed along as-is. + */ + data?: object | string; + + /** + * Custom attributes/extensions. Must be strings. Added to the event as is. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#extension-context-attributes + */ + [key: string]: any; + } diff --git a/src/eventarc/eventarc-client-internal.ts b/src/eventarc/eventarc-client-internal.ts new file mode 100644 index 0000000000..ce02967c93 --- /dev/null +++ b/src/eventarc/eventarc-client-internal.ts @@ -0,0 +1,160 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as validator from '../utils/validator'; +import { FirebaseEventarcError, toCloudEventProtoFormat } from './eventarc-utils'; +import { App } from '../app'; +import { Channel } from './eventarc'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient +} from '../utils/api-request'; +import { FirebaseApp } from '../app/firebase-app'; +import * as utils from '../utils'; +import { PrefixedFirebaseError } from '../utils/error'; +import { CloudEvent } from './cloudevent'; + +const EVENTARC_API = 'https://eventarcpublishing.googleapis.com/v1'; +const FIREBASE_VERSION_HEADER = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, +}; +const CHANNEL_NAME_REGEX = /^(projects\/([^/]+)\/)?locations\/([^/]+)\/channels\/([^/]+)$/; +const DEFAULT_CHANNEL_REGION = 'us-central1'; + +/** + * Class that facilitates sending requests to the Eventarc backend API. + * + * @internal + */ +export class EventarcApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + private readonly resolvedChannelName: Promise; + + constructor(private readonly app: App, private readonly channel: Channel) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Channel() must be a valid Eventarc service instance.'); + } + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + this.resolvedChannelName = this.resolveChannelName(channel.name); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseEventarcError( + 'unknown-error', + 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + } + this.projectId = projectId; + return projectId; + }); + } + + /** + * Publishes provided events to this channel. If channel was created with `allowedEventsTypes` and event type + * is not on that list, the event is ignored. + * + * The following CloudEvent fields are auto-populated if not set: + * * specversion - `1.0` + * * id - uuidv4() + * * source - populated with `process.env.EVENTARC_CLOUD_EVENT_SOURCE` and + * if not set an error is thrown. + * + * @param events - CloudEvent to publish to the channel. + */ + public async publish(events: CloudEvent | CloudEvent[]): Promise { + if (!Array.isArray(events)) { + events = [events as CloudEvent]; + } + return this.publishToEventarcApi( + await this.resolvedChannelName, + events + .filter(e => typeof this.channel.allowedEventTypes === 'undefined' || + this.channel.allowedEventTypes.includes(e.type)) + .map(toCloudEventProtoFormat)); + } + + private async publishToEventarcApi(channel:string, events: CloudEvent[]): Promise { + if (events.length === 0) { + return; + } + const request: HttpRequestConfig = { + method: 'POST', + url: `${this.getEventarcHost()}/${channel}:publishEvents`, + data: JSON.stringify({ events }), + }; + return this.sendRequest(request); + } + + private sendRequest(request: HttpRequestConfig): Promise { + request.headers = FIREBASE_VERSION_HEADER; + return this.httpClient.send(request) + .then(() => undefined) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + return new FirebaseEventarcError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + private resolveChannelName(name: string): Promise { + if (!name.includes('/')) { + const location = DEFAULT_CHANNEL_REGION; + const channelId = name; + return this.resolveChannelNameProjectId(location, channelId); + } else { + const match = CHANNEL_NAME_REGEX.exec(name); + if (match === null || match.length < 4) { + throw new FirebaseEventarcError('invalid-argument', 'Invalid channel name format.'); + } + const projectId = match[2]; + const location = match[3]; + const channelId = match[4]; + if (validator.isNonEmptyString(projectId)) { + return Promise.resolve(`projects/${projectId}/locations/${location}/channels/${channelId}`); + } else { + return this.resolveChannelNameProjectId(location, channelId); + } + } + } + + private async resolveChannelNameProjectId(location: string, channelId: string): Promise { + const projectId = await this.getProjectId(); + return `projects/${projectId}/locations/${location}/channels/${channelId}`; + } + + private getEventarcHost(): string { + return process.env.CLOUD_EVENTARC_EMULATOR_HOST ?? EVENTARC_API; + } +} diff --git a/src/eventarc/eventarc-utils.ts b/src/eventarc/eventarc-utils.ts new file mode 100644 index 0000000000..6bf6531285 --- /dev/null +++ b/src/eventarc/eventarc-utils.ts @@ -0,0 +1,138 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PrefixedFirebaseError } from '../utils/error'; +import { CloudEvent } from './cloudevent'; +import { v4 as uuid } from 'uuid'; +import * as validator from '../utils/validator'; + +// List of CloudEvent properties that are handled "by hand" and should be skipped by +// automatic attribute copy. +const TOP_LEVEL_CE_ATTRS: string[] = + ['id', 'type', 'specversion', 'source', 'data', 'time', 'datacontenttype', 'subject']; + +export type EventarcErrorCode = 'unknown-error' | 'invalid-argument' + +/** + * Firebase Eventarc error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseEventarcError extends PrefixedFirebaseError { + constructor(code: EventarcErrorCode, message: string) { + super('eventarc', code, message); + } +} + +export function toCloudEventProtoFormat(ce: CloudEvent): any { + const source = ce.source ?? process.env.EVENTARC_CLOUD_EVENT_SOURCE; + if (typeof source === 'undefined' || !validator.isNonEmptyString(source)) { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'source' is required."); + } + if (!validator.isNonEmptyString(ce.type)) { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'type' is required."); + } + const out: Record = { + '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', + 'id': ce.id ?? uuid(), + 'type': ce.type, + 'specVersion': ce.specversion ?? '1.0', + 'source': source + } + + if (typeof ce.time !== 'undefined') { + if (!validator.isISODateString(ce.time)) { + throw new FirebaseEventarcError( + 'invalid-argument', "CloudEvent 'tyme' must be in ISO date format."); + } + setAttribute(out, 'time', { + 'ceTimestamp': ce.time + }); + } else { + setAttribute(out, 'time', { + 'ceTimestamp': new Date().toISOString() + }); + } + if (typeof ce.datacontenttype !== 'undefined') { + if (!validator.isNonEmptyString(ce.datacontenttype)) { + throw new FirebaseEventarcError( + 'invalid-argument', + "CloudEvent 'datacontenttype' if specified must be non-empty string."); + } + setAttribute(out, 'datacontenttype', { + 'ceString': ce.datacontenttype + }); + } + if (ce.subject) { + if (!validator.isNonEmptyString(ce.subject)) { + throw new FirebaseEventarcError( + 'invalid-argument', + "CloudEvent 'subject' if specified must be non-empty string."); + } + setAttribute(out, 'subject', { + 'ceString': ce.subject + }); + } + + if (typeof ce.data === 'undefined') { + throw new FirebaseEventarcError('invalid-argument', "CloudEvent 'data' is required."); + } + if (validator.isObject(ce.data)) { + out['textData'] = JSON.stringify(ce.data); + if (!ce.datacontenttype) { + setAttribute(out, 'datacontenttype', { + 'ceString': 'application/json' + }); + } + } else if (validator.isNonEmptyString(ce.data)) { + out['textData'] = ce.data; + if (!ce.datacontenttype) { + setAttribute(out, 'datacontenttype', { + 'ceString': 'text/plain' + }); + } + } else { + throw new FirebaseEventarcError( + 'invalid-argument', + `CloudEvent 'data' must be string or an object (which are converted to JSON), got '${typeof ce.data}'.`); + } + + for (const attr in ce) { + if (TOP_LEVEL_CE_ATTRS.includes(attr)) { + continue; + } + if (!validator.isNonEmptyString(ce[attr])) { + throw new FirebaseEventarcError( + 'invalid-argument', + `CloudEvent extension attributes ('${attr}') must be string.`); + } + setAttribute(out, attr, { + 'ceString': ce[attr] + }); + } + + return out; +} + +function setAttribute(event: any, attr: string, value: any): void { + if (!Object.prototype.hasOwnProperty.call(event, 'attributes')) { + event.attributes = {}; + } + event['attributes'][attr] = value; +} diff --git a/src/eventarc/eventarc.ts b/src/eventarc/eventarc.ts new file mode 100644 index 0000000000..a1edc4a011 --- /dev/null +++ b/src/eventarc/eventarc.ts @@ -0,0 +1,194 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import * as validator from '../utils/validator'; +import { FirebaseEventarcError } from './eventarc-utils'; +import { CloudEvent } from './cloudevent'; +import { EventarcApiClient } from './eventarc-client-internal'; + +/** + * Channel options interface. + */ +export interface ChannelOptions { + /** + * An array of allowed event types. If specified, publishing events of + * unknown types is a no op. When not provided, no event filtering is + * performed. + */ + allowedEventTypes?: string[] | string | undefined +} + +/** + * Eventarc service bound to the provided app. + */ +export class Eventarc { + + private readonly appInternal: App; + + /** + * @internal + */ + constructor(app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Eventarc() must be a valid Firebase app instance.', + ); + } + + this.appInternal = app; + } + + /** + * The {@link firebase-admin.app#App} associated with the current Eventarc service + * instance. + * + * @example + * ```javascript + * var app = eventarc.app; + * ``` + */ + get app(): App { + return this.appInternal; + } + + /** + * Creates a reference to the Eventarc channel using the provided channel resource name. + * The channel resource name can be either: + * + * - A fully qualified channel resource name: + * `projects/{project}/locations/{location}/channels/{channel-id}` + * + * - A partial resource name with location and channel ID, in which case + * the runtime project ID of the function is used: + * `locations/{location}/channels/{channel-id}` + * + * - A partial channel ID, in which case the runtime project ID of the + * function and `us-central1` as location is used: + * `{channel-id}` + * + * @param name - Channel resource name. + * @param options - (optional) additional channel options + * @returns An Eventarc channel reference for publishing events. + */ + public channel(name: string, options?: ChannelOptions): Channel; + + /** + * Create a reference to the default Firebase channel: + * `locations/us-central1/channels/firebase` + * + * @param options - (optional) additional channel options + * @returns Eventarc channel reference for publishing events. + */ + public channel(options?: ChannelOptions): Channel; + + public channel(nameOrOptions?: string | ChannelOptions, options?: ChannelOptions): Channel { + let channel: string; + let opts: ChannelOptions; + if (validator.isNonEmptyString(nameOrOptions)) { + channel = nameOrOptions; + } else { + channel = 'locations/us-central1/channels/firebase'; + } + + if (validator.isNonNullObject(nameOrOptions)) { + opts = nameOrOptions as ChannelOptions; + } else { + opts = options as ChannelOptions; + } + let allowedEventTypes : string[] | undefined = undefined; + if (typeof opts?.allowedEventTypes === 'string') { + allowedEventTypes = opts.allowedEventTypes.split(','); + } else if (validator.isArray(opts?.allowedEventTypes)) { + allowedEventTypes = opts?.allowedEventTypes as string[]; + } else if (typeof opts?.allowedEventTypes !== 'undefined') { + throw new FirebaseEventarcError( + 'invalid-argument', + 'AllowedEventTypes must be either an array of strings or a comma separated string.', + ); + } + return new Channel(this, channel, allowedEventTypes); + } +} + +/** + * Eventarc Channel. + */ +export class Channel { + private readonly eventarcInternal: Eventarc; + private nameInternal: string; + + /** + * List of event types allowed by this channel for publishing. Other event types are ignored. + */ + public readonly allowedEventTypes?: string[] + + private readonly client: EventarcApiClient; + + /** + * @internal + */ + constructor(eventarc: Eventarc, name: string, allowedEventTypes?: string[]) { + if (!validator.isNonNullObject(eventarc)) { + throw new FirebaseEventarcError( + 'invalid-argument', + 'First argument passed to Channel() must be a valid Eventarc service instance.', + ); + } + if (!validator.isNonEmptyString(name)) { + throw new FirebaseEventarcError( + 'invalid-argument', 'name is required.', + ); + } + + this.nameInternal = name; + this.eventarcInternal = eventarc; + this.allowedEventTypes = allowedEventTypes; + this.client = new EventarcApiClient(eventarc.app, this); + } + + /** + * The {@link firebase-admin.eventarc#Eventarc} service instance associated with the current `Channel`. + * + * @example + * ```javascript + * var app = channel.eventarc; + * ``` + */ + get eventarc(): Eventarc { + return this.eventarcInternal; + } + + /** + * The channel name as provided during channel creation. If it was not specifed, the default channel name is returned + * ('locations/us-central1/channels/firebase'). + */ + get name(): string { + return this.nameInternal; + } + + /** + * Publishes provided events to this channel. If channel was created with `allowedEventTypes` and event type is not + * on that list, the event is ignored. + * + * @param events - CloudEvent to publish to the channel. + */ + public publish(events: CloudEvent | CloudEvent[]): Promise { + return this.client.publish(events); + } +} diff --git a/src/eventarc/index.ts b/src/eventarc/index.ts new file mode 100644 index 0000000000..d1e6fc79bd --- /dev/null +++ b/src/eventarc/index.ts @@ -0,0 +1,65 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Eventarc. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; + +import { Eventarc } from './eventarc'; + +export { CloudEvent, CloudEventVersion } from './cloudevent'; +export { Eventarc, Channel, ChannelOptions } from './eventarc'; + +/** + * Gets the {@link Eventarc} service for the default app or a given app. + * + * `getEventarc()` can be called with no arguments to access the default + * app's `Eventarc` service or as `getEventarc(app)` to access the + * `Eventarc` service associated with specific app. + * + * @example + * ```javascript + * // Get the Eventarc service for the default app + * const defaultEventarc = getEventarc(); + * ``` + * + * @example + * ```javascript + * // Get the Eventarc service for a given app + * const otherEventarc = getEventarc(otherApp); + * ``` + * + * @param app - Optional app whose `Eventarc` service will be returned. + * If not provided, the default `Eventarc` service will be returned. + * + * @returns The default `Eventarc` service if no + * app is provided or the `Eventarc` service associated with the provided + * app. + */ +export function getEventarc(app?: App): Eventarc { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('eventarc', (app) => new Eventarc(app)); +} diff --git a/src/functions/functions-api-client-internal.ts b/src/functions/functions-api-client-internal.ts new file mode 100644 index 0000000000..36b7bf99c2 --- /dev/null +++ b/src/functions/functions-api-client-internal.ts @@ -0,0 +1,334 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { + HttpRequestConfig, HttpClient, HttpError, AuthorizedHttpClient +} from '../utils/api-request'; +import { PrefixedFirebaseError } from '../utils/error'; +import * as utils from '../utils/index'; +import * as validator from '../utils/validator'; +import { TaskOptions } from './functions-api'; + +const CLOUD_TASKS_API_URL_FORMAT = 'https://cloudtasks.googleapis.com/v2/projects/{projectId}/locations/{locationId}/queues/{resourceId}/tasks'; +const FIREBASE_FUNCTION_URL_FORMAT = 'https://{locationId}-{projectId}.cloudfunctions.net/{resourceId}'; + +const FIREBASE_FUNCTIONS_CONFIG_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}` +}; + +// Default canonical location ID of the task queue. +const DEFAULT_LOCATION = 'us-central1'; + +/** + * Class that facilitates sending requests to the Firebase Functions backend API. + * + * @internal + */ +export class FunctionsApiClient { + private readonly httpClient: HttpClient; + private projectId?: string; + private accountId?: string; + + constructor(private readonly app: App) { + if (!validator.isNonNullObject(app) || !('options' in app)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + 'First argument passed to getFunctions() must be a valid Firebase app instance.'); + } + this.httpClient = new AuthorizedHttpClient(app as FirebaseApp); + } + + /** + * Creates a task and adds it to a queue. + * + * @param data - The data payload of the task. + * @param functionName - The functionName of the queue. + * @param extensionId - Optional canonical ID of the extension. + * @param opts - Optional options when enqueuing a new task. + */ + public enqueue(data: any, functionName: string, extensionId?: string, opts?: TaskOptions): Promise { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a non empty string'); + } + + const task = this.validateTaskOptions(data, opts); + let resources: utils.ParsedResource; + try { + resources = utils.parseResourceName(functionName, 'functions'); + } + catch (err) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Function name must be a single string or a qualified resource name'); + } + + if (typeof extensionId !== 'undefined' && validator.isNonEmptyString(extensionId)) { + resources.resourceId = `ext-${extensionId}-${resources.resourceId}`; + } + + return this.getUrl(resources, CLOUD_TASKS_API_URL_FORMAT) + .then((serviceUrl) => { + return this.updateTaskPayload(task, resources) + .then((task) => { + const request: HttpRequestConfig = { + method: 'POST', + url: serviceUrl, + headers: FIREBASE_FUNCTIONS_CONFIG_HEADERS, + data: { + task, + } + }; + return this.httpClient.send(request); + }) + }) + .then(() => { + return; + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + private getUrl(resourceName: utils.ParsedResource, urlFormat: string): Promise { + let { locationId } = resourceName; + const { projectId, resourceId } = resourceName; + if (typeof locationId === 'undefined' || !validator.isNonEmptyString(locationId)) { + locationId = DEFAULT_LOCATION; + } + return Promise.resolve() + .then(() => { + if (typeof projectId !== 'undefined' && validator.isNonEmptyString(projectId)) { + return projectId; + } + return this.getProjectId(); + }) + .then((projectId) => { + const urlParams = { + projectId, + locationId, + resourceId, + }; + // Formats a string of form 'project/{projectId}/{api}' and replaces + // with corresponding arguments {projectId: '1234', api: 'resource'} + // and returns output: 'project/1234/resource'. + return utils.formatString(urlFormat, urlParams); + }); + } + + private getProjectId(): Promise { + if (this.projectId) { + return Promise.resolve(this.projectId); + } + return utils.findProjectId(this.app) + .then((projectId) => { + if (!validator.isNonEmptyString(projectId)) { + throw new FirebaseFunctionsError( + 'unknown-error', + 'Failed to determine project ID. Initialize the ' + + 'SDK with service account credentials or set project ID as an app option. ' + + 'Alternatively, set the GOOGLE_CLOUD_PROJECT environment variable.'); + } + this.projectId = projectId; + return projectId; + }); + } + + private getServiceAccount(): Promise { + if (this.accountId) { + return Promise.resolve(this.accountId); + } + return utils.findServiceAccountEmail(this.app) + .then((accountId) => { + if (!validator.isNonEmptyString(accountId)) { + throw new FirebaseFunctionsError( + 'unknown-error', + 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.'); + } + this.accountId = accountId; + return accountId; + }); + } + + private validateTaskOptions(data: any, opts?: TaskOptions): Task { + const task: Task = { + httpRequest: { + url: '', + oidcToken: { + serviceAccountEmail: '', + }, + body: Buffer.from(JSON.stringify({ data })).toString('base64'), + headers: { 'Content-Type': 'application/json' } + } + } + + if (typeof opts !== 'undefined') { + if (!validator.isNonNullObject(opts)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'TaskOptions must be a non-null object'); + } + if ('scheduleTime' in opts && 'scheduleDelaySeconds' in opts) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'Both scheduleTime and scheduleDelaySeconds are provided. ' + + 'Only one value should be set.'); + } + if ('scheduleTime' in opts && typeof opts.scheduleTime !== 'undefined') { + if (!(opts.scheduleTime instanceof Date)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'scheduleTime must be a valid Date object.'); + } + task.scheduleTime = opts.scheduleTime.toISOString(); + } + if ('scheduleDelaySeconds' in opts && typeof opts.scheduleDelaySeconds !== 'undefined') { + if (!validator.isNumber(opts.scheduleDelaySeconds) || opts.scheduleDelaySeconds < 0) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'scheduleDelaySeconds must be a non-negative duration in seconds.'); + } + const date = new Date(); + date.setSeconds(date.getSeconds() + opts.scheduleDelaySeconds); + task.scheduleTime = date.toISOString(); + } + if (typeof opts.dispatchDeadlineSeconds !== 'undefined') { + if (!validator.isNumber(opts.dispatchDeadlineSeconds) || opts.dispatchDeadlineSeconds < 15 + || opts.dispatchDeadlineSeconds > 1800) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.'); + } + task.dispatchDeadline = `${opts.dispatchDeadlineSeconds}s`; + } + if (typeof opts.uri !== 'undefined') { + if (!validator.isURL(opts.uri)) { + throw new FirebaseFunctionsError( + 'invalid-argument', 'uri must be a valid URL string.'); + } + task.httpRequest.url = opts.uri; + } + } + return task; + } + + private updateTaskPayload(task: Task, resources: utils.ParsedResource): Promise { + return Promise.resolve() + .then(() => { + if (validator.isNonEmptyString(task.httpRequest.url)) { + return task.httpRequest.url; + } + return this.getUrl(resources, FIREBASE_FUNCTION_URL_FORMAT); + }) + .then((functionUrl) => { + return this.getServiceAccount() + .then((account) => { + task.httpRequest.oidcToken.serviceAccountEmail = account; + task.httpRequest.url = functionUrl; + return task; + }) + }); + } + + private toFirebaseError(err: HttpError): PrefixedFirebaseError { + if (err instanceof PrefixedFirebaseError) { + return err; + } + + const response = err.response; + if (!response.isJson()) { + return new FirebaseFunctionsError( + 'unknown-error', + `Unexpected response with status: ${response.status} and body: ${response.text}`); + } + + const error: Error = (response.data as ErrorResponse).error || {}; + let code: FunctionsErrorCode = 'unknown-error'; + if (error.status && error.status in FUNCTIONS_ERROR_CODE_MAPPING) { + code = FUNCTIONS_ERROR_CODE_MAPPING[error.status]; + } + const message = error.message || `Unknown server error: ${response.text}`; + return new FirebaseFunctionsError(code, message); + } +} + +interface ErrorResponse { + error?: Error; +} + +interface Error { + code?: number; + message?: string; + status?: string; +} + +interface Task { + // A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional + // digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z". + scheduleTime?: string; + // A duration in seconds with up to nine fractional digits, terminated by 's'. Example: "3.5s". + dispatchDeadline?: string; + httpRequest: { + url: string; + oidcToken: { + serviceAccountEmail: string; + }; + // A base64-encoded string. + body: string; + headers: { [key: string]: string }; + }; +} + +export const FUNCTIONS_ERROR_CODE_MAPPING: { [key: string]: FunctionsErrorCode } = { + ABORTED: 'aborted', + INVALID_ARGUMENT: 'invalid-argument', + INVALID_CREDENTIAL: 'invalid-credential', + INTERNAL: 'internal-error', + FAILED_PRECONDITION: 'failed-precondition', + PERMISSION_DENIED: 'permission-denied', + UNAUTHENTICATED: 'unauthenticated', + NOT_FOUND: 'not-found', + UNKNOWN: 'unknown-error', +}; + +export type FunctionsErrorCode = + 'aborted' + | 'invalid-argument' + | 'invalid-credential' + | 'internal-error' + | 'failed-precondition' + | 'permission-denied' + | 'unauthenticated' + | 'not-found' + | 'unknown-error'; + +/** + * Firebase Functions error code structure. This extends PrefixedFirebaseError. + * + * @param code - The error code. + * @param message - The error message. + * @constructor + */ +export class FirebaseFunctionsError extends PrefixedFirebaseError { + constructor(code: FunctionsErrorCode, message: string) { + super('functions', code, message); + + /* tslint:disable:max-line-length */ + // Set the prototype explicitly. See the following link for more details: + // https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + /* tslint:enable:max-line-length */ + (this as any).__proto__ = FirebaseFunctionsError.prototype; + } +} diff --git a/src/functions/functions-api.ts b/src/functions/functions-api.ts new file mode 100644 index 0000000000..60351211e9 --- /dev/null +++ b/src/functions/functions-api.ts @@ -0,0 +1,72 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Interface representing task options with delayed delivery. + */ +export interface DelayDelivery { + /** + * The duration of delay of the time when the task is scheduled to be attempted or retried. + * This delay is added to the current time. + */ + scheduleDelaySeconds?: number; + /** @alpha */ + scheduleTime?: never; +} + +/** + * Interface representing task options with absolute delivery. + */ +export interface AbsoluteDelivery { + /** + * The time when the task is scheduled to be attempted or retried. + */ + scheduleTime?: Date; + /** @alpha */ + scheduleDelaySeconds?: never; +} + +/** + * Type representing delivery schedule options. + * `DeliverySchedule` is a union type of {@link DelayDelivery} and {@link AbsoluteDelivery} types. + */ +export type DeliverySchedule = DelayDelivery | AbsoluteDelivery + +/** + * Type representing task options. + */ +export type TaskOptions = DeliverySchedule & TaskOptionsExperimental & { + + /** + * The deadline for requests sent to the worker. If the worker does not respond by this deadline + * then the request is cancelled and the attempt is marked as a DEADLINE_EXCEEDED failure. + * Cloud Tasks will retry the task according to the `RetryConfig`. + * The default is 10 minutes. The deadline must be in the range of 15 seconds and 30 minutes. + */ + dispatchDeadlineSeconds?: number; +} + +/** + * Type representing experimental (beta) task options. + */ +export interface TaskOptionsExperimental { + /** + * The full URL path that the request will be sent to. Must be a valid URL. + * @beta + */ + uri?: string; +} diff --git a/src/functions/functions.ts b/src/functions/functions.ts new file mode 100644 index 0000000000..08a38ab7ac --- /dev/null +++ b/src/functions/functions.ts @@ -0,0 +1,105 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from '../app'; +import { FirebaseFunctionsError, FunctionsApiClient } from './functions-api-client-internal'; +import { TaskOptions } from './functions-api'; +import * as validator from '../utils/validator'; + +/** + * The Firebase `Functions` service interface. + */ +export class Functions { + + private readonly client: FunctionsApiClient; + + /** + * @param app - The app for this `Functions` service. + * @constructor + * @internal + */ + constructor(readonly app: App) { + this.client = new FunctionsApiClient(app); + } + + /** + * Creates a reference to a {@link TaskQueue} for a given function name. + * The function name can be either: + * + * 1) A fully qualified function resource name: + * `projects/{project}/locations/{location}/functions/{functionName}` + * + * 2) A partial resource name with location and function name, in which case + * the runtime project ID is used: + * `locations/{location}/functions/{functionName}` + * + * 3) A partial function name, in which case the runtime project ID and the default location, + * `us-central1`, is used: + * `{functionName}` + * + * @param functionName - The name of the function. + * @param extensionId - Optional Firebase extension ID. + * @returns A promise that fulfills with a `TaskQueue`. + */ + public taskQueue>(functionName: string, extensionId?: string): TaskQueue { + return new TaskQueue(functionName, this.client, extensionId); + } +} + +/** + * The `TaskQueue` interface. + */ +export class TaskQueue> { + + /** + * @param functionName - The name of the function. + * @param client - The `FunctionsApiClient` instance. + * @param extensionId - Optional canonical ID of the extension. + * @constructor + * @internal + */ + constructor(private readonly functionName: string, private readonly client: FunctionsApiClient, + private readonly extensionId?: string) { + if (!validator.isNonEmptyString(functionName)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + '`functionName` must be a non-empty string.'); + } + if (!validator.isNonNullObject(client) || !('enqueue' in client)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + } + if (typeof extensionId !== 'undefined' && !validator.isString(extensionId)) { + throw new FirebaseFunctionsError( + 'invalid-argument', + '`extensionId` must be a string.'); + } + } + + /** + * Creates a task and adds it to the queue. Tasks cannot be updated after creation. + * This action requires `cloudtasks.tasks.create` IAM permission on the service account. + * + * @param data - The data payload of the task. + * @param opts - Optional options when enqueuing a new task. + * @returns A promise that resolves when the task has successfully been added to the queue. + */ + public enqueue(data: Args, opts?: TaskOptions): Promise { + return this.client.enqueue(data, this.functionName, this.extensionId, opts); + } +} diff --git a/src/functions/index.ts b/src/functions/index.ts new file mode 100644 index 0000000000..a046c4dd93 --- /dev/null +++ b/src/functions/index.ts @@ -0,0 +1,73 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Firebase Functions service. + * + * @packageDocumentation + */ + +import { App, getApp } from '../app'; +import { FirebaseApp } from '../app/firebase-app'; +import { Functions } from './functions'; + +export { + DelayDelivery, + AbsoluteDelivery, + DeliverySchedule, + TaskOptions, + TaskOptionsExperimental +} from './functions-api'; +export { + Functions, + TaskQueue +} from './functions'; + +/** + * Gets the {@link Functions} service for the default app + * or a given app. + * + * `getFunctions()` can be called with no arguments to access the default + * app's `Functions` service or as `getFunctions(app)` to access the + * `Functions` service associated with a specific app. + * + * @example + * ```javascript + * // Get the `Functions` service for the default app + * const defaultFunctions = getFunctions(); + * ``` + * + * @example + * ```javascript + * // Get the `Functions` service for a given app + * const otherFunctions = getFunctions(otherApp); + * ``` + * + * @param app - Optional app for which to return the `Functions` service. + * If not provided, the default `Functions` service is returned. + * + * @returns The default `Functions` service if no app is provided, or the `Functions` + * service associated with the provided app. + */ +export function getFunctions(app?: App): Functions { + if (typeof app === 'undefined') { + app = getApp(); + } + + const firebaseApp: FirebaseApp = app as FirebaseApp; + return firebaseApp.getOrInitService('functions', (app) => new Functions(app)); +} diff --git a/src/utils/error.ts b/src/utils/error.ts index 7989e7ecad..6c74748ed1 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -452,6 +452,10 @@ export class AuthClientErrorCode { code: 'invalid-email', message: 'The email address is improperly formatted.', }; + public static INVALID_NEW_EMAIL = { + code: 'invalid-new-email', + message: 'The new email address is improperly formatted.', + }; public static INVALID_ENROLLED_FACTORS = { code: 'invalid-enrolled-factors', message: 'The enrolled factors must be a valid array of MultiFactorInfo objects.', @@ -908,6 +912,8 @@ const AUTH_SERVER_TO_CLIENT_CODE: ServerToClientCode = { INVALID_DURATION: 'INVALID_SESSION_COOKIE_DURATION', // Invalid email provided. INVALID_EMAIL: 'INVALID_EMAIL', + // Invalid new email provided. + INVALID_NEW_EMAIL: 'INVALID_NEW_EMAIL', // Invalid tenant display name. This can be thrown on CreateTenant and UpdateTenant. INVALID_DISPLAY_NAME: 'INVALID_DISPLAY_NAME', // Invalid ID token provided. diff --git a/src/utils/index.ts b/src/utils/index.ts index ed9b2bc80e..824d41c0f9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -120,6 +120,53 @@ export function findProjectId(app: App): Promise { return Promise.resolve(null); } +/** + * Returns the service account email associated with a Firebase app, if it's explicitly + * specified in either the Firebase app options, credentials or the local environment. + * Otherwise returns null. + * + * @param app - A Firebase app to get the service account email from. + * + * @returns A service account email string or null. + */ +export function getExplicitServiceAccountEmail(app: App): string | null { + const options = app.options; + if (validator.isNonEmptyString(options.serviceAccountId)) { + return options.serviceAccountId; + } + + const credential = app.options.credential; + if (credential instanceof ServiceAccountCredential) { + return credential.clientEmail; + } + return null; +} + +/** + * Determines the service account email associated with a Firebase app. This method first + * checks if a service account email is explicitly specified in either the Firebase app options, + * credentials or the local environment in that order. If no explicit service account email is + * configured, but the SDK has been initialized with ComputeEngineCredentials, this + * method attempts to discover the service account email from the local metadata service. + * + * @param app - A Firebase app to get the service account email from. + * + * @returns A service account email ID string or null. + */ +export function findServiceAccountEmail(app: App): Promise { + const accountId = getExplicitServiceAccountEmail(app); + if (accountId) { + return Promise.resolve(accountId); + } + + const credential = app.options.credential; + if (credential instanceof ComputeEngineCredential) { + return credential.getServiceAccountEmail(); + } + + return Promise.resolve(null); +} + /** * Encodes data using web-safe-base64. * @@ -217,3 +264,40 @@ export function transformMillisecondsToSecondsString(milliseconds: number): stri } return duration; } + +/** + * Internal type to represent a resource name + */ +export type ParsedResource = { + projectId?: string; + locationId?: string; + resourceId: string; +} + +/** + * Parses the top level resources of a given resource name. + * Supports both full and partial resources names, example: + * `locations/{location}/functions/{functionName}`, + * `projects/{project}/locations/{location}/functions/{functionName}`, or {functionName} + * Does not support deeply nested resource names. + * + * @param resourceName - The resource name string. + * @param resourceIdKey - The key of the resource name to be parsed. + * @returns A parsed resource name object. + */ +export function parseResourceName(resourceName: string, resourceIdKey: string): ParsedResource { + if (!resourceName.includes('/')) { + return { resourceId: resourceName }; + } + const CHANNEL_NAME_REGEX = + new RegExp(`^(projects/([^/]+)/)?locations/([^/]+)/${resourceIdKey}/([^/]+)$`); + const match = CHANNEL_NAME_REGEX.exec(resourceName); + if (match === null) { + throw new Error('Invalid resource name format.'); + } + const projectId = match[2]; + const locationId = match[3]; + const resourceId = match[4]; + + return { projectId, locationId, resourceId }; +} diff --git a/test/integration/auth.spec.ts b/test/integration/auth.spec.ts index 2308ca6879..e1005d9c4a 100644 --- a/test/integration/auth.spec.ts +++ b/test/integration/auth.spec.ts @@ -1073,6 +1073,7 @@ describe('admin.auth', () => { describe('Link operations', () => { const uid = generateRandomString(20).toLowerCase(); const email = uid + '@example.com'; + const newEmail = uid + 'new@example.com'; const newPassword = 'newPassword'; const userData = { uid, @@ -1152,6 +1153,31 @@ describe('admin.auth', () => { expect(result.user!.emailVerified).to.be.true; }); }); + + it('generateVerifyAndChangeEmailLink() should return a verification link', function() { + if (authEmulatorHost) { + return this.skip(); // Not yet supported in Auth Emulator. + } + // Ensure the user's email is verified. + return getAuth().updateUser(uid, { password: 'password', emailVerified: true }) + .then((userRecord) => { + expect(userRecord.emailVerified).to.be.true; + return getAuth().generateVerifyAndChangeEmailLink(email, newEmail, actionCodeSettings); + }) + .then((link) => { + const code = getActionCode(link); + expect(getContinueUrl(link)).equal(actionCodeSettings.url); + return clientAuth().applyActionCode(code); + }) + .then(() => { + return clientAuth().signInWithEmailAndPassword(newEmail, 'password'); + }) + .then((result) => { + expect(result.user).to.exist; + expect(result.user!.email).to.equal(newEmail); + expect(result.user!.emailVerified).to.be.true; + }); + }); }); describe('Tenant management operations', () => { diff --git a/test/integration/functions.spec.ts b/test/integration/functions.spec.ts new file mode 100644 index 0000000000..9b1f5277f9 --- /dev/null +++ b/test/integration/functions.spec.ts @@ -0,0 +1,35 @@ +/*! + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as chai from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { getFunctions } from '../../lib/functions/index'; + +chai.should(); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('getFunctions()', () => { + + describe('taskQueue()', () => { + it('successfully returns a taskQueue', () => { + const factorizeQueue = getFunctions().taskQueue('queue-name'); + expect(factorizeQueue).to.be.not.undefined; + expect(typeof factorizeQueue.enqueue).to.equal('function'); + }); + }); +}); diff --git a/test/integration/postcheck/esm/example.test.js b/test/integration/postcheck/esm/example.test.js index c731577f66..29d0654374 100644 --- a/test/integration/postcheck/esm/example.test.js +++ b/test/integration/postcheck/esm/example.test.js @@ -22,6 +22,7 @@ import { getAppCheck, AppCheck } from 'firebase-admin/app-check'; import { getAuth, Auth } from 'firebase-admin/auth'; import { getDatabase, getDatabaseWithUrl, ServerValue } from 'firebase-admin/database'; import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getFunctions } from 'firebase-admin/functions'; import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; import { getMessaging, Messaging } from 'firebase-admin/messaging'; @@ -110,6 +111,12 @@ describe('ESM entry points', () => { expect(ref).to.be.instanceOf(DocumentReference); }); + it('Should return a Functions client', () => { + const fn = getFunctions(app); + expect(fn).to.be.not.undefined; + expect(typeof fn.taskQueue).to.equal('function'); + }); + it('Should return an InstanceId client', () => { const client = getInstanceId(app); expect(client).to.be.instanceOf(InstanceId); diff --git a/test/integration/postcheck/typescript/example-modular.test.ts b/test/integration/postcheck/typescript/example-modular.test.ts index 0d67abade4..c5ccf3c8a2 100644 --- a/test/integration/postcheck/typescript/example-modular.test.ts +++ b/test/integration/postcheck/typescript/example-modular.test.ts @@ -22,6 +22,7 @@ import { getAppCheck, AppCheck } from 'firebase-admin/app-check'; import { getAuth, Auth } from 'firebase-admin/auth'; import { getDatabase, getDatabaseWithUrl, Database, ServerValue } from 'firebase-admin/database'; import { getFirestore, DocumentReference, Firestore, FieldValue } from 'firebase-admin/firestore'; +import { getFunctions, Functions } from 'firebase-admin/functions'; import { getInstanceId, InstanceId } from 'firebase-admin/instance-id'; import { getMachineLearning, MachineLearning } from 'firebase-admin/machine-learning'; import { getMessaging, Messaging } from 'firebase-admin/messaging'; @@ -116,6 +117,12 @@ describe('Modular API', () => { expect(ref).to.be.instanceOf(DocumentReference); }); + it('Should return a Functions client', () => { + const fn: Functions = getFunctions(app); + expect(fn).to.be.not.undefined; + expect(typeof fn.taskQueue).to.equal('function'); + }); + it('Should return an InstanceId client', () => { const client = getInstanceId(app); expect(client).to.be.instanceOf(InstanceId); diff --git a/test/unit/app-check/app-check-api-client-internal.spec.ts b/test/unit/app-check/app-check-api-client-internal.spec.ts index b846fd1c68..65bc64d69d 100644 --- a/test/unit/app-check/app-check-api-client-internal.spec.ts +++ b/test/unit/app-check/app-check-api-client-internal.spec.ts @@ -56,7 +56,7 @@ describe('AppCheckApiClient', () => { const TEST_TOKEN_TO_EXCHANGE = 'signed-custom-token'; const TEST_RESPONSE = { - attestationToken: 'token', + token: 'token', ttl: '3s' }; @@ -203,11 +203,11 @@ describe('AppCheckApiClient', () => { stubs.push(stub); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) .then((resp) => { - expect(resp.token).to.deep.equal(TEST_RESPONSE.attestationToken); + expect(resp.token).to.deep.equal(TEST_RESPONSE.token); expect(resp.ttlMillis).to.deep.equal(3000); expect(stub).to.have.been.calledOnce.and.calledWith({ method: 'POST', - url: `https://firebaseappcheck.googleapis.com/v1beta/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, + url: `https://firebaseappcheck.googleapis.com/v1/projects/test-project/apps/${APP_ID}:exchangeCustomToken`, headers: EXPECTED_HEADERS, data: { customToken: TEST_TOKEN_TO_EXCHANGE } }); @@ -229,7 +229,7 @@ describe('AppCheckApiClient', () => { stubs.push(stub); return apiClient.exchangeToken(TEST_TOKEN_TO_EXCHANGE, APP_ID) .then((resp) => { - expect(resp.token).to.deep.equal(response.attestationToken); + expect(resp.token).to.deep.equal(response.token); expect(resp.ttlMillis).to.deep.equal(ttlMillis); }); }); diff --git a/test/unit/app-check/token-generator.spec.ts b/test/unit/app-check/token-generator.spec.ts index 7fea6b8594..f892c9fa08 100644 --- a/test/unit/app-check/token-generator.spec.ts +++ b/test/unit/app-check/token-generator.spec.ts @@ -44,7 +44,7 @@ const expect = chai.expect; const ALGORITHM = 'RS256'; const FIVE_MIN_IN_SECONDS = 5 * 60; -const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1beta.TokenExchangeService'; +const FIREBASE_APP_CHECK_AUDIENCE = 'https://firebaseappcheck.googleapis.com/google.firebase.appcheck.v1.TokenExchangeService'; /** * Verifies a token is signed with the private key corresponding to the provided public key. diff --git a/test/unit/auth/auth-api-request.spec.ts b/test/unit/auth/auth-api-request.spec.ts index f948b27b17..574962df53 100644 --- a/test/unit/auth/auth-api-request.spec.ts +++ b/test/unit/auth/auth-api-request.spec.ts @@ -3065,6 +3065,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { const path = handler.path('v1', '/accounts:sendOobCode', 'project_id'); const method = 'POST'; const email = 'user@example.com'; + const newEmail = 'usernew@example.com'; const actionCodeSettings = { url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, @@ -3110,12 +3111,14 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { requestType, email, returnOobLink: true, + ...(requestType === 'VERIFY_AND_CHANGE_EMAIL') && { newEmail }, }, expectedActionCodeSettingsRequest); const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); stubs.push(stub); const requestHandler = handler.init(mockApp); - return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings) + return requestHandler.getEmailActionLink(requestType, email, actionCodeSettings, + (requestType === 'VERIFY_AND_CHANGE_EMAIL') ? newEmail: undefined) .then((oobLink: string) => { expect(oobLink).to.be.equal(expectedLink); expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); @@ -3124,7 +3127,7 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); EMAIL_ACTION_REQUEST_TYPES.forEach((requestType) => { - if (requestType === 'EMAIL_SIGNIN') { + if (requestType === 'EMAIL_SIGNIN' || requestType === 'VERIFY_AND_CHANGE_EMAIL') { return; } it('should be fulfilled given requestType:' + requestType + ' and no ActionCodeSettings', () => { @@ -3145,6 +3148,25 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be fulfilled given a valid requestType: VERIFY_AND_CHANGE_EMAIL and no ActionCodeSettings', () => { + const VERIFY_AND_CHANGE_EMAIL = 'VERIFY_AND_CHANGE_EMAIL'; + const requestData = { + requestType: VERIFY_AND_CHANGE_EMAIL, + email, + returnOobLink: true, + newEmail, + }; + const stub = sinon.stub(HttpClient.prototype, 'send').resolves(expectedResult); + stubs.push(stub); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink(VERIFY_AND_CHANGE_EMAIL, email, undefined, newEmail) + .then((oobLink: string) => { + expect(oobLink).to.be.equal(expectedLink); + expect(stub).to.have.been.calledOnce.and.calledWith(callParams(path, method, requestData)); + }); + }); + it('should be rejected given requestType:EMAIL_SIGNIN and no ActionCodeSettings', () => { const invalidRequestType = 'EMAIL_SIGNIN'; const requestHandler = handler.init(mockApp); @@ -3153,6 +3175,22 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); + it('should be rejected given requestType: VERIFY_AND_CHANGE and no new Email address', () => { + const requestHandler = handler.init(mockApp); + const expectedError = new FirebaseAuthError( + AuthClientErrorCode.INVALID_ARGUMENT, + '`newEmail` is required when `requestType` === \'VERIFY_AND_CHANGE_EMAIL\'', + ) + + return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid argument error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given an invalid email', () => { const invalidEmail = 'invalid'; const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_EMAIL); @@ -3167,6 +3205,20 @@ AUTH_REQUEST_HANDLER_TESTS.forEach((handler) => { }); }); + it('should be rejected given an invalid new email', () => { + const invalidNewEmail = 'invalid'; + const expectedError = new FirebaseAuthError(AuthClientErrorCode.INVALID_NEW_EMAIL); + + const requestHandler = handler.init(mockApp); + return requestHandler.getEmailActionLink('VERIFY_AND_CHANGE_EMAIL', email, actionCodeSettings, invalidNewEmail) + .then(() => { + throw new Error('Unexpected success'); + }, (error) => { + // Invalid new email error should be thrown. + expect(error).to.deep.include(expectedError); + }); + }); + it('should be rejected given an invalid request type', () => { const invalidRequestType = 'invalid'; const expectedError = new FirebaseAuthError( diff --git a/test/unit/auth/auth.spec.ts b/test/unit/auth/auth.spec.ts index 1e596c2721..0995a310d4 100644 --- a/test/unit/auth/auth.spec.ts +++ b/test/unit/auth/auth.spec.ts @@ -2869,10 +2869,12 @@ AUTH_CONFIGS.forEach((testConfig) => { { api: 'generatePasswordResetLink', requestType: 'PASSWORD_RESET', requiresSettings: false }, { api: 'generateEmailVerificationLink', requestType: 'VERIFY_EMAIL', requiresSettings: false }, { api: 'generateSignInWithEmailLink', requestType: 'EMAIL_SIGNIN', requiresSettings: true }, + { api: 'generateVerifyAndChangeEmailLink', requestType: 'VERIFY_AND_CHANGE_EMAIL', requiresSettings: false }, ]; emailActionFlows.forEach((emailActionFlow) => { describe(`${emailActionFlow.api}()`, () => { const email = 'user@example.com'; + const newEmail = 'usernew@example.com'; const actionCodeSettings = { url: 'https://www.example.com/path/file?a=1&b=2', handleCodeInApp: true, @@ -2898,32 +2900,71 @@ AUTH_CONFIGS.forEach((testConfig) => { }); it('should be rejected given no email', () => { - return (auth as any)[emailActionFlow.api](undefined, actionCodeSettings) + let args: any = [ undefined, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ undefined, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); }); it('should be rejected given an invalid email', () => { - return (auth as any)[emailActionFlow.api]('invalid', actionCodeSettings) + let args: any = [ 'invalid', actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ 'invalid', newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-email'); }); + it('should be rejected given no new email when request type is `generateVerifyAndChangeEmailLink`', () => { + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + return (auth as any)[emailActionFlow.api](email) + .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); + } + }); + + it('should be rejected given an invalid new email when request type is `generateVerifyAndChangeEmailLink`', + () => { + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + return (auth as any)[emailActionFlow.api](email, 'invalid') + .should.eventually.be.rejected.and.have.property('code', 'auth/invalid-new-email'); + } + }); + it('should be rejected given an invalid ActionCodeSettings object', () => { - return (auth as any)[emailActionFlow.api](email, 'invalid') + let args: any = [ email, 'invalid' ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, 'invalid' ]; + } + return (auth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'auth/argument-error'); }); it('should be rejected given an app which returns null access tokens', () => { - return (nullAccessTokenAuth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (nullAccessTokenAuth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); it('should be rejected given an app which returns invalid access tokens', () => { - return (malformedAccessTokenAuth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (malformedAccessTokenAuth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); it('should be rejected given an app which fails to generate access tokens', () => { - return (rejectedPromiseAccessTokenAuth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (rejectedPromiseAccessTokenAuth as any)[emailActionFlow.api](...args) .should.eventually.be.rejected.and.have.property('code', 'app/invalid-credential'); }); @@ -2932,7 +2973,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') .resolves(expectedLink); stubs.push(getEmailActionLinkStub); - return (auth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .then((actualLink: string) => { // Confirm underlying API called with expected parameters. expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( @@ -2953,7 +2998,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') .resolves(expectedLink); stubs.push(getEmailActionLinkStub); - return (auth as any)[emailActionFlow.api](email) + let args: any = [ email ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail ]; + } + return (auth as any)[emailActionFlow.api](...args) .then((actualLink: string) => { // Confirm underlying API called with expected parameters. expect(getEmailActionLinkStub).to.have.been.calledOnce.and.calledWith( @@ -2969,7 +3018,11 @@ AUTH_CONFIGS.forEach((testConfig) => { const getEmailActionLinkStub = sinon.stub(testConfig.RequestHandler.prototype, 'getEmailActionLink') .rejects(expectedError); stubs.push(getEmailActionLinkStub); - return (auth as any)[emailActionFlow.api](email, actionCodeSettings) + let args: any = [ email, actionCodeSettings ]; + if (emailActionFlow.api === 'generateVerifyAndChangeEmailLink') { + args = [ email, newEmail, actionCodeSettings ]; + } + return (auth as any)[emailActionFlow.api](...args) .then(() => { throw new Error('Unexpected success'); }, (error: any) => { diff --git a/test/unit/eventarc/eventarc-utils.spec.ts b/test/unit/eventarc/eventarc-utils.spec.ts new file mode 100644 index 0000000000..d2f18b7d96 --- /dev/null +++ b/test/unit/eventarc/eventarc-utils.spec.ts @@ -0,0 +1,193 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as sinon from 'sinon'; +import * as utils from '../../../src/eventarc/eventarc-utils'; +import * as chai from 'chai'; +import chaiExclude from 'chai-exclude'; + +const expect = chai.expect; +chai.use(chaiExclude); + +describe('eventarc-utils', () => { + before(() => { + sinon + .stub(Date.prototype, 'toISOString') + .returns('2022-03-16T20:20:42.212Z'); + }); + + after(() => { + sinon.restore(); + }); + + afterEach(() => { + delete process.env.EVENTARC_CLOUD_EVENT_SOURCE; + }); + + describe('toCloudEventProtoFormat', () => { + it('converts cloud event to proto format', () => { + expect(utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + specversion: '1.0', + subject: 'context', + datacontenttype: 'application/json', + id: 'user-provided-id', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + customattr: 'custom value', + })).to.deep.eq({ + '@type': 'type.googleapis.com/io.cloudevents.v1.CloudEvent', + 'attributes': { + 'customattr': { + 'ceString': 'custom value' + }, + 'datacontenttype': { + 'ceString': 'application/json' + }, + 'time': { + 'ceTimestamp': '2022-03-16T20:20:42.212Z' + }, + 'subject': { + 'ceString': 'context' + } + }, + 'id': 'user-provided-id', + 'source': '/my/functions', + 'specVersion': '1.0', + 'textData': '{"hello":"world"}', + 'type': 'some.custom.event', + }); + }); + + it('populates specversion if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + expect(got['specVersion']).to.eq('1.0'); + }); + + it('populates time if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + specversion: '1.0', + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + }); + expect(got['attributes']['time']).to.deep.eq({ + 'ceTimestamp': '2022-03-16T20:20:42.212Z' + }); + }); + + it('populates id if not provided', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got).to.haveOwnProperty('id'); + }); + + it('populates source from EVENTARC_CLOUD_EVENT_SOURCE env var if not set', () => { + process.env.EVENTARC_CLOUD_EVENT_SOURCE = '//source/from/env/var'; + const got = utils.toCloudEventProtoFormat({ + specversion: '1.0', + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + }); + expect(got['source']).to.eq('//source/from/env/var'); + }); + + it('throws invalid argument when source not set', () => { + expect(() => utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + time: new Date().toISOString(), + })).throws("CloudEvent 'source' is required."); + }); + + it('throws invalid argument when custom attr not string', () => { + expect(() => utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + datacontenttype: 'application/json', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + customattr: 123, + })).throws("CloudEvent extension attributes ('customattr') must be string"); + }); + + it('populates converts object data to JSON and sets datacontenttype', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + data: { + hello: 'world' + }, + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got['textData']).to.eq('{"hello":"world"}'); + expect(got['attributes']['datacontenttype']).to.deep.eq({ + 'ceString': 'application/json' + }); + }); + + it('populates string data and sets datacontenttype', () => { + const got = utils.toCloudEventProtoFormat({ + type: 'some.custom.event', + id: 'user-provided-id', + data: 'hello world', + source: '/my/functions', + time: new Date().toISOString(), + }); + // Couldn't figure out how to stub uuid, so just checking for presense. + expect(got['textData']).to.eq('hello world'); + expect(got['attributes']['datacontenttype']).to.deep.eq({ + 'ceString': 'text/plain' + }); + }); + }); +}); diff --git a/test/unit/eventarc/eventarc.spec.ts b/test/unit/eventarc/eventarc.spec.ts new file mode 100644 index 0000000000..1bbddd5a4d --- /dev/null +++ b/test/unit/eventarc/eventarc.spec.ts @@ -0,0 +1,572 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as sinon from 'sinon'; +import { Channel, Eventarc } from '../../../src/eventarc'; +import { toCloudEventProtoFormat } from '../../../src/eventarc/eventarc-utils'; +import { CloudEvent } from '../../../src/eventarc/cloudevent'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseApp } from '../../../src/app/firebase-app'; +import * as mocks from '../../resources/mocks'; +import * as utils from '../utils'; +import * as chai from 'chai'; +import chaiExclude from 'chai-exclude'; +import { getSdkVersion } from '../../../src/utils/index'; + +const expect = chai.expect; +chai.use(chaiExclude); + +const TEST_EVENT1 : CloudEvent = { + type: 'some.custom.event1', + specversion: '1.0', + id: 'user-provided-id-1', + data: 'hello world', + source: '/my/functions', + time: '2011-11-11T11:11:11.111Z', +}; +const TEST_EVENT1_SERIALIZED = JSON.stringify(toCloudEventProtoFormat(TEST_EVENT1)); + +const TEST_EVENT2 : CloudEvent = { + type: 'some.custom.event2', + specversion: '1.0', + id: 'user-provided-id-2', + data: 'hello world', + source: '/my/functions', + time: '2011-11-11T11:11:11.111Z', +}; +const TEST_EVENT2_SERIALIZED = JSON.stringify(toCloudEventProtoFormat(TEST_EVENT2)); + +describe('eventarc', () => { + let mockApp: FirebaseApp; + let eventarc: Eventarc; + + before(() => { + mockApp = mocks.app(); + eventarc = new Eventarc(mockApp); + }); + + after(() => { + sinon.restore(); + }); + + afterEach(() => { + delete process.env.EVENTARC_CLOUD_EVENT_SOURCE; + }); + + describe('Eventarc', () => { + it('inintializes Eventarc object', () => { + expect(eventarc.app).eq(mockApp); + }); + }); + + it('throws invalid argument with creating channel with invalid name', () => { + expect(() => eventarc.channel('foo/bar')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('foo/bar/baz')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projectid/locations/us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('v1/projects/projectid/locations/us-central1/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/channels/foo')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/locations/us-central1')) + .throws('Invalid channel name format.'); + expect(() => eventarc.channel('projects/projectid/locations_us-central1/channels/foo')) + .throws('Invalid channel name format.'); + }); + + describe('default Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel(); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('locations/us-central1/channels/firebase'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('full resource name Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('projects/other-project-id/locations/us-west1/channels/my-channel2'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('projects/other-project-id/locations/us-west1/channels/my-channel2'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/other-project-id/locations/us-west1/channels/my-channel2:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/other-project-id/locations/us-west1/channels/my-channel2:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('partial (no project) Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('locations/us-west1/channels/my-channel'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('locations/us-west1/channels/my-channel'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-west1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-west1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('partial (channel id only) Channel', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel('my-channel'); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.name).eq('my-channel'); + expect(channel.allowedEventTypes).is.undefined; + }); + + it('publishes single event to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes multiple events to the API', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, TEST_EVENT2]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/my-channel:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED},${TEST_EVENT2_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('Channel with empty allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: [] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).is.empty; + }); + + it('filters out event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.not.have.been.called; + }); + + it('filters out all event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { type: 'foo' }]); + + expect(httpStub).to.not.have.been.called; + }); + }); + + describe('Channel with channel and empty allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel( + 'adasdas', + { allowedEventTypes: [] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).is.empty; + }); + + it('filters out event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.not.have.been.called; + }); + + it('filters out all event and publishes none', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { type: 'foo' }]); + + expect(httpStub).to.not.have.been.called; + }); + }); + + describe('Channel with allowed events', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: ['some.custom.event1'] }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).deep.eq(['some.custom.event1']); + }); + + it('publishes events with allowed type', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes events with allowed type and filters out others', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { + type: 'some.custom.event2' + }]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); + + describe('Channel with allowed events as string', () => { + let channel : Channel; + let mockAccessToken: string; + let httpStub: sinon.SinonStub; + let accessTokenStub: sinon.SinonStub; + + before(() => { + channel = eventarc.channel({ allowedEventTypes: 'some.custom.event1,some.other.event.type' }); + mockAccessToken = utils.generateRandomAccessToken(); + accessTokenStub = utils.stubGetAccessToken(mockAccessToken); + }); + + after(() => { + accessTokenStub?.restore(); + }); + + afterEach(() => { + httpStub?.restore(); + }); + + it('inintializes Channel object', () => { + expect(channel.eventarc).eq(eventarc); + expect(channel.allowedEventTypes).deep.eq(['some.custom.event1', 'some.other.event.type']); + }); + + it('publishes events with allowed type', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish(TEST_EVENT1); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + + it('publishes events with allowed type and filters out others', async () => { + httpStub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({})); + + await channel.publish([TEST_EVENT1, { + type: 'some.custom.event2' + }]); + + expect(httpStub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://eventarcpublishing.googleapis.com/v1/projects/project_id/locations/us-central1/channels/firebase:publishEvents', + data: `{"events":[${TEST_EVENT1_SERIALIZED}]}`, + headers: { + 'X-Firebase-Client': 'fire-admin-node/' + getSdkVersion(), + Authorization: 'Bearer ' + mockAccessToken + } + }); + }); + }); +}); diff --git a/test/unit/functions/functions-api-client-internal.spec.ts b/test/unit/functions/functions-api-client-internal.spec.ts new file mode 100644 index 0000000000..8f5cc1b6f7 --- /dev/null +++ b/test/unit/functions/functions-api-client-internal.spec.ts @@ -0,0 +1,447 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as utils from '../utils'; +import * as mocks from '../../resources/mocks'; +import { getSdkVersion } from '../../../src/utils'; + +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { FirebaseFunctionsError, FunctionsApiClient } from '../../../src/functions/functions-api-client-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { FirebaseAppError } from '../../../src/utils/error'; +import { deepCopy } from '../../../src/utils/deep-copy'; + +const expect = chai.expect; + +describe('FunctionsApiClient', () => { + + const ERROR_RESPONSE = { + error: { + code: 404, + message: 'Requested entity not found', + status: 'NOT_FOUND', + }, + }; + + const EXPECTED_HEADERS = { + 'X-Firebase-Client': `fire-admin-node/${getSdkVersion()}`, + 'Authorization': 'Bearer mock-token' + }; + + const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + const DEFAULT_REGION = 'us-central1'; + const CUSTOM_REGION = 'us-west1'; + const FUNCTION_NAME = 'function-name'; + const CUSTOM_PROJECT_ID = 'taskq-project'; + const EXTENSION_ID = 'image-resize'; + const PARTIAL_RESOURCE_NAME = `locations/${CUSTOM_REGION}/functions/${FUNCTION_NAME}`; + const FULL_RESOURCE_NAME = `projects/${CUSTOM_PROJECT_ID}/locations/${CUSTOM_REGION}/functions/${FUNCTION_NAME}`; + + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + serviceAccountId: 'service-acct@email.com' + }; + + const TEST_TASK_PAYLOAD = { + httpRequest: { + url: `https://${DEFAULT_REGION}-${mockOptions.projectId}.cloudfunctions.net/${FUNCTION_NAME}`, + oidcToken: { + serviceAccountEmail: mockOptions.serviceAccountId, + }, + body: Buffer.from(JSON.stringify({ data: {} })).toString('base64'), + headers: { 'Content-Type' : 'application/json' } + } + } + + const CLOUD_TASKS_URL = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${DEFAULT_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_EXT = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${DEFAULT_REGION}/queues/ext-${EXTENSION_ID}-${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_FULL_RESOURCE = `https://cloudtasks.googleapis.com/v2/projects/${CUSTOM_PROJECT_ID}/locations/${CUSTOM_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const CLOUD_TASKS_URL_PARTIAL_RESOURCE = `https://cloudtasks.googleapis.com/v2/projects/${mockOptions.projectId}/locations/${CUSTOM_REGION}/queues/${FUNCTION_NAME}/tasks`; + + const clientWithoutProjectId = new FunctionsApiClient(mocks.mockCredentialApp()); + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + let app: FirebaseApp; + let apiClient: FunctionsApiClient; + + beforeEach(() => { + app = mocks.appWithOptions(mockOptions); + apiClient = new FunctionsApiClient(app); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + return app.delete(); + }); + + describe('Constructor', () => { + it('should reject when the app is null', () => { + expect(() => new FunctionsApiClient(null as unknown as FirebaseApp)) + .to.throw('First argument passed to getFunctions() must be a valid Firebase app instance.'); + }); + }); + + describe('enqueue', () => { + let clock: sinon.SinonFakeTimers | undefined; + + afterEach(() => { + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + it('should reject when project id is not available', () => { + return clientWithoutProjectId.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + it('should reject when project id is not available in partial resource name', () => { + return clientWithoutProjectId.enqueue({}, PARTIAL_RESOURCE_NAME) + .should.eventually.be.rejectedWith(noProjectId); + }); + + for (const invalidName of [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop, undefined]) { + it(`should throw if functionName is ${invalidName}`, () => { + expect(() => apiClient.enqueue({}, invalidName as any)) + .to.throw('Function name must be a non empty string'); + }); + } + + for (const invalidName of ['project/abc/locations/east/fname', 'location/west/', '//']) { + it(`should throw if functionName is ${invalidName}`, () => { + expect(() => apiClient.enqueue({}, invalidName as any)) + .to.throw('Function name must be a single string or a qualified resource name'); + }); + } + + for (const invalidOption of [null, 'abc', '', [], true, 102, 1.2]) { + it(`should throw if options is ${invalidOption}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', invalidOption as any)) + .to.throw('TaskOptions must be a non-null object'); + }); + } + + for (const invalidScheduleTime of [null, '', 'abc', 102, 1.2, [], {}, true, NaN]) { + it(`should throw if scheduleTime is ${invalidScheduleTime}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleTime: invalidScheduleTime } as any)) + .to.throw('scheduleTime must be a valid Date object.'); + }); + } + + for (const invalidScheduleDelaySeconds of [null, 'abc', '', [], {}, true, NaN, -1]) { + it(`should throw if scheduleDelaySeconds is ${invalidScheduleDelaySeconds}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', + { scheduleDelaySeconds: invalidScheduleDelaySeconds } as any)) + .to.throw('scheduleDelaySeconds must be a non-negative duration in seconds.'); + }); + } + + for (const invalidDispatchDeadlineSeconds of [null, 'abc', '', [], {}, true, NaN, -1, 14, 1801]) { + it(`should throw if dispatchDeadlineSeconds is ${invalidDispatchDeadlineSeconds}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', + { dispatchDeadlineSeconds: invalidDispatchDeadlineSeconds } as any)) + .to.throw('dispatchDeadlineSeconds must be a non-negative duration in seconds ' + + 'and must be in the range of 15s to 30 mins.'); + }); + } + + for (const invalidUri of [null, '', 'a', 'foo', 'image.jpg', [], {}, true, NaN]) { + it(`should throw given an invalid uri: ${invalidUri}`, () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', + { uri: invalidUri } as any)) + .to.throw('uri must be a valid URL string.'); + }); + } + + it('should throw when both scheduleTime and scheduleDelaySeconds are provided', () => { + expect(() => apiClient.enqueue({}, FUNCTION_NAME, '', { + scheduleTime: new Date(), + scheduleDelaySeconds: 1000 + } as any)) + .to.throw('Both scheduleTime and scheduleDelaySeconds are provided. Only one value should be set.'); + }); + + it('should reject when a full platform error response is received', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom(ERROR_RESPONSE, 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError('not-found', 'Requested entity not found'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error when error code is not present', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom({}, 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError('unknown-error', 'Unknown server error: {}'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject with unknown-error for non-json response', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(utils.errorFrom('not json', 404)); + stubs.push(stub); + const expected = new FirebaseFunctionsError( + 'unknown-error', 'Unexpected response with status: 404 and body: not json'); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should reject when rejected with a FirebaseAppError', () => { + const expected = new FirebaseAppError('network-error', 'socket hang up'); + const stub = sinon + .stub(HttpClient.prototype, 'send') + .rejects(expected); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME) + .should.eventually.be.rejected.and.deep.include(expected); + }); + + it('should resolve on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: TEST_TASK_PAYLOAD + } + }); + }); + }); + + it('should resolve the projectId and location from the full resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${CUSTOM_PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FULL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_FULL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should resolve the location from the partial resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${mockOptions.projectId}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, PARTIAL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_PARTIAL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should update the function name when the extension-id is provided', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${DEFAULT_REGION}-${mockOptions.projectId}.cloudfunctions.net/ext-${EXTENSION_ID}-${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, EXTENSION_ID) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_EXT, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + }); + }); + + it('should use the default projectId following a request with a full resource name', () => { + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.url = + `https://${CUSTOM_REGION}-${CUSTOM_PROJECT_ID}.cloudfunctions.net/${FUNCTION_NAME}`; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + // pass the full resource name. SDK should not use the default values + return apiClient.enqueue({}, FULL_RESOURCE_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL_FULL_RESOURCE, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload + } + }); + + // passing just the function name. SDK should deffer to default values + return apiClient.enqueue({}, FUNCTION_NAME); + }) + .then(() => { + expect(stub).to.have.been.calledTwice.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: TEST_TASK_PAYLOAD + } + }); + }); + }); + + + + // tests for Task Options + it('should convert scheduleTime to ISO string', () => { + const scheduleTime = new Date(); + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + // timestamps should be converted to ISO strings + (expectedPayload as any).scheduleTime = scheduleTime.toISOString(); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleTime }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should set scheduleTime based on scheduleDelaySeconds', () => { + clock = sinon.useFakeTimers(1000); + + const scheduleDelaySeconds = 1800; + const scheduleTime = new Date(); // '1970-01-01T00:00:01.000Z' + scheduleTime.setSeconds(scheduleTime.getSeconds() + scheduleDelaySeconds); // '1970-01-01T00:30:01.000Z' + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + // timestamps should be converted to ISO strings + (expectedPayload as any).scheduleTime = scheduleTime.toISOString(); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { scheduleDelaySeconds }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should convert dispatchDeadline to a duration with `s` prefix', () => { + const dispatchDeadlineSeconds = 1800; + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + (expectedPayload as any).dispatchDeadline = `${dispatchDeadlineSeconds}s`; + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue({}, FUNCTION_NAME, '', { dispatchDeadlineSeconds }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + + it('should encode data in the payload', () => { + const data = { privateKey: '~/.ssh/id_rsa.pub' }; + const expectedPayload = deepCopy(TEST_TASK_PAYLOAD); + expectedPayload.httpRequest.body = Buffer.from(JSON.stringify({ data })).toString('base64'); + + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom({}, 200)); + stubs.push(stub); + return apiClient.enqueue(data, FUNCTION_NAME) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: CLOUD_TASKS_URL, + headers: EXPECTED_HEADERS, + data: { + task: expectedPayload, + } + }); + }); + }); + }); +}); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts new file mode 100644 index 0000000000..36e6098d42 --- /dev/null +++ b/test/unit/functions/functions.spec.ts @@ -0,0 +1,187 @@ +/*! + * @license + * Copyright 2022 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as _ from 'lodash'; +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as mocks from '../../resources/mocks'; + +import { FirebaseApp } from '../../../src/app/firebase-app'; +import { FunctionsApiClient, FirebaseFunctionsError } from '../../../src/functions/functions-api-client-internal'; +import { HttpClient } from '../../../src/utils/api-request'; +import { Functions, TaskQueue } from '../../../src/functions/functions'; + +const expect = chai.expect; + +describe('Functions', () => { + const mockOptions = { + credential: new mocks.MockCredential(), + projectId: 'test-project', + }; + + let functions: Functions; + + let mockApp: FirebaseApp; + let mockCredentialApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.appWithOptions(mockOptions); + mockCredentialApp = mocks.mockCredentialApp(); + functions = new Functions(mockApp); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + for (const invalidApp of [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid app: ' + JSON.stringify(invalidApp), () => { + expect(() => { + const functionsAny: any = Functions; + return new functionsAny(invalidApp); + }).to.throw( + 'First argument passed to getFunctions() must be a valid Firebase app ' + + 'instance.'); + }); + } + + it('should reject when initialized without project ID', () => { + // Remove Project ID from the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const noProjectId = 'Failed to determine project ID. Initialize the SDK with service ' + + 'account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + const functionsWithoutProjectId = new Functions(mockCredentialApp); + return functionsWithoutProjectId.taskQueue('task-name').enqueue({}) + .should.eventually.rejectedWith(noProjectId); + }); + + it('should reject when failed to contact the Metadata server for service account email', () => { + const functionsWithProjectId = new Functions(mockApp); + const stub = sinon.stub(HttpClient.prototype, 'send') + .rejects(new Error('network error.')); + stubs.push(stub); + const expected = 'Failed to determine service account. Initialize the ' + + 'SDK with service account credentials or set service account ID as an app option.'; + return functionsWithProjectId.taskQueue('task-name').enqueue({}) + .should.eventually.be.rejectedWith(expected); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return new Functions(mockApp); + }).not.to.throw(); + }); + }); + + describe('app', () => { + it('returns the app from the constructor', () => { + // We expect referential equality here + expect(functions.app).to.equal(mockApp); + }); + }); +}); + +describe('TaskQueue', () => { + const INTERNAL_ERROR = new FirebaseFunctionsError('internal-error', 'message'); + const FUNCTION_NAME = 'function-name'; + + let taskQueue: TaskQueue; + let mockClient: FunctionsApiClient; + + let mockApp: FirebaseApp; + + // Stubs used to simulate underlying api calls. + let stubs: sinon.SinonStub[] = []; + + before(() => { + mockApp = mocks.app(); + mockClient = new FunctionsApiClient(mockApp); + taskQueue = new TaskQueue(FUNCTION_NAME, mockClient); + }); + + after(() => { + return mockApp.delete(); + }); + + afterEach(() => { + _.forEach(stubs, (stub) => stub.restore()); + stubs = []; + }); + + describe('Constructor', () => { + for (const invalidClient of [null, NaN, 0, 1, true, false, '', 'a', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid client: ' + JSON.stringify(invalidClient), () => { + expect(() => { + const taskQueueAny: any = TaskQueue; + return new taskQueueAny(FUNCTION_NAME, invalidClient); + }).to.throw( + 'Must provide a valid FunctionsApiClient instance to create a new TaskQueue.'); + }); + } + + for (const invalidFunctionName of [null, NaN, 0, 1, true, false, '', [], [1, 'a'], {}, { a: 1 }, _.noop, + undefined]) { + it('should throw given invalid name: ' + JSON.stringify(invalidFunctionName), () => { + expect(() => { + const taskQueueAny: any = TaskQueue; + return new taskQueueAny(invalidFunctionName, mockClient); + }).to.throw('`functionName` must be a non-empty string.'); + }); + } + + it('should not throw given a valid name and client', () => { + expect(() => { + return new TaskQueue(FUNCTION_NAME, mockClient); + }).not.to.throw(); + }); + }); + + describe('enqueue', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(FunctionsApiClient.prototype, 'enqueue') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return taskQueue.enqueue({}) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + it('should propagate API errors with task options', () => { + const stub = sinon + .stub(FunctionsApiClient.prototype, 'enqueue') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return taskQueue.enqueue({}, { scheduleDelaySeconds: 3600 }) + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + }); +}); diff --git a/test/unit/functions/index.spec.ts b/test/unit/functions/index.spec.ts new file mode 100644 index 0000000000..8bfcaa5dfe --- /dev/null +++ b/test/unit/functions/index.spec.ts @@ -0,0 +1,76 @@ +/*! + * @license + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +import * as chai from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as chaiAsPromised from 'chai-as-promised'; + +import * as mocks from '../../resources/mocks'; +import { App } from '../../../src/app/index'; +import { getFunctions, Functions } from '../../../src/functions/index'; + +chai.should(); +chai.use(sinonChai); +chai.use(chaiAsPromised); + +const expect = chai.expect; + +describe('Functions', () => { + let mockApp: App; + let mockCredentialApp: App; + + const noProjectIdError = 'Failed to determine project ID. Initialize the SDK ' + + 'with service account credentials or set project ID as an app option. Alternatively, set the ' + + 'GOOGLE_CLOUD_PROJECT environment variable.'; + + beforeEach(() => { + mockApp = mocks.app(); + mockCredentialApp = mocks.mockCredentialApp(); + }); + + describe('getFunctions()', () => { + it('should throw when default app is not available', () => { + expect(() => { + return getFunctions(); + }).to.throw('The default Firebase app does not exist.'); + }); + + it('should reject given an invalid credential without project ID', () => { + // Project ID not set in the environment. + delete process.env.GOOGLE_CLOUD_PROJECT; + delete process.env.GCLOUD_PROJECT; + const functions = getFunctions(mockCredentialApp); + const factorizedQueue = functions.taskQueue('task-name'); + return factorizedQueue.enqueue({}) + .should.eventually.rejectedWith(noProjectIdError); + }); + + it('should not throw given a valid app', () => { + expect(() => { + return getFunctions(mockApp); + }).not.to.throw(); + }); + + it('should return the same instance for a given app instance', () => { + const fn1: Functions = getFunctions(mockApp); + const fn2: Functions = getFunctions(mockApp); + expect(fn1).to.equal(fn2); + }); + }); +}); diff --git a/test/unit/index.spec.ts b/test/unit/index.spec.ts index ade3503069..aa7b262111 100644 --- a/test/unit/index.spec.ts +++ b/test/unit/index.spec.ts @@ -103,3 +103,11 @@ import './app-check/app-check.spec'; import './app-check/app-check-api-client-internal.spec'; import './app-check/token-generator.spec'; import './app-check/token-verifier.spec.ts'; + +// Eventarc +import './eventarc/eventarc.spec'; +import './eventarc/eventarc-utils.spec'; +// Functions +import './functions/index.spec'; +import './functions/functions.spec'; +import './functions/functions-api-client-internal.spec'; diff --git a/test/unit/utils/index.spec.ts b/test/unit/utils/index.spec.ts index 7d007f2f78..7105726e67 100644 --- a/test/unit/utils/index.spec.ts +++ b/test/unit/utils/index.spec.ts @@ -22,7 +22,7 @@ import * as sinon from 'sinon'; import * as mocks from '../../resources/mocks'; import { addReadonlyGetter, getExplicitProjectId, findProjectId, - toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, + toWebSafeBase64, formatString, generateUpdateMask, transformMillisecondsToSecondsString, parseResourceName, } from '../../../src/utils/index'; import { isNonEmptyString } from '../../../src/utils/validator'; import { FirebaseApp } from '../../../src/app/firebase-app'; @@ -396,3 +396,27 @@ describe('transformMillisecondsToSecondsString()', () => { }); }); }); + +describe('parseResourceName()', () => { + + const FULL_RESOURCE_NAME = 'projects/abc/locations/us/functions/f1'; + const PARTIAL_RESOURCE_NAME = 'locations/us/functions/f1'; + const projectId = 'abc'; + const locationId = 'us'; + const resourceId = 'f1'; + + it('should return projectId, location, and resource when given a full resource name', () => { + expect(parseResourceName(FULL_RESOURCE_NAME, 'functions')) + .to.deep.equal({ projectId, locationId, resourceId }); + }); + + it('should return location and resource when given a partial resource name', () => { + expect(parseResourceName(PARTIAL_RESOURCE_NAME, 'functions')) + .to.deep.equal({ projectId: undefined, locationId, resourceId }); + }); + + it('should return the resource when given only the resource name', () => { + expect(parseResourceName('f1', 'functions')) + .to.deep.equal({ resourceId }); + }); +});