@schibsted/account-sdk-browser 5.2.7 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schibsted/account-sdk-browser",
3
- "version": "5.2.7",
3
+ "version": "5.3.0",
4
4
  "description": "Schibsted account SDK for browsers",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -8,8 +8,11 @@
8
8
  "build": "./build.sh",
9
9
  "clean": "rimraf .cache coverage dist docs",
10
10
  "docs": "rimraf docs && jsdoc -c ./jsdoc.conf.json --verbose",
11
+ "docs:build": "npm run docs",
11
12
  "lint": "eslint .",
12
13
  "lint:fix": "eslint . --fix",
14
+ "prepare": "npm run vendor",
15
+ "vendor": "node scripts/vendor.js",
13
16
  "test": "jest",
14
17
  "cover": "jest --coverage"
15
18
  },
@@ -21,9 +24,11 @@
21
24
  "devDependencies": {
22
25
  "@babel/core": "^7.11.4",
23
26
  "@babel/preset-env": "^7.23.2",
27
+ "@schibsted/consent-url-utils": "^2.0.1",
24
28
  "babel-loader": "^8.1.0",
25
29
  "core-js": "^3.6.5",
26
30
  "docdash": "git+https://github.com/torarvid/docdash.git#v0.5.0",
31
+ "esbuild": "^0.28.2",
27
32
  "eslint": "^6.8.0",
28
33
  "eslint-plugin-import": "^2.20.2",
29
34
  "jest": "^26.4.2",
@@ -62,5 +67,8 @@
62
67
  ]
63
68
  ]
64
69
  },
65
- "typings": "index.d.ts"
70
+ "typings": "index.d.ts",
71
+ "publishConfig": {
72
+ "registry": "https://registry.npmjs.org/"
73
+ }
66
74
  }
@@ -0,0 +1,87 @@
1
+ /* Copyright 2026 Schibsted Products & Technology AS. Licensed under the terms of the MIT license.
2
+ * See LICENSE.md in the project root.
3
+ */
4
+
5
+ 'use strict';
6
+
7
+ import { getAndSerializeConsentsSync } from './vendor/consent-url-utils.js';
8
+
9
+ const LINK_ACTIVATION_EVENTS = ['click', 'auxclick'];
10
+
11
+ /**
12
+ * Reads consents from the Schibsted Sourcepoint integration, the default consent reader.
13
+ * Pages without the integration have no consents to forward, so they are skipped before
14
+ * `getAndSerializeConsentsSync` would warn about the missing consent manager on every login URL.
15
+ * @private
16
+ * @return {URLSearchParams|null}
17
+ */
18
+ export function readSourcepointConsents() {
19
+ if (!window._tcf_ && !window._scc_) {
20
+ return null;
21
+ }
22
+ return getAndSerializeConsentsSync();
23
+ }
24
+
25
+ /**
26
+ * Sets the given parameters on a URL, replacing previous values of the same parameters.
27
+ * @private
28
+ * @param {string} href
29
+ * @param {URLSearchParams} consentParams
30
+ * @return {string} - The URL with the parameters replaced
31
+ */
32
+ export function replaceConsentParams(href, consentParams) {
33
+ const url = new URL(href);
34
+ // Two passes on purpose: the reader may return repeated keys (e.g. `purpose=a&purpose=b`).
35
+ // A single `set()` pass would keep only the last value, and a single `append()` pass would keep
36
+ // stale values of the link. So first drop every stale value, then append all fresh ones.
37
+ consentParams.forEach((_, key) => {
38
+ url.searchParams.delete(key);
39
+ });
40
+ consentParams.forEach((value, key) => {
41
+ url.searchParams.append(key, value);
42
+ });
43
+ return url.href;
44
+ }
45
+
46
+ /**
47
+ * Finds the link that was activated by a click, including links inside open shadow roots.
48
+ * @private
49
+ * @param {Event} event
50
+ * @return {HTMLAnchorElement|undefined}
51
+ */
52
+ function findActivatedLink(event) {
53
+ const path = typeof event.composedPath === 'function' ? event.composedPath() : [event.target];
54
+ return path.find(node => node && node.tagName === 'A' && typeof node.href === 'string');
55
+ }
56
+
57
+ /**
58
+ * Refreshes consents of already rendered login links right before the browser follows them.
59
+ * Links are often generated before the user answers the consent dialog, so their consents are stale.
60
+ * The listener runs in the capture phase, before page handlers read the link.
61
+ * @private
62
+ * @param {object} document
63
+ * @param {object} options
64
+ * @param {function(string): boolean} options.isLoginUrl - Tells whether a URL is a login URL to refresh
65
+ * @param {function(): URLSearchParams} options.getConsentParams - Current consent parameters
66
+ * @return {void}
67
+ */
68
+ export function refreshLoginLinksOnActivation(document, { isLoginUrl, getConsentParams }) {
69
+ const refreshLink = (event) => {
70
+ const link = findActivatedLink(event);
71
+ if (!link || !isLoginUrl(link.href)) {
72
+ return;
73
+ }
74
+
75
+ const consentParams = getConsentParams();
76
+ // Unknown consents never overwrite the ones the link was created with.
77
+ if (consentParams.toString() === '') {
78
+ return;
79
+ }
80
+
81
+ link.href = replaceConsentParams(link.href, consentParams);
82
+ };
83
+
84
+ LINK_ACTIVATION_EVENTS.forEach((type) => {
85
+ document.addEventListener(type, refreshLink, true);
86
+ });
87
+ }
package/src/identity.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { TinyEmitter } from 'tiny-emitter';
2
+ /** Synchronously returns the current consents as query parameters; must not wait for the user's decision. */
3
+ export type ConsentReader = () => URLSearchParams | null;
2
4
 
