diff --git a/package.json b/package.json index c58eb40d0a..dab22314d3 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "json5": "^2.1.0", "jsonschema": "1.2.2", "jwt-decode": "^2.2.0", - "klaro": "^0.5.34", + "klaro": "^0.6.3", "moment": "^2.22.1", "morgan": "^1.9.1", "ng-mocks": "^8.1.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index e3b7149740..dc164476d4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -31,8 +31,8 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { environment } from '../environments/environment'; import { models } from './core/core.module'; import { LocaleService } from './core/locale/locale.service'; -import { KlaroService } from './shared/cookies/klaro.service'; import { hasValue } from './shared/empty.util'; +import { KlaroService } from './shared/cookies/klaro.service'; @Component({ selector: 'ds-app', diff --git a/src/app/footer/footer.component.ts b/src/app/footer/footer.component.ts index ef11093c7b..6ece2cf08b 100644 --- a/src/app/footer/footer.component.ts +++ b/src/app/footer/footer.component.ts @@ -1,6 +1,6 @@ import { Component, Optional } from '@angular/core'; -import { KlaroService } from '../shared/cookies/klaro.service'; import { hasValue } from '../shared/empty.util'; +import { KlaroService } from '../shared/cookies/klaro.service'; @Component({ selector: 'ds-footer', diff --git a/src/app/shared/cookies/klaro.service.spec.ts b/src/app/shared/cookies/browser-klaro.service.spec.ts similarity index 61% rename from src/app/shared/cookies/klaro.service.spec.ts rename to src/app/shared/cookies/browser-klaro.service.spec.ts index 0c1f5b368e..e33f497d73 100644 --- a/src/app/shared/cookies/klaro.service.spec.ts +++ b/src/app/shared/cookies/browser-klaro.service.spec.ts @@ -1,12 +1,12 @@ import { TestBed } from '@angular/core/testing'; -import { KlaroService } from './klaro.service'; +import { BrowserKlaroService } from './browser-klaro.service'; describe('KlaroService', () => { beforeEach(() => TestBed.configureTestingModule({})); it('should be created', () => { - const service: KlaroService = TestBed.get(KlaroService); + const service: BrowserKlaroService = TestBed.get(BrowserKlaroService); expect(service).toBeTruthy(); }); }); diff --git a/src/app/shared/cookies/browser-klaro.service.ts b/src/app/shared/cookies/browser-klaro.service.ts new file mode 100644 index 0000000000..d62e0a29cd --- /dev/null +++ b/src/app/shared/cookies/browser-klaro.service.ts @@ -0,0 +1,324 @@ +import { Injectable } from '@angular/core'; +import * as Klaro from 'klaro' +import { BehaviorSubject, Observable } from 'rxjs'; +import { TOKENITEM } from '../../core/auth/models/auth-token-info.model'; +import { AuthService, IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service'; +import { LANG_COOKIE } from '../../core/locale/locale.service'; +import { TranslateService } from '@ngx-translate/core'; +import { environment } from '../../../environments/environment'; +import { map, switchMap, take, tap } from 'rxjs/operators'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; +import { KlaroService } from './klaro.service'; +import { hasValue } from '../empty.util'; +import { CookieService } from '../../core/services/cookie.service'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; + +export const HAS_AGREED_END_USER = 'dsHasAgreedEndUser'; +export const COOKIE_MDFIELD = 'dspace.agreements.cookies'; +const cookieNameMessagePrefix = 'cookies.consent.app.title.'; +const cookieDescriptionMessagePrefix = 'cookies.consent.app.description.'; +const cookiePurposeMessagePrefix = 'cookies.consent.purpose.'; + +@Injectable() +export class BrowserKlaroService extends KlaroService { + + message$: BehaviorSubject = new BehaviorSubject(''); + + klaroConfig: any = { + storageName: this.getStorageName('anonymous'), + + privacyPolicy: '/info/privacy', + + /* + Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in + the consent notice. We strongly advise against using this under most + circumstances, as it keeps the user from customizing his/her consent choices. + */ + hideLearnMore: false, + + /* + Setting 'acceptAll' to 'true' will show an "accept all" button in the notice and + modal, which will enable all third-party apps if the user clicks on it. If set + to 'false', there will be an "accept" button that will only enable the apps that + are enabled in the consent modal. + */ + acceptAll: true, + + /* + You can also set a custom expiration time for the Klaro cookie. By default, it + will expire after 30 days. Only relevant if 'storageMethod' is set to 'cookie'. + */ + cookieExpiresAfterDays: 365, + + htmlTexts: true, + + /* + You can overwrite existing translations and add translations for your app + descriptions and purposes. See `src/translations/` for a full list of + translations that can be overwritten: + https://github.com/KIProtect/klaro/tree/master/src/translations + */ + translations: { + en: { + acceptAll: 'cookies.consent.accept-all', + acceptSelected: 'cookies.consent.accept-selected', + app: { + optOut: { + description: 'cookies.consent.app.opt-out.description', + title: 'cookies.consent.app.opt-out.title' + }, + purpose: 'cookies.consent.app.purpose', + purposes: 'cookies.consent.app.purposes', + required: { + description: 'cookies.consent.app.required.description', + title: 'cookies.consent.app.required.title' + } + }, + close: 'cookies.consent.close', + decline: 'cookies.consent.decline', + consentNotice: { + description: 'cookies.consent.content-notice.description', + learnMore: 'cookies.consent.content-notice.learnMore' + }, + consentModal: { + description: 'cookies.consent.content-modal.description', + privacyPolicy: { + name: 'cookies.consent.content-modal.privacy-policy.name', + text: 'cookies.consent.content-modal.privacy-policy.text' + }, + title: 'cookies.consent.content-modal.title' + }, + purposes: {} + } + }, + apps: [ + { + name: 'token_item', + purposes: ['authentication'], + required: true, + cookies: [ + TOKENITEM + ] + }, + { + name: 'impersonation', + purposes: ['authentication'], + required: true, + cookies: [ + IMPERSONATING_COOKIE + ] + }, + { + name: 'redirect', + purposes: ['authentication'], + required: true, + cookies: [ + REDIRECT_COOKIE + ] + }, + { + name: 'language', + purposes: ['preferences'], + required: true, + cookies: [ + LANG_COOKIE + ] + }, + { + name: 'klaro', + purposes: ['acknowledgement'], + required: true, + cookies: [ + [/^klaro-.+$/], + ] + }, + { + name: 'has_agreed_end_user', + purposes: ['acknowledgement'], + required: true, + cookies: [ + HAS_AGREED_END_USER + ] + }, + { + name: 'google-analytics', + purposes: ['statistics'], + required: false, + cookies: [ + // /* + // you an either only provide a cookie name or regular expression (regex) or a list + // consisting of a name or regex, a path and a cookie domain. Providing a path and + // domain is necessary if you have apps that set cookies for a path that is not + // "/", or a domain that is not the current domain. If you do not set these values + // properly, the cookie can't be deleted by Klaro, as there is no way to access the + // path or domain of a cookie in JS. Notice that it is not possible to delete + // cookies that were set on a third-party domain, or cookies that have the HTTPOnly + // attribute: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#new- + // cookie_domain + // */ + // + // /* + // This rule will match cookies that contain the string '_pk_' and that are set on + // the path '/' and the domain 'klaro.kiprotect.com' + // */ + [/^_ga.?$/], + [/^_gid$/], + // + // /* + // Same as above, only for the 'localhost' domain + // */ + // [/^_pk_.*$/, '/', 'localhost'], + // + // /* + // This rule will match all cookies named 'piwik_ignore' that are set on the path + // '/' on the current domain + // */ + // 'piwik_ignore', + ], + + /* + You can define an optional callback function that will be called each time the + consent state for the given app changes. The consent value will be passed as the + first parameter to the function (true=consented). The `app` config will be + passed as the second parameter. + */ + callback: (consent, app) => { + console.log(consent, app); + this.message$.next('User consent for app ' + app.name + ': consent=' + consent); + }, + /* + If 'onlyOnce' is set to 'true', the app will only be executed once regardless + how often the user toggles it on and off. This is relevant e.g. for tracking + scripts that would generate new page view events every time Klaro disables and + re-enables them due to a consent change by the user. + */ + onlyOnce: true, + }, + ], + }; + + constructor( + private translateService: TranslateService, + private authService: AuthService, + private ePersonService: EPersonDataService, + private cookieService: CookieService) { + super(); + } + + initialize() { + + /** + * Make sure the fallback language is english + */ + this.translateService.setDefaultLang(environment.defaultLanguage); + + const user$: Observable = this.authService.isAuthenticated() + .pipe( + take(1), + switchMap((loggedIn: boolean) => { + if (loggedIn) { + return this.authService.getAuthenticatedUserFromStore(); + } + return observableOf(undefined); + }), + take(1) + ); + + const translationServiceReady$ = this.translateService.get('loading.default').pipe(take(1)); + + observableCombineLatest(user$, translationServiceReady$) + .subscribe(([user, translation]: [EPerson, string]) => { + if (hasValue(user)) { + this.klaroConfig.callback = (consent, app) => this.updateSettingsForUsers(user); + this.klaroConfig.storageName = this.getStorageName(user.uuid); + } + + /** + * Add all message keys for apps and purposes + */ + this.addAppMessages(); + + /** + * Subscribe on a message to make sure the translation service is ready + * Translate all keys in the translation section of the configuration + * Show the configuration if the configuration has not been confirmed + */ + this.translateConfiguration(); + Klaro.renderKlaro(this.klaroConfig, false); + Klaro.initialize(); + }); + + } + + private getTitleTranslation(title: string) { + return cookieNameMessagePrefix + title; + } + + private getDescriptionTranslation(description: string) { + return cookieDescriptionMessagePrefix + description; + } + + private getPurposeTranslation(purpose: string) { + return cookiePurposeMessagePrefix + purpose; + } + + /** + * Show the cookie consent form + */ + showSettings() { + Klaro.show(this.klaroConfig); + } + + /** + * Add message keys for all apps and purposes + */ + addAppMessages() { + this.klaroConfig.apps.forEach((app) => { + this.klaroConfig.translations.en[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) }; + app.purposes.forEach((purpose) => { + this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose); + }) + }); + } + + /** + * Translate the translation section from the Klaro configuration + */ + translateConfiguration() { + this.translate(this.klaroConfig.translations.en); + } + + private translate(object) { + if (typeof (object) === 'string') { + return this.translateService.instant(object); + } + Object.entries(object).forEach(([key, value]: [string, any]) => { + object[key] = this.translate(value); + }); + return object; + } + + getSettingsForUser(user: EPerson) { + return JSON.parse(user.firstMetadataValue(COOKIE_MDFIELD)); + } + + setSettingsForUser(user: EPerson, config: object) { + user.setMetadata(COOKIE_MDFIELD, undefined, JSON.stringify(config)); + this.ePersonService.update(user); + } + + restoreSettingsForUsers(user: EPerson) { + console.log('restore klaro', user); + this.cookieService.set(this.getStorageName(user.uuid), this.getSettingsForUser(user)); + } + + updateSettingsForUsers(user: EPerson) { + console.log('update klaro', user); + this.setSettingsForUser(user, this.cookieService.get(this.getStorageName(user.uuid))) + } + + getStorageName(identifier: string) { + return 'klaro-' + identifier + } +} diff --git a/src/app/shared/cookies/klaro.effects.ts b/src/app/shared/cookies/klaro.effects.ts new file mode 100644 index 0000000000..5409779978 --- /dev/null +++ b/src/app/shared/cookies/klaro.effects.ts @@ -0,0 +1,3 @@ +export class KlaroEffects { + +} diff --git a/src/app/shared/cookies/klaro.service.ts b/src/app/shared/cookies/klaro.service.ts index ffae51969c..1e4f348d2b 100644 --- a/src/app/shared/cookies/klaro.service.ts +++ b/src/app/shared/cookies/klaro.service.ts @@ -1,317 +1,7 @@ import { Injectable } from '@angular/core'; -import * as Klaro from 'klaro' -import { BehaviorSubject, Observable } from 'rxjs'; -import { TOKENITEM } from '../../core/auth/models/auth-token-info.model'; -import { AuthService, IMPERSONATING_COOKIE, REDIRECT_COOKIE } from '../../core/auth/auth.service'; -import { LANG_COOKIE } from '../../core/locale/locale.service'; -import { TranslateService } from '@ngx-translate/core'; -import { environment } from '../../../environments/environment'; -import { map, switchMap, take } from 'rxjs/operators'; -import { EPerson } from '../../core/eperson/models/eperson.model'; -import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; - -export const HAS_AGREED_END_USER = 'dsHasAgreedEndUser'; -export const COOKIE_MDFIELD = 'dspace.agreements.cookies'; -const cookieNameMessagePrefix = 'cookies.consent.app.title.'; -const cookieDescriptionMessagePrefix = 'cookies.consent.app.description.'; -const cookiePurposeMessagePrefix = 'cookies.consent.purpose.'; @Injectable() -export class KlaroService { - - message$: BehaviorSubject = new BehaviorSubject(''); - - klaroConfig = { - storageName: 'klaro-anonymous', - - privacyPolicy: '/info/privacy', - - /* - Setting 'hideLearnMore' to 'true' will hide the "learn more / customize" link in - the consent notice. We strongly advise against using this under most - circumstances, as it keeps the user from customizing his/her consent choices. - */ - hideLearnMore: false, - - /* - Setting 'acceptAll' to 'true' will show an "accept all" button in the notice and - modal, which will enable all third-party apps if the user clicks on it. If set - to 'false', there will be an "accept" button that will only enable the apps that - are enabled in the consent modal. - */ - acceptAll: true, - - /* - You can also set a custom expiration time for the Klaro cookie. By default, it - will expire after 30 days. Only relevant if 'storageMethod' is set to 'cookie'. - */ - cookieExpiresAfterDays: 365, - - htmlTexts: true, - - /* - You can overwrite existing translations and add translations for your app - descriptions and purposes. See `src/translations/` for a full list of - translations that can be overwritten: - https://github.com/KIProtect/klaro/tree/master/src/translations - */ - translations: { - en: { - acceptAll: 'cookies.consent.accept-all', - acceptSelected: 'cookies.consent.accept-selected', - app: { - optOut: { - description: 'cookies.consent.app.opt-out.description', - title: 'cookies.consent.app.opt-out.title' - }, - purpose: 'cookies.consent.app.purpose', - purposes: 'cookies.consent.app.purposes', - required: { - description: 'cookies.consent.app.required.description', - title: 'cookies.consent.app.required.title' - } - }, - close: 'cookies.consent.close', - decline: 'cookies.consent.decline', - consentNotice: { - description: 'cookies.consent.content-notice.description', - learnMore: 'cookies.consent.content-notice.learnMore' - }, - consentModal: { - description: 'cookies.consent.content-modal.description', - privacyPolicy: { - name: 'cookies.consent.content-modal.privacy-policy.name', - text: 'cookies.consent.content-modal.privacy-policy.text' - }, - title: 'cookies.consent.content-modal.title' - }, - purposes: {} - } - }, - apps: [ - { - name: 'token_item', - purposes: ['authentication'], - required: true, - cookies: [ - TOKENITEM - ] - }, - { - name: 'impersonation', - purposes: ['authentication'], - required: true, - cookies: [ - IMPERSONATING_COOKIE - ] - }, - { - name: 'redirect', - purposes: ['authentication'], - required: true, - cookies: [ - REDIRECT_COOKIE - ] - }, - { - name: 'language', - purposes: ['preferences'], - required: true, - cookies: [ - LANG_COOKIE - ] - }, - { - name: 'klaro', - purposes: ['acknowledgement'], - required: true, - cookies: [ - [/^klaro-.+$/], - ] - }, - { - name: 'has_agreed_end_user', - purposes: ['acknowledgement'], - required: true, - cookies: [ - HAS_AGREED_END_USER - ] - }, - { - name: 'google-analytics', - purposes: ['statistics'], - required: false, - cookies: [ - // /* - // you an either only provide a cookie name or regular expression (regex) or a list - // consisting of a name or regex, a path and a cookie domain. Providing a path and - // domain is necessary if you have apps that set cookies for a path that is not - // "/", or a domain that is not the current domain. If you do not set these values - // properly, the cookie can't be deleted by Klaro, as there is no way to access the - // path or domain of a cookie in JS. Notice that it is not possible to delete - // cookies that were set on a third-party domain, or cookies that have the HTTPOnly - // attribute: https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#new- - // cookie_domain - // */ - // - // /* - // This rule will match cookies that contain the string '_pk_' and that are set on - // the path '/' and the domain 'klaro.kiprotect.com' - // */ - [/^_ga.?$/], - [/^_gid$/], - // - // /* - // Same as above, only for the 'localhost' domain - // */ - // [/^_pk_.*$/, '/', 'localhost'], - // - // /* - // This rule will match all cookies named 'piwik_ignore' that are set on the path - // '/' on the current domain - // */ - // 'piwik_ignore', - ], - - /* - You can define an optional callback function that will be called each time the - consent state for the given app changes. The consent value will be passed as the - first parameter to the function (true=consented). The `app` config will be - passed as the second parameter. - */ - callback: (consent, app) => { - console.log(consent, app); - this.message$.next('User consent for app ' + app.name + ': consent=' + consent); - }, - /* - If 'onlyOnce' is set to 'true', the app will only be executed once regardless - how often the user toggles it on and off. This is relevant e.g. for tracking - scripts that would generate new page view events every time Klaro disables and - re-enables them due to a consent change by the user. - */ - onlyOnce: true, - }, - ], - /* - You can define an optional callback function that will be called each time the - consent state for any given app changes. The consent value will be passed as the - first parameter to the function (true=consented). The `app` config will be - passed as the second parameter. - */ - callback: (consent, app) => { - - /* - You can define an optional callback function that will be called each time the - consent state for any given app changes. The consent value will be passed as the - first parameter to the function (true=consented). The `app` config will be - passed as the second parameter. - */ - console.log( - 'User consent for app ' + app.name + ': consent=' + consent - ); - // this.updateUserCookieConsent(); - }, - }; - - constructor( - private translateService: TranslateService, - private authService: AuthService) { - } - - initialize() { - - /** - * Make sure the fallback language is english - */ - this.translateService.setDefaultLang(environment.defaultLanguage); - - const storageName$: Observable = this.authService.isAuthenticated() - .pipe( - take(1), - switchMap((loggedIn: boolean) => { - if (loggedIn) { - return this.authService.getAuthenticatedUserFromStore().pipe(map((user: EPerson) => 'klaro-' + user.uuid), take(1)) - } else { - return observableOf('klaro-anonymous') - } - }) - ); - const translationServiceReady$ = this.translateService.get('loading.default').pipe(take(1)); - - observableCombineLatest(storageName$, translationServiceReady$) - .subscribe(([name, translation]: string[]) => { - this.klaroConfig.storageName = name; - - /** - * Add all message keys for apps and purposes - */ - this.addAppMessages(); - - /** - * Subscribe on a message to make sure the translation service is ready - * Translate all keys in the translation section of the configuration - * Show the configuration if the configuration has not been confirmed - */ - this.translateConfiguration(); - Klaro.renderKlaro(this.klaroConfig, false); - Klaro.initialize(); - }); - - } - - private getTitleTranslation(title: string) { - return cookieNameMessagePrefix + title; - } - - private getDescriptionTranslation(description: string) { - return cookieDescriptionMessagePrefix + description; - } - - private getPurposeTranslation(purpose: string) { - return cookiePurposeMessagePrefix + purpose; - } - - /** - * Show the cookie consent form - */ - showSettings() { - Klaro.show(this.klaroConfig); - } - - /** - * Add message keys for all apps and purposes - */ - addAppMessages() { - this.klaroConfig.apps.forEach((app) => { - this.klaroConfig.translations.en[app.name] = { title: this.getTitleTranslation(app.name), description: this.getDescriptionTranslation(app.name) }; - app.purposes.forEach((purpose) => { - this.klaroConfig.translations.en.purposes[purpose] = this.getPurposeTranslation(purpose); - }) - }); - } - - /** - * Translate the translation section from the Klaro configuration - */ - translateConfiguration() { - this.translate(this.klaroConfig.translations.en); - } - - private translate(object) { - if (typeof (object) === 'string') { - return this.translateService.instant(object); - } - Object.entries(object).forEach(([key, value]: [string, any]) => { - object[key] = this.translate(value); - }); - return object; - } - - getSettingsForUser(user: EPerson) { - return JSON.parse(user.firstMetadataValue(COOKIE_MDFIELD)); - } - - setSettingsForUser(user: EPerson, config: object) { - return user.setMetadata(COOKIE_MDFIELD, undefined, JSON.stringify(config)); - } +export abstract class KlaroService { + abstract initialize(); + abstract showSettings(); } diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index e00f998ba6..fad4a07b9d 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -21,6 +21,7 @@ import { AuthService } from '../../app/core/auth/auth.service'; import { Angulartics2RouterlessModule } from 'angulartics2/routerlessmodule'; import { SubmissionService } from '../../app/submission/submission.service'; import { StatisticsModule } from '../../app/statistics/statistics.module'; +import { BrowserKlaroService } from '../../app/shared/cookies/browser-klaro.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { HardRedirectService } from '../../app/core/services/hard-redirect.service'; import { BrowserHardRedirectService } from '../../app/core/services/browser-hard-redirect.service'; @@ -86,7 +87,7 @@ export function locationProvider(): Location { }, { provide: KlaroService, - useClass: KlaroService + useClass: BrowserKlaroService }, { provide: SubmissionService, diff --git a/tsconfig.server.json b/tsconfig.server.json index 979f5c2743..1329b32ace 100644 --- a/tsconfig.server.json +++ b/tsconfig.server.json @@ -6,15 +6,5 @@ }, "angularCompilerOptions": { "entryModule": "./src/modules/app/server-app.module#ServerAppModule" - }, - "exclude": [ - "src/test.ts", - "src/**/*.spec.ts", - "src/**/*.mock.ts", - "src/**/*.test.ts", - "src/**/*.stub.ts", - "src/**/testing/*", - "src/**/mocks/*", - "node_modules/klaro/*" - ], + } } diff --git a/webpack.server.config.js b/webpack.server.config.js index e762c14cce..7df6d6b83c 100644 --- a/webpack.server.config.js +++ b/webpack.server.config.js @@ -25,11 +25,6 @@ module.exports = { module: { noParse: /polyfills-.*\.js/, rules: [ - { - test: /\.js$/, - exclude: [/node_modules\/klaro/], - }, - { test: /\.ts$/, loader: 'ts-loader', options: { diff --git a/webpack/webpack.prod.ts b/webpack/webpack.prod.ts index ddb7f03403..e9fba2e65e 100644 --- a/webpack/webpack.prod.ts +++ b/webpack/webpack.prod.ts @@ -18,15 +18,6 @@ module.exports = Object.assign({}, commonExports, { recordsOutputPath: projectRoot('webpack.records.json'), entry: buildRoot('./main.server.ts'), target: 'node', - module: { - rules: [ - ...commonExports.module.rules, - { - test: /\.js$/, - exclude: [/node_modules\/klaro/], - } - ] - }, externals: [nodeExternals({ whitelist: [ /@angular/, diff --git a/yarn.lock b/yarn.lock index 5f918fd7e0..b06ce40b71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6165,10 +6165,10 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -klaro@^0.5.34: - version "0.5.34" - resolved "https://registry.yarnpkg.com/klaro/-/klaro-0.5.34.tgz#0b524be96a1bb177fe88ff2603e1be75f494fc98" - integrity sha512-M6KHqlBWpMyYoxOK1icoJMeYsaPT7YhzJIAQ3wdxZWGgBc0sV7xQsf0PsgMUVnuTD0AeC58QegCGEv0qYeq4gw== +klaro@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/klaro/-/klaro-0.6.3.tgz#b2aaa810d17f073c9a1b5eab618a9f13d5f40fa3" + integrity sha512-rRP37FaJaHHSScHIe3YUdMZJ1asxOF5+C/RMrFB2RzhAUfGVMM5/GiucECM3Si1lhW2LL0xGVymE8JhYZl2Bjg== last-call-webpack-plugin@^3.0.0: version "3.0.0"