diff --git a/.changeset/pretty-ways-search.md b/.changeset/pretty-ways-search.md new file mode 100644 index 0000000000..212c23b09a --- /dev/null +++ b/.changeset/pretty-ways-search.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Add Firestore as key-store provider. +Add `auth.keyStore` section to application config. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index eea86a6097..7e478953bc 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -311,3 +311,4 @@ Zalando Zhou zoomable zsh +Firestore diff --git a/app-config.yaml b/app-config.yaml index e1b648c472..8aa3bc569c 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -292,6 +292,15 @@ scaffolder: visibility: public # or or 'private' auth: + ### Add auth.keyStore.provider to more granularly control how to store JWK data when running + # the auth-backend. + # + # keyStore: + # provider: firestore + # firestore: + # projectId: my-project + # path: my-sessions + environment: development ### Providing an auth.session.secret will enable session support in the auth-backend # session: diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts index 61bbe15d05..25da7fa332 100644 --- a/plugins/auth-backend/config.d.ts +++ b/plugins/auth-backend/config.d.ts @@ -31,6 +31,32 @@ export interface Config { secret?: string; }; + /** To control how to store JWK data in auth-backend */ + keyStore?: { + provider?: 'database' | 'memory' | 'firestore'; + firestore?: { + /** The host to connect to */ + host?: string; + /** The port to connect to */ + port?: number; + /** Whether to use SSL when connecting. */ + ssl?: boolean; + /** The Google Cloud Project ID */ + projectId?: string; + /** + * Local file containing the Service Account credentials. + * You can omit this value to automatically read from + * GOOGLE_APPLICATION_CREDENTIALS env which is useful for local + * development. + */ + keyFilename?: string; + /** The path to use for the collection. Defaults to 'sessions' */ + path?: string; + /** Timeout used for database operations. Defaults to 10000ms */ + timeout?: number; + }; + }; + /** * The available auth-provider options and attributes */ diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 342718db55..8f7f623652 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -36,6 +36,7 @@ "@backstage/config": "^0.1.10", "@backstage/errors": "^0.1.1", "@backstage/test-utils": "^0.1.19", + "@google-cloud/firestore": "^4.15.1", "@types/express": "^4.17.6", "@types/passport": "^1.0.3", "compression": "^1.7.4", diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts new file mode 100644 index 0000000000..a7192ed4ab --- /dev/null +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.test.ts @@ -0,0 +1,184 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { Firestore } from '@google-cloud/firestore'; + +import { + DEFAULT_DOCUMENT_PATH, + DEFAULT_TIMEOUT_MS, + FirestoreKeyStore, + FirestoreKeyStoreSettings, +} from './FirestoreKeyStore'; +import { AnyJWK } from './types'; + +const data = jest.fn().mockReturnValue('data'); +const toDate = jest.fn().mockReturnValue('date'); +const get = jest.fn().mockReturnValue({ + docs: [{ data, createTime: { toDate } }], +}); +const set = jest.fn(); + +const firestoreMock = { + limit: jest.fn().mockReturnThis(), + collection: jest.fn().mockReturnThis(), + delete: jest.fn(), + doc: jest.fn().mockReturnThis(), + set, + get, +}; + +jest.mock('@google-cloud/firestore', () => ({ + Firestore: jest.fn().mockImplementation(() => firestoreMock), +})); + +describe('FirestoreKeyStore', () => { + const key = { + kid: '123', + use: 'sig', + kty: 'plain', + alg: 'Base64', + } as AnyJWK; + + const settings = { + projectId: 'my-project', + host: 'my-host', + port: 8080, + ssl: false, + keyFilename: 'cred.json', + }; + const path = 'my-path'; + const timeout = 10; + const firestoreSettings = { + ...settings, + path, + timeout, + } as FirestoreKeyStoreSettings; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('can create an instance without settings', async () => { + const keyStore = await FirestoreKeyStore.create(); + + expect(keyStore).toBeInstanceOf(FirestoreKeyStore); + expect(Firestore).toHaveBeenCalledWith({}); + }); + + it('can create an instance with settings', async () => { + await FirestoreKeyStore.create(firestoreSettings); + + expect(Firestore).toHaveBeenCalledWith(settings); + }); + + it('can verify that is has a connection to the database', async () => { + const keyStore = await FirestoreKeyStore.create(); + + await expect( + FirestoreKeyStore.verifyConnection(keyStore), + ).resolves.not.toThrow(); + }); + + it('can verify that it can not connect to the database', async () => { + const keyStore = await FirestoreKeyStore.create(); + firestoreMock.get = jest.fn().mockRejectedValue(new Error()); + + await expect( + FirestoreKeyStore.verifyConnection(keyStore), + ).rejects.toThrow(); + + firestoreMock.get = get; + }); + + it('can use a default timeout and path', async () => { + const keyStore = await FirestoreKeyStore.create(); + await keyStore.addKey(key); + + expect(setTimeout).toBeCalledWith(expect.any(Function), DEFAULT_TIMEOUT_MS); + expect(firestoreMock.collection).toBeCalledWith(DEFAULT_DOCUMENT_PATH); + }); + + it('can handle a timeout', async () => { + firestoreMock.set = jest + .fn() + .mockImplementation( + () => new Promise(resolve => setTimeout(resolve, 20)), + ); + const keyStore = await FirestoreKeyStore.create(firestoreSettings); + const add = keyStore.addKey(key); + + jest.advanceTimersByTime(50); + + await expect(add).rejects.toEqual( + new Error(`Operation timed out after ${timeout}ms`), + ); + + firestoreMock.set = set; + }); + + it('can add keys', async () => { + const keyStore = await FirestoreKeyStore.create(firestoreSettings); + await keyStore.addKey(key); + + expect(setTimeout).toBeCalledTimes(1); + expect(firestoreMock.collection).toBeCalledWith(path); + expect(firestoreMock.doc).toBeCalledWith(key.kid); + expect(firestoreMock.set).toHaveBeenCalledWith({ + kid: key.kid, + key: JSON.stringify(key), + }); + }); + + it('can delete a single key', async () => { + const keyStore = await FirestoreKeyStore.create(firestoreSettings); + await keyStore.removeKeys(['123']); + + expect(setTimeout).toBeCalledTimes(1); + expect(firestoreMock.collection).toBeCalledWith(path); + expect(firestoreMock.doc).toBeCalledWith('123'); + expect(firestoreMock.delete).toBeCalledTimes(1); + }); + + it('can delete a multiple keys', async () => { + const keyStore = await FirestoreKeyStore.create(firestoreSettings); + await keyStore.removeKeys(['123', '456']); + + expect(setTimeout).toBeCalledTimes(2); + expect(firestoreMock.collection).toBeCalledWith(path); + expect(firestoreMock.doc).toBeCalledWith('123'); + expect(firestoreMock.doc).toBeCalledWith('456'); + expect(firestoreMock.delete).toBeCalledTimes(2); + }); + + it('can list keys', async () => { + const keyStore = await FirestoreKeyStore.create(firestoreSettings); + const items = await keyStore.listKeys(); + + expect(setTimeout).toBeCalledTimes(1); + expect(firestoreMock.collection).toBeCalledWith(path); + expect(firestoreMock.get).toBeCalledTimes(1); + expect(data).toBeCalledTimes(1); + expect(toDate).toBeCalledTimes(1); + expect(items).toMatchObject({ + items: [{ key: 'data', createdAt: 'date' }], + }); + }); +}); diff --git a/plugins/auth-backend/src/identity/FirestoreKeyStore.ts b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts new file mode 100644 index 0000000000..a5479a6470 --- /dev/null +++ b/plugins/auth-backend/src/identity/FirestoreKeyStore.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { Logger } from 'winston'; +import { + DocumentData, + Firestore, + QuerySnapshot, + Settings, + WriteResult, +} from '@google-cloud/firestore'; + +import { AnyJWK, KeyStore, StoredKey } from './types'; + +export type FirestoreKeyStoreSettings = Settings & Options; + +type Options = { + path?: string; + timeout?: number; +}; + +export const DEFAULT_TIMEOUT_MS = 10000; +export const DEFAULT_DOCUMENT_PATH = 'sessions'; + +export class FirestoreKeyStore implements KeyStore { + static async create( + settings?: FirestoreKeyStoreSettings, + ): Promise { + const { path, timeout, ...firestoreSettings } = settings ?? {}; + const database = new Firestore(firestoreSettings); + + return new FirestoreKeyStore( + database, + path ?? DEFAULT_DOCUMENT_PATH, + timeout ?? DEFAULT_TIMEOUT_MS, + ); + } + + private constructor( + private readonly database: Firestore, + private readonly path: string, + private readonly timeout: number, + ) {} + + static async verifyConnection( + keyStore: FirestoreKeyStore, + logger?: Logger, + ): Promise { + try { + await keyStore.verify(); + } catch (error) { + if (process.env.NODE_ENV !== 'development') { + throw new Error( + `Failed to connect to database: ${(error as Error).message}`, + ); + } + logger?.warn( + `Failed to connect to database: ${(error as Error).message}`, + ); + } + } + + async addKey(key: AnyJWK): Promise { + await this.withTimeout( + this.database + .collection(this.path) + .doc(key.kid) + .set({ + kid: key.kid, + key: JSON.stringify(key), + }), + ); + } + + async listKeys(): Promise<{ items: StoredKey[] }> { + const keys = await this.withTimeout>( + this.database.collection(this.path).get(), + ); + + return { + items: keys.docs.map(key => ({ + key: key.data() as AnyJWK, + createdAt: key.createTime.toDate(), + })), + }; + } + + async removeKeys(kids: string[]): Promise { + // This is probably really slow, but it's done async in the background + for (const kid of kids) { + await this.withTimeout( + this.database.collection(this.path).doc(kid).delete(), + ); + } + + /** + * This could be achieved with batching but there's a couple of limitations with that: + * + * - A batched write can contain a maximum of 500 operations + * https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes + * + * - The "in" operator can combine a maximum of 10 equality clauses + * https://firebase.google.com/docs/firestore/query-data/queries#in_not-in_and_array-contains-any + * + * Example: + * + * const batch = this.database.batch(); + * const docs = await this.database + * .collection(this.path) + * .where('kid', 'in', kids) + * .get(); + * docs.forEach(doc => { + * batch.delete(doc.ref); + * }); + * await batch.commit(); + * + */ + } + + /** + * Helper function to allow us to modify the timeout used when + * performing Firestore database operations. + * + * The reason for this is that it seems that there's no other + * practical solution to change the default timeout of 10mins + * that Firestore has. + * + */ + private async withTimeout(operation: Promise): Promise { + const timer = new Promise((_, reject) => + setTimeout(() => { + reject(new Error(`Operation timed out after ${this.timeout}ms`)); + }, this.timeout), + ); + return Promise.race([operation, timer]); + } + + /** + * Used to verify that the database is reachable. + */ + private async verify(): Promise { + await this.withTimeout(this.database.collection(this.path).limit(1).get()); + } +} diff --git a/plugins/auth-backend/src/identity/KeyStores.test.ts b/plugins/auth-backend/src/identity/KeyStores.test.ts new file mode 100644 index 0000000000..f30263e27f --- /dev/null +++ b/plugins/auth-backend/src/identity/KeyStores.test.ts @@ -0,0 +1,98 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { DatabaseManager } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; + +import { MemoryKeyStore } from './MemoryKeyStore'; +import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { KeyStores } from './KeyStores'; + +describe('KeyStores', () => { + const defaultConfigOptions = { + auth: { + keyStore: { + provider: 'memory', + }, + }, + }; + const defaultConfig = new ConfigReader(defaultConfigOptions); + + it('reads auth section from config', async () => { + const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig'); + const keyStore = await KeyStores.fromConfig(defaultConfig); + + expect(keyStore).toBeInstanceOf(MemoryKeyStore); + expect(configSpy).toHaveBeenCalledWith('auth.keyStore'); + expect( + defaultConfig + .getOptionalConfig('auth.keyStore') + ?.getOptionalString('provider'), + ).toBe(defaultConfigOptions.auth.keyStore.provider); + }); + + it('can handle without auth config', async () => { + const config = new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }); + const database = + DatabaseManager.fromConfig(config).forPlugin('auth-backend'); + const keyStore = await KeyStores.fromConfig(config, { database }); + + expect(keyStore).toBeInstanceOf(DatabaseKeyStore); + }); + + it('can handle additional provider config', async () => { + jest.spyOn(FirestoreKeyStore, 'verifyConnection').mockResolvedValue(); + const createSpy = jest.spyOn(FirestoreKeyStore, 'create'); + + const configOptions = { + auth: { + keyStore: { + provider: 'firestore', + firestore: { + projectId: 'my-project', + keyFilename: 'cred.json', + path: 'my-path', + timeout: 100, + host: 'localhost', + port: 8088, + ssl: false, + }, + }, + }, + }; + const config = new ConfigReader(configOptions); + const keyStore = await KeyStores.fromConfig(config); + + expect(keyStore).toBeInstanceOf(FirestoreKeyStore); + expect(createSpy).toHaveBeenCalledWith( + configOptions.auth.keyStore.firestore, + ); + expect( + config + .getOptionalConfig('auth.keyStore') + ?.getOptionalConfig('firestore') + ?.getOptionalString('projectId'), + ).toBe(configOptions.auth.keyStore.firestore.projectId); + }); +}); diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts new file mode 100644 index 0000000000..74e60a0502 --- /dev/null +++ b/plugins/auth-backend/src/identity/KeyStores.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2021 The Backstage Authors + * + * 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 { Logger } from 'winston'; + +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { Config } from '@backstage/config'; + +import { DatabaseKeyStore } from './DatabaseKeyStore'; +import { MemoryKeyStore } from './MemoryKeyStore'; +import { FirestoreKeyStore } from './FirestoreKeyStore'; +import { KeyStore } from './types'; + +type Options = { + logger?: Logger; + database?: PluginDatabaseManager; +}; + +export class KeyStores { + /** + * Looks at the `auth.keyStore` section in the application configuration + * and returns a KeyStore store. Defaults to `database` + * + * @returns a KeyStore store + */ + static async fromConfig( + config: Config, + options?: Options, + ): Promise { + const { logger, database } = options ?? {}; + + const ks = config.getOptionalConfig('auth.keyStore'); + const provider = ks?.getOptionalString('provider') ?? 'database'; + + logger?.info(`Configuring "${provider}" as KeyStore provider`); + + if (provider === 'database') { + if (!database) { + throw new Error('This KeyStore provider requires a database'); + } + + return await DatabaseKeyStore.create({ + database: await database.getClient(), + }); + } + + if (provider === 'memory') { + return new MemoryKeyStore(); + } + + if (provider === 'firestore') { + const settings = ks?.getConfig(provider); + + const keyStore = await FirestoreKeyStore.create({ + projectId: settings?.getOptionalString('projectId'), + keyFilename: settings?.getOptionalString('keyFilename'), + host: settings?.getOptionalString('host'), + port: settings?.getOptionalNumber('port'), + ssl: settings?.getOptionalBoolean('ssl'), + path: settings?.getOptionalString('path'), + timeout: settings?.getOptionalNumber('timeout'), + }); + + await FirestoreKeyStore.verifyConnection(keyStore, logger); + + return keyStore; + } + + throw new Error(`Unknown KeyStore provider: ${provider}`); + } +} diff --git a/plugins/auth-backend/src/identity/index.ts b/plugins/auth-backend/src/identity/index.ts index d88339e8e4..73858d7e07 100644 --- a/plugins/auth-backend/src/identity/index.ts +++ b/plugins/auth-backend/src/identity/index.ts @@ -18,4 +18,7 @@ export { createOidcRouter } from './router'; export { IdentityClient } from './IdentityClient'; export { TokenFactory } from './TokenFactory'; export { DatabaseKeyStore } from './DatabaseKeyStore'; +export { MemoryKeyStore } from './MemoryKeyStore'; +export { FirestoreKeyStore } from './FirestoreKeyStore'; +export { KeyStores } from './KeyStores'; export type { KeyStore, TokenIssuer, TokenParams } from './types'; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 90678da4e4..4aa9ed8778 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -29,7 +29,7 @@ import { import { assertError, NotFoundError } from '@backstage/errors'; import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; -import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; +import { createOidcRouter, TokenFactory, KeyStores } from '../identity'; import session from 'express-session'; import passport from 'passport'; import { Minimatch } from 'minimatch'; @@ -56,11 +56,9 @@ export async function createRouter({ const appUrl = config.getString('app.baseUrl'); const authUrl = await discovery.getExternalBaseUrl('auth'); + const keyStore = await KeyStores.fromConfig(config, { logger, database }); const keyDurationSeconds = 3600; - const keyStore = await DatabaseKeyStore.create({ - database: await database.getClient(), - }); const tokenIssuer = new TokenFactory({ issuer: authUrl, keyStore, diff --git a/yarn.lock b/yarn.lock index 5b4d206979..c905fbea6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2971,6 +2971,16 @@ dependencies: google-gax "^2.12.0" +"@google-cloud/firestore@^4.15.1": + version "4.15.1" + resolved "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-4.15.1.tgz#ed764fc76823ce120e68fe8c27ef1edd0650cd93" + integrity sha512-2PWsCkEF1W02QbghSeRsNdYKN1qavrHBP3m72gPDMHQSYrGULOaTi7fSJquQmAtc4iPVB2/x6h80rdLHTATQtA== + dependencies: + fast-deep-equal "^3.1.1" + functional-red-black-tree "^1.0.1" + google-gax "^2.24.1" + protobufjs "^6.8.6" + "@google-cloud/paginator@^3.0.0": version "3.0.5" resolved "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-3.0.5.tgz#9d6b96c421a89bd560c1bc2c197c7611ef21db6c" @@ -15238,6 +15248,21 @@ google-auth-library@^7.0.0, google-auth-library@^7.0.2: jws "^4.0.0" lru-cache "^6.0.0" +google-auth-library@^7.6.1: + version "7.10.0" + resolved "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.10.0.tgz#6ab852f8e1abbe425aec695ac6501f80bd5eba56" + integrity sha512-ICsqaU+lxMHVlDUzMrfVIEqnARw2AwBiZ/2KnNM6BcTf9Nott+Af87DTIzmlnW865p3REUP2MVL0xkPC3a61aQ== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + google-gax@^2.12.0: version "2.14.1" resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.14.1.tgz#74885c5d9f01db412917fc49bbf20c4884828d36" @@ -15256,6 +15281,25 @@ google-gax@^2.12.0: protobufjs "^6.10.2" retry-request "^4.0.0" +google-gax@^2.24.1: + version "2.27.0" + resolved "https://registry.npmjs.org/google-gax/-/google-gax-2.27.0.tgz#78ba655ae7707cb92ba37c35932381b5970c1772" + integrity sha512-xcLCeNKCqNm/w0At7/vdZHV/zol/iRS+PSAZTu7i6xNGBra/kWI3cfn4M6ZLQXeUEGbTVLJ4zGm53TVc4lvbDA== + dependencies: + "@grpc/grpc-js" "~1.3.0" + "@grpc/proto-loader" "^0.6.1" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" + duplexify "^4.0.0" + fast-text-encoding "^1.0.3" + google-auth-library "^7.6.1" + is-stream-ended "^0.1.4" + node-fetch "^2.6.1" + object-hash "^2.1.1" + proto3-json-serializer "^0.1.1" + protobufjs "6.11.2" + retry-request "^4.0.0" + google-p12-pem@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz#673ac3a75d3903a87f05878f3c75e06fc151669e" @@ -20307,6 +20351,7 @@ minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== dependencies: + encoding "^0.1.12" minipass "^3.1.0" minipass-sized "^1.0.3" minizlib "^2.0.0" @@ -23148,7 +23193,12 @@ proto-list@~1.2.1: resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= -protobufjs@^6.10.0, protobufjs@^6.10.2: +proto3-json-serializer@^0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-0.1.4.tgz#aa2dc4c9c9b7ea05631354b2c2e52c227539a7f0" + integrity sha512-bFzdsKU/zaTobWrRxRniMZIzzcgKYlmBWL1gAcTXZ2M7TQTGPI0JoYYs6bN7tpWj59ZCfwg7Ii/A2e8BbQGYnQ== + +protobufjs@6.11.2, protobufjs@^6.10.0, protobufjs@^6.10.2, protobufjs@^6.8.6: version "6.11.2" resolved "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==