3
5
  /**
4
6
  * Provides Identity functionality to a web page
@@ -14,9 +16,13 @@ export class Identity extends TinyEmitter {
14
16
  * no logging will be done
15
17
  * @param {object} [options.window] - window object
16
18
  * @param {function} [options.callbackBeforeRedirect] - callback triggered before session refresh redirect happen
19
+ * @param {ConsentReader} [options.getConsents] - reads consents forwarded in login URLs.
20
+ * Defaults to the Schibsted Sourcepoint integration
21
+ * @param {boolean} [options.refreshLoginLinks=true] - refresh consents of rendered login links when
22
+ * they are clicked
17
23
  * @throws {SDKError} - If any of options are invalid
18
24
  */
19
- constructor({ clientId, redirectUri, sessionDomain, env, log, window, callbackBeforeRedirect }: {
25
+ constructor({ clientId, redirectUri, sessionDomain, env, log, window, callbackBeforeRedirect, getConsents, refreshLoginLinks }: {
20
26
  clientId: string;
21
27
  sessionDomain: string;
22
28
  redirectUri: string;
@@ -24,6 +30,8 @@ export class Identity extends TinyEmitter {
24
30
  log?: Function;
25
31
  window?: any;
26
32
  callbackBeforeRedirect?: Function;
33
+ getConsents?: ConsentReader;
34
+ refreshLoginLinks?: boolean;
27
35
  });
28
36
  _sessionInitiatedSent: boolean;
29
37
  window: any;
@@ -34,6 +42,7 @@ export class Identity extends TinyEmitter {
34
42
  env: string;
35
43
  log: Function;
36
44
  callbackBeforeRedirect: Function;
45
+ private _getConsents;
37
46
  _sessionDomain: string;
38
47
  _enableSessionCaching: boolean;
39
48
  _session: {};
package/src/identity.js CHANGED
@@ -4,9 +4,10 @@
4
4
 
5
5
  'use strict';
6
6
 
7
- import { assert, isStr, isNonEmptyString, isObject, isUrl, isStrIn } from './validate.js';
7
+ import { assert, isStr, isNonEmptyString, isObject, isUrl, isStrIn, isFunction } from './validate.js';
8
8
  import { cloneDeep } from './object.js';
9
9
  import { urlMapper } from './url.js';
10
+ import { readSourcepointConsents, refreshLoginLinksOnActivation, replaceConsentParams } from './consents.js';
10
11
  import { ENDPOINTS, NAMESPACE } from './config.js';
11
12
  import EventEmitter from 'tiny-emitter';
12
13
  import Cache from './cache.js';
@@ -92,6 +93,15 @@ import { registerGlobal } from './global-registry.js';
92
93
  * @property {string} [originCampaign] - Campaign identifier for tracking in Pulse
93
94
  */
94
95
 
96
+ /**
97
+ * Synchronously returns the current consents, serialized as query parameters, when a login URL is
98
+ * created. Empty params or `null` mean that consent information is unavailable. It must not wait for
99
+ * the user's decision.
100
+ *
101
+ * @callback ConsentReader
102
+ * @return {URLSearchParams|null}
103
+ */
104
+
95
105
  /**
96
106
  * @typedef {object} HasSessionSuccessResponse
97
107
  * @property {boolean} result - Is the user connected to the merchant? (it means that the merchant
@@ -175,6 +185,10 @@ export class Identity extends EventEmitter {
175
185
  * no logging will be done
176
186
  * @param {object} [options.window] - window object
177
187
  * @param {function} [options.callbackBeforeRedirect] - callback triggered before session refresh redirect happen
188
+ * @param {ConsentReader} [options.getConsents] - reads consents forwarded in login URLs.
189
+ * Defaults to the Schibsted Sourcepoint integration
190
+ * @param {boolean} [options.refreshLoginLinks=true] - refresh consents of rendered login links when
191
+ * they are clicked
178
192
  * @throws {SDKError} - If any of options are invalid
179
193
  */
180
194
  constructor({
@@ -184,13 +198,17 @@ export class Identity extends EventEmitter {
184
198
  env = 'PRE',
185
199
  log,
186
200
  window = globalWindow(),
187
- callbackBeforeRedirect = ()=>{}
201
+ callbackBeforeRedirect = ()=>{},
202
+ getConsents = readSourcepointConsents,
203
+ refreshLoginLinks = true
188
204
  }) {
189
205
  super();
190
206
  assert(isNonEmptyString(clientId), 'clientId parameter is required');
191
207
  assert(isObject(window), 'The reference to window is missing');
192
208
  assert(!redirectUri || isUrl(redirectUri), 'redirectUri parameter is invalid');
193
209
  assert(sessionDomain && isUrl(sessionDomain), 'sessionDomain parameter is not a valid URL');
210
+ assert(isFunction(getConsents),
211
+ 'getConsents parameter must be a function');
194
212
 
195
213
  spidTalk.emulate(window);
196
214
  this._sessionInitiatedSent = false;
@@ -202,6 +220,7 @@ export class Identity extends EventEmitter {
202
220
  this.env = env;
203
221
  this.log = log;
204
222
  this.callbackBeforeRedirect = callbackBeforeRedirect;
223
+ this._getConsents = getConsents;
205
224
  this._sessionDomain = sessionDomain;
206
225
 
207
226
  // Internal hack: set to false to always refresh from hassession
@@ -220,6 +239,13 @@ export class Identity extends EventEmitter {
220
239
 
221
240
  this._unblockSessionCall();
222
241
 
242
+ if (refreshLoginLinks && window.document && isFunction(window.document.addEventListener)) {
243
+ refreshLoginLinksOnActivation(window.document, {
244
+ isLoginUrl: href => this._isLoginUrl(href),
245
+ getConsentParams: () => this._getConsentParams()
246
+ });
247
+ }
248
+
223
249
  registerGlobal(window, 'Identity', this);
224
250
 
225
251
  }
@@ -1003,7 +1029,7 @@ export class Identity extends EventEmitter {
1003
1029
  assert(isNonEmptyString(state),
1004
1030
  `the state parameter should be a non empty string but it is ${state}`);
1005
1031
 
1006
- return this._oauthService.makeUrl('oauth/authorize', {
1032
+ return replaceConsentParams(this._oauthService.makeUrl('oauth/authorize', {
1007
1033
  response_type: 'code',
1008
1034
  redirect_uri: redirectUri,
1009
1035
  scope,
@@ -1019,7 +1045,43 @@ export class Identity extends EventEmitter {
1019
1045
  x_domain_id: xDomainId,
1020
1046
  x_env_id: xEnvironmentId,
1021
1047
  utm_campaign: originCampaign
1022
- });
1048
+ }), this._getConsentParams());
1049
+ }
1050
+
1051
+ /**
1052
+ * Reads the current consent parameters. A failing consent reader must not prevent a login,
1053
+ * so the failure is logged and the login continues without consents.
1054
+ * @private
1055
+ * @return {URLSearchParams} - Consent query parameters; empty when unavailable
1056
+ */
1057
+ _getConsentParams() {
1058
+ try {
1059
+ const consentParams = this._getConsents() || new URLSearchParams();
1060
+ assert(consentParams instanceof URLSearchParams,
1061
+ `getConsents must return URLSearchParams but it returned ${consentParams}`);
1062
+ return consentParams;
1063
+ } catch (error) {
1064
+ const log = this.log || console.warn;
1065
+ log('Schibsted account SDK: could not read consents, logging in without them', error);
1066
+ return new URLSearchParams();
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Tells whether a URL is a login URL generated by this instance
1072
+ * @private
1073
+ * @param {string} href
1074
+ * @return {boolean}
1075
+ */
1076
+ _isLoginUrl(href) {
1077
+ if (!isUrl(href)) {
1078
+ return false;
1079
+ }
1080
+ const url = new URL(href);
1081
+ const authorizeUrl = new URL(this._oauthService.makeUrl('oauth/authorize', {}, false));
1082
+ return url.origin === authorizeUrl.origin
1083
+ && url.pathname === authorizeUrl.pathname
1084
+ && url.searchParams.get('client_id') === this.clientId;
1023
1085
  }
1024
1086
 
1025
1087
  /**
@@ -0,0 +1,50 @@
1
+ // Generated by scripts/vendor.js. Do not edit.
2
+
3
+ // node_modules/@schibsted/consent-url-utils/lib/esm/consent-manager.js
4
+ function getConsentsSync(_window = window) {
5
+ const manager = _window._tcf_ || _window._scc_;
6
+ if (!manager) {
7
+ console.warn("Consent-query-utils: getConsentsSync called but no consent manager found on window");
8
+ return null;
9
+ }
10
+ const advertising = manager.getPermissionSync("CMP:advertising");
11
+ const analytics = manager.getPermissionSync("CMP:analytics");
12
+ const marketing = manager.getPermissionSync("CMP:marketing");
13
+ const personalisation = manager.getPermissionSync("CMP:personalisation");
14
+ const consents = { advertising, analytics, marketing, personalisation };
15
+ return consents;
16
+ }
17
+
18
+ // node_modules/@schibsted/consent-url-utils/lib/esm/serializeConsents.js
19
+ var REJECTED_SENTINEL = "rejected";
20
+ var CONSENT_VERSION = "v1";
21
+ function hasConsentInformation(consents) {
22
+ return Object.values(consents).some((purpose) => purpose !== null);
23
+ }
24
+ function serializeConsents(consents) {
25
+ const consentsSearchParams = new URLSearchParams();
26
+ if (!consents || !hasConsentInformation(consents)) {
27
+ return consentsSearchParams;
28
+ }
29
+ const granted = [];
30
+ for (const [category, purpose] of Object.entries(consents)) {
31
+ if (purpose === "1") {
32
+ granted.push(category);
33
+ }
34
+ }
35
+ if (granted.length === 0) {
36
+ granted.push(REJECTED_SENTINEL);
37
+ }
38
+ const uniqueSorted = [...new Set(granted)].sort();
39
+ consentsSearchParams.set("consents", uniqueSorted.join(","));
40
+ consentsSearchParams.set("consent_version", CONSENT_VERSION);
41
+ return consentsSearchParams;
42
+ }
43
+ function getAndSerializeConsentsSync() {
44
+ var _a;
45
+ const consents = (_a = getConsentsSync()) != null ? _a : null;
46
+ return serializeConsents(consents);
47
+ }
48
+ export {
49
+ getAndSerializeConsentsSync
50
+ };
package/src/version.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // Version is bumped automatically by release-please. See release-please-config.json.
2
2
 
3
3
  'use strict'
4
- const version = '5.2.7'; // x-release-please-version
4
+ const version = '5.3.0'; // x-release-please-version
5
5
  export default version;