@schibsted/account-sdk-browser 5.2.7 → 6.0.0-alpha.10

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.
Files changed (109) hide show
  1. package/README.md +46 -301
  2. package/dist/account-sdk.d.ts +22 -0
  3. package/dist/account.d.ts +179 -0
  4. package/dist/cache.d.ts +65 -0
  5. package/dist/config.d.ts +86 -0
  6. package/dist/globals.d.ts +11 -0
  7. package/dist/has-access-response.d.ts +2 -0
  8. package/dist/identity.d.ts +302 -0
  9. package/dist/index.d.ts +11 -0
  10. package/dist/index.js +828 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/monetization.d.ts +39 -0
  13. package/{src → dist}/object.d.ts +4 -9
  14. package/dist/popup.d.ts +9 -0
  15. package/dist/resolve-browser-window.d.ts +1 -0
  16. package/dist/rest-client.d.ts +102 -0
  17. package/dist/rest-clients.d.ts +10 -0
  18. package/dist/sdk-error.d.ts +29 -0
  19. package/dist/session-response.d.ts +54 -0
  20. package/{src/spidTalk.d.ts → dist/spid-talk.d.ts} +4 -6
  21. package/dist/types/account.d.ts +48 -0
  22. package/dist/types/common.d.ts +28 -0
  23. package/dist/types/identity.d.ts +23 -0
  24. package/dist/types/login.d.ts +125 -0
  25. package/dist/types/monetization.d.ts +26 -0
  26. package/dist/types/session.d.ts +146 -0
  27. package/dist/url.d.ts +8 -0
  28. package/dist/validate.d.ts +50 -0
  29. package/dist/version.d.ts +2 -0
  30. package/package.json +34 -52
  31. package/src/account-sdk.ts +92 -0
  32. package/src/account.ts +332 -0
  33. package/src/{cache.js → cache.ts} +53 -38
  34. package/src/{config.js → config.ts} +7 -32
  35. package/src/globals.ts +8 -0
  36. package/src/has-access-response.ts +35 -0
  37. package/src/identity.ts +1023 -0
  38. package/src/index.ts +40 -0
  39. package/src/monetization.ts +128 -0
  40. package/src/{object.js → object.ts} +9 -16
  41. package/src/popup.ts +74 -0
  42. package/src/resolve-browser-window.ts +11 -0
  43. package/src/rest-client.ts +253 -0
  44. package/src/rest-clients.ts +30 -0
  45. package/src/sdk-error.ts +61 -0
  46. package/src/session-response.ts +100 -0
  47. package/src/{spidTalk.js → spid-talk.ts} +10 -12
  48. package/src/types/account.ts +55 -0
  49. package/src/types/common.ts +32 -0
  50. package/src/types/identity.ts +26 -0
  51. package/src/types/login.ts +150 -0
  52. package/src/types/monetization.ts +29 -0
  53. package/src/types/session.ts +169 -0
  54. package/src/{url.js → url.ts} +6 -10
  55. package/src/{validate.js → validate.ts} +27 -43
  56. package/src/{version.js → version.ts} +1 -2
  57. package/es5/global.js +0 -12968
  58. package/es5/global.js.map +0 -1
  59. package/es5/global.min.js +0 -2
  60. package/es5/global.min.js.map +0 -1
  61. package/es5/identity.js +0 -12212
  62. package/es5/identity.js.map +0 -1
  63. package/es5/identity.min.js +0 -2
  64. package/es5/identity.min.js.map +0 -1
  65. package/es5/index.js +0 -12940
  66. package/es5/index.js.map +0 -1
  67. package/es5/index.min.js +0 -2
  68. package/es5/index.min.js.map +0 -1
  69. package/es5/monetization.js +0 -9176
  70. package/es5/monetization.js.map +0 -1
  71. package/es5/monetization.min.js +0 -2
  72. package/es5/monetization.min.js.map +0 -1
  73. package/es5/payment.js +0 -8853
  74. package/es5/payment.js.map +0 -1
  75. package/es5/payment.min.js +0 -2
  76. package/es5/payment.min.js.map +0 -1
  77. package/identity.d.ts +0 -1
  78. package/identity.js +0 -5
  79. package/index.d.ts +0 -4
  80. package/index.js +0 -9
  81. package/monetization.d.ts +0 -1
  82. package/monetization.js +0 -5
  83. package/payment.d.ts +0 -1
  84. package/payment.js +0 -5
  85. package/src/RESTClient.d.ts +0 -89
  86. package/src/RESTClient.js +0 -193
  87. package/src/SDKError.d.ts +0 -16
  88. package/src/SDKError.js +0 -55
  89. package/src/__mocks__/.eslintrc.yml +0 -8
  90. package/src/__mocks__/RESTClient.js +0 -54
  91. package/src/cache.d.ts +0 -64
  92. package/src/config.d.ts +0 -34
  93. package/src/es5/global.js +0 -13
  94. package/src/es5/identity.js +0 -10
  95. package/src/es5/index.js +0 -13
  96. package/src/es5/monetization.js +0 -10
  97. package/src/es5/payment.js +0 -10
  98. package/src/global-registry.js +0 -20
  99. package/src/identity.d.ts +0 -679
  100. package/src/identity.js +0 -1157
  101. package/src/monetization.d.ts +0 -80
  102. package/src/monetization.js +0 -172
  103. package/src/payment.d.ts +0 -115
  104. package/src/payment.js +0 -211
  105. package/src/popup.d.ts +0 -10
  106. package/src/popup.js +0 -59
  107. package/src/url.d.ts +0 -10
  108. package/src/validate.d.ts +0 -64
  109. package/src/version.d.ts +0 -2
@@ -0,0 +1,1023 @@
1
+ /* Copyright 2024 Schibsted Products & Technology AS. Licensed under the terms of the MIT license.
2
+ * See LICENSE.md in the project root.
3
+ */
4
+
5
+ import Cache from './cache.js';
6
+ import { ENDPOINTS } from './config.js';
7
+ import { cloneDeep } from './object.js';
8
+ import * as popup from './popup.js';
9
+ import type RESTClient from './rest-client.js';
10
+ import { buildClientSdrn, createAccountRestClient } from './rest-clients.js';
11
+ import SDKError from './sdk-error.js';
12
+ import {
13
+ isConnectedSessionResponse,
14
+ isConnectedSessionWithUserIdResponse,
15
+ isRedirectSessionPayload,
16
+ isSessionFailureResponse,
17
+ isSessionSuccessResponse,
18
+ normalizeSessionResponse,
19
+ } from './session-response.js';
20
+ import * as spidTalk from './spid-talk.js';
21
+ import type { AccountEventName } from './types/account.js';
22
+ import type { LogFunction } from './types/common.js';
23
+ import type {
24
+ IdentityBeforeRedirectCallback,
25
+ IdentityOptions,
26
+ VarnishCookieOptions,
27
+ } from './types/identity.js';
28
+ import {
29
+ hasOpenSimplifiedLoginWidget,
30
+ type LoginOptions,
31
+ type OpenSimplifiedLoginWidget,
32
+ type SimplifiedLoginData,
33
+ type SimplifiedLoginWidgetInitialParams,
34
+ type SimplifiedLoginWidgetLoginOptions,
35
+ type SimplifiedLoginWidgetOptions,
36
+ } from './types/login.js';
37
+ import type {
38
+ ConnectedSessionResponse,
39
+ NormalizedSessionPayload,
40
+ SessionObjectResponse,
41
+ SessionResponse,
42
+ } from './types/session.js';
43
+ import { urlMapper } from './url.js';
44
+ import { assert, isNonEmptyString, isObject, isStr, isStrIn, isUrl } from './validate.js';
45
+ import version from './version.js';
46
+
47
+ declare global {
48
+ interface Window {
49
+ openSimplifiedLoginWidget?: OpenSimplifiedLoginWidget;
50
+ }
51
+ }
52
+
53
+ const HAS_SESSION_CACHE_KEY = 'hasSession-cache';
54
+ const SESSION_CALL_BLOCKED_CACHE_KEY = 'sessionCallBlocked-cache';
55
+ const SESSION_CALL_BLOCKED_TTL = 1000 * 60 * 5;
56
+
57
+ const TAB_ID_KEY = 'tab-id-cache';
58
+ const TAB_ID = Math.floor(Math.random() * 100000);
59
+ const TAB_ID_TTL = 1000 * 60 * 60 * 24 * 30;
60
+
61
+ const globalWindow = () => window;
62
+
63
+ /**
64
+ * Provides Identity functionalty to a web page
65
+ */
66
+ export class Identity {
67
+ // Event bus
68
+ private readonly eventTarget = new EventTarget();
69
+
70
+ // Configuration
71
+ private readonly window: Window;
72
+ private readonly clientId: string;
73
+ private readonly redirectUri: string;
74
+ private readonly env: string;
75
+ private readonly log?: LogFunction;
76
+ private readonly callbackBeforeRedirect: IdentityBeforeRedirectCallback;
77
+
78
+ // Caches
79
+ private readonly sessionStorageCache: Cache;
80
+ private readonly localStorageCache: Cache;
81
+ private enableSessionCaching: boolean;
82
+
83
+ // REST clients
84
+ private readonly spid: RESTClient;
85
+ private readonly oauthService: RESTClient;
86
+ private readonly bffService: RESTClient;
87
+ private readonly sessionService: RESTClient;
88
+ private readonly globalSessionService: RESTClient;
89
+ private readonly usedSessionServiceGetSessionEndpoint: 'v2/session' | 'session';
90
+
91
+ // Session state
92
+ /**
93
+ * @internal Starts as `{}` before the first `hasSession()` call, then stores the latest
94
+ * object response from hasSession().
95
+ */
96
+ private session: SessionObjectResponse;
97
+ private hasSessionInProgress?: false | Promise<SessionResponse>;
98
+
99
+ // Popup & Varnish cookie state
100
+ private popup?: Window | null;
101
+ private setVarnishCookie?: boolean;
102
+ private varnishExpiresIn?: number;
103
+ private varnishCookieDomain?: string;
104
+
105
+ /**
106
+ * @param options
107
+ * @param options.clientId - Example: "1234567890abcdef12345678"
108
+ * @param options.sessionDomain - Example: "https://id.site.com"
109
+ * @param options.redirectUri - Example: "https://site.com"
110
+ * @param options.env - Schibsted account environment: `PRE` (default), `PRO`, `PRO_NO`, `PRO_FI` or `PRO_DK`
111
+ * @param options.log - A function that receives debug log information. If not set,
112
+ * no logging will be done
113
+ * @param options.window - window object
114
+ * @param options.callbackBeforeRedirect - callback triggered before session refresh redirect happen
115
+ * @throws {SDKError} - If any of options are invalid
116
+ */
117
+ constructor({
118
+ clientId,
119
+ redirectUri,
120
+ sessionDomain,
121
+ env = 'PRE',
122
+ log,
123
+ window = globalWindow(),
124
+ callbackBeforeRedirect = () => {},
125
+ }: IdentityOptions) {
126
+ assert(isNonEmptyString(clientId), 'clientId parameter is required');
127
+ assert(isObject(window), 'The reference to window is missing');
128
+ assert(!redirectUri || isUrl(redirectUri), 'redirectUri parameter is invalid');
129
+ assert(
130
+ isNonEmptyString(sessionDomain) && isUrl(sessionDomain),
131
+ 'sessionDomain parameter is not a valid URL',
132
+ );
133
+ assert(isStr(env), `env parameter is invalid: ${env}`);
134
+
135
+ spidTalk.emulate(window);
136
+ this.window = window;
137
+ this.clientId = clientId;
138
+ this.redirectUri = redirectUri;
139
+ this.env = env;
140
+ this.log = log;
141
+ this.callbackBeforeRedirect = callbackBeforeRedirect;
142
+
143
+ this.sessionStorageCache = new Cache(() => this.window && this.window.sessionStorage);
144
+ this.localStorageCache = new Cache(() => this.window && this.window.localStorage);
145
+ // Internal hack: set to false to always refresh from hassession
146
+ this.enableSessionCaching = true;
147
+
148
+ this.session = {};
149
+
150
+ const accountClientParams = { client_id: this.clientId, redirect_uri: this.redirectUri };
151
+ const clientSdrn = buildClientSdrn(this.env, this.clientId);
152
+
153
+ this.spid = createAccountRestClient({
154
+ serverUrl: urlMapper(env, ENDPOINTS.SPiD),
155
+ log: this.log,
156
+ defaultParams: accountClientParams,
157
+ });
158
+ this.oauthService = createAccountRestClient({
159
+ serverUrl: urlMapper(env, ENDPOINTS.SPiD),
160
+ log: this.log,
161
+ defaultParams: accountClientParams,
162
+ });
163
+ this.bffService = createAccountRestClient({
164
+ serverUrl: urlMapper(env, ENDPOINTS.BFF),
165
+ log: this.log,
166
+ defaultParams: accountClientParams,
167
+ });
168
+ this.sessionService = createAccountRestClient({
169
+ serverUrl: sessionDomain,
170
+ log: this.log,
171
+ defaultParams: {
172
+ client_sdrn: clientSdrn,
173
+ redirect_uri: this.redirectUri,
174
+ sdk_version: version,
175
+ },
176
+ });
177
+ this.globalSessionService = createAccountRestClient({
178
+ serverUrl: urlMapper(env, ENDPOINTS.SESSION_SERVICE),
179
+ log: this.log,
180
+ defaultParams: { client_sdrn: clientSdrn, sdk_version: version },
181
+ });
182
+ this.usedSessionServiceGetSessionEndpoint =
183
+ this.sessionService.url.pathname && this.sessionService.url.pathname.length <= 1
184
+ ? 'v2/session'
185
+ : 'session';
186
+
187
+ this._unblockSessionCall();
188
+ }
189
+
190
+ addEventListener(type: AccountEventName, listener: EventListener): void {
191
+ this.eventTarget.addEventListener(type, listener);
192
+ }
193
+
194
+ removeEventListener(type: AccountEventName, listener: EventListener): void {
195
+ this.eventTarget.removeEventListener(type, listener);
196
+ }
197
+
198
+ /**
199
+ * Read tabId from session storage
200
+ * @private
201
+ */
202
+ _getTabId(): number | undefined {
203
+ if (this.enableSessionCaching) {
204
+ const tabId = this.sessionStorageCache.get(TAB_ID_KEY);
205
+ if (typeof tabId !== 'number') {
206
+ this.sessionStorageCache.set(TAB_ID_KEY, TAB_ID, TAB_ID_TTL);
207
+ return TAB_ID;
208
+ }
209
+
210
+ return tabId;
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Checks if getting session is blocked
216
+ * @private
217
+ *
218
+ */
219
+ _isSessionCallBlocked(): boolean {
220
+ return Boolean(this.localStorageCache.get(SESSION_CALL_BLOCKED_CACHE_KEY));
221
+ }
222
+
223
+ /**
224
+ * Block calls to get session
225
+ * @private
226
+ *
227
+ */
228
+ _blockSessionCall(): void {
229
+ const SESSION_CALL_BLOCKED = true;
230
+
231
+ this.localStorageCache.set(
232
+ SESSION_CALL_BLOCKED_CACHE_KEY,
233
+ SESSION_CALL_BLOCKED,
234
+ SESSION_CALL_BLOCKED_TTL,
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Unblocks calls to get session
240
+ * @private
241
+ *
242
+ */
243
+ _unblockSessionCall(): void {
244
+ this.localStorageCache.delete(SESSION_CALL_BLOCKED_CACHE_KEY);
245
+ }
246
+
247
+ /**
248
+ * Close this.popup if it exists and is open
249
+ * @private
250
+ */
251
+ _closePopup(): void {
252
+ if (this.popup) {
253
+ if (!this.popup.closed) {
254
+ this.popup.close();
255
+ }
256
+ this.popup = null;
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Set the Varnish cookie (`SP_ID`) when hasSession() is called. Note that most browsers require
262
+ * that you are on a "real domain" for this to work — so, **not** `localhost`
263
+ * @param options
264
+ * @param options.expiresIn Override this to set number of seconds before the varnish
265
+ * cookie expires. The default is to use the same time that hasSession responses are cached for
266
+ * @param options.domain Override cookie domain. E.g. «vg.no» instead of «www.vg.no»
267
+ */
268
+ enableVarnishCookie(options: VarnishCookieOptions): void {
269
+ if (typeof options !== 'object') {
270
+ throw new SDKError('VarnishCookieOptions must be an object');
271
+ }
272
+
273
+ const { expiresIn = 0, domain } = options;
274
+
275
+ assert(Number.isInteger(expiresIn), `'expiresIn' must be an integer`);
276
+ assert(expiresIn >= 0, `'expiresIn' cannot be negative`);
277
+ this.setVarnishCookie = true;
278
+ this.varnishExpiresIn = expiresIn;
279
+ this.varnishCookieDomain = domain;
280
+ }
281
+
282
+ /**
283
+ * Set the Varnish cookie if configured
284
+ * @private
285
+ * @param sessionData
286
+ */
287
+ _maybeSetVarnishCookie(sessionData: object): void {
288
+ if (!this.setVarnishCookie || !isSessionSuccessResponse(sessionData)) {
289
+ return;
290
+ }
291
+ const date = new Date();
292
+ const validExpires =
293
+ this.varnishExpiresIn ||
294
+ (typeof sessionData.expiresIn === 'number' && sessionData.expiresIn > 0);
295
+ if (validExpires) {
296
+ const expires = this.varnishExpiresIn || sessionData.expiresIn || 0;
297
+ date.setTime(date.getTime() + expires * 1000);
298
+ } else {
299
+ date.setTime(0);
300
+ }
301
+
302
+ // If the domain is missing or of the wrong type, we'll use document.domain
303
+ const domain =
304
+ this.varnishCookieDomain ||
305
+ (typeof sessionData.baseDomain === 'string'
306
+ ? sessionData.baseDomain
307
+ : this.window.location.hostname) ||
308
+ '';
309
+
310
+ const cookie = [
311
+ `SP_ID=${sessionData.sp_id}`,
312
+ `expires=${date.toUTCString()}`,
313
+ `path=/`,
314
+ `domain=.${domain}`,
315
+ ].join('; ');
316
+ document.cookie = cookie;
317
+ }
318
+
319
+ /**
320
+ * Clear the Varnish cookie if configured
321
+ * @private
322
+ */
323
+ _maybeClearVarnishCookie(): void {
324
+ if (this.setVarnishCookie) {
325
+ this._clearVarnishCookie();
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Clear the Varnish cookie
331
+ * @private
332
+ */
333
+ _clearVarnishCookie(): void {
334
+ const baseDomain =
335
+ this.session &&
336
+ isSessionSuccessResponse(this.session) &&
337
+ typeof this.session.baseDomain === 'string'
338
+ ? this.session.baseDomain
339
+ : this.window.location.hostname;
340
+
341
+ const domain = this.varnishCookieDomain || baseDomain || '';
342
+
343
+ document.cookie = `SP_ID=nothing; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
344
+ }
345
+
346
+ /**
347
+ * @summary Queries the hassession endpoint and returns information about the status of the user
348
+ * @remarks When we send a request to this endpoint, cookies sent along with the request
349
+ * determines the status of the user.
350
+ * @throws {SDKError} - If the call to the hasSession service fails in any way (this will happen
351
+ * if, say, the user is not logged in)
352
+ * @fires Identity#sessionUpdated
353
+ */
354
+ hasSession(): Promise<SessionResponse> {
355
+ const isSessionCallBlocked = this._isSessionCallBlocked();
356
+
357
+ if (isSessionCallBlocked) {
358
+ const sessionData = normalizeSessionResponse(this.session);
359
+ return Promise.resolve(isRedirectSessionPayload(sessionData) ? {} : sessionData);
360
+ }
361
+
362
+ if (this.hasSessionInProgress) {
363
+ return this.hasSessionInProgress;
364
+ }
365
+
366
+ let sessionChanged = false;
367
+ const _postProcess = (sessionData: SessionObjectResponse): SessionObjectResponse => {
368
+ if (isSessionFailureResponse(sessionData)) {
369
+ const errorObject = isObject(sessionData.error)
370
+ ? sessionData.error
371
+ : { description: String(sessionData.error) };
372
+ throw new SDKError('HasSession failed', errorObject);
373
+ }
374
+ this._maybeSetVarnishCookie(sessionData);
375
+ const currentSessionId = isSessionSuccessResponse(this.session) && this.session.id;
376
+ const incomingSessionId = isSessionSuccessResponse(sessionData) && sessionData.id;
377
+ sessionChanged = currentSessionId !== incomingSessionId;
378
+ this.session = sessionData;
379
+ return sessionData;
380
+ };
381
+
382
+ const _getCachedSession = (): NormalizedSessionPayload | null => {
383
+ const cachedSession = this.sessionStorageCache.get(HAS_SESSION_CACHE_KEY);
384
+ return cachedSession ? normalizeSessionResponse(cachedSession) : null;
385
+ };
386
+
387
+ const _getSession = async (): Promise<SessionResponse> => {
388
+ if (this.enableSessionCaching) {
389
+ // Try to resolve from cache (it has a TTL)
390
+ const cachedSession = _getCachedSession();
391
+ if (cachedSession) {
392
+ if (isRedirectSessionPayload(cachedSession)) {
393
+ return {};
394
+ }
395
+ return _postProcess(cachedSession);
396
+ }
397
+ }
398
+ let sessionData: NormalizedSessionPayload | null = null;
399
+ try {
400
+ sessionData = await this.sessionService.get(
401
+ this.usedSessionServiceGetSessionEndpoint,
402
+ { tabId: this._getTabId() },
403
+ normalizeSessionResponse,
404
+ );
405
+ } catch (err: unknown) {
406
+ const cacheableError = isObject(err) ? err : undefined;
407
+ if (cacheableError?.code === 400 && this.enableSessionCaching) {
408
+ const errorExpiresIn =
409
+ typeof cacheableError.expiresIn === 'number'
410
+ ? cacheableError.expiresIn
411
+ : 300;
412
+ const expiresIn = 1000 * errorExpiresIn;
413
+ this.sessionStorageCache.set(HAS_SESSION_CACHE_KEY, { error: err }, expiresIn);
414
+ } else if (
415
+ cacheableError &&
416
+ typeof cacheableError.code === 'number' &&
417
+ cacheableError.code >= 500 &&
418
+ cacheableError.code < 600 &&
419
+ this.enableSessionCaching
420
+ ) {
421
+ // Temporary fix: 30 seconds cache to limit number of calls when service is unavailable
422
+ this.sessionStorageCache.set(HAS_SESSION_CACHE_KEY, { error: err }, 30 * 1000);
423
+ }
424
+ throw err;
425
+ }
426
+
427
+ if (sessionData) {
428
+ // for expiring session and safari browser do full page redirect to gain new session
429
+ if (isRedirectSessionPayload(sessionData)) {
430
+ this._blockSessionCall();
431
+
432
+ await this.callbackBeforeRedirect();
433
+
434
+ const redirectUrl = this.sessionService.makeUrl(sessionData.redirectURL, {
435
+ tabId: this._getTabId(),
436
+ });
437
+ return redirectUrl;
438
+ }
439
+
440
+ if (this.enableSessionCaching) {
441
+ const expiresIn =
442
+ 1000 *
443
+ (isSessionSuccessResponse(sessionData)
444
+ ? sessionData.expiresIn || 300
445
+ : 300);
446
+ this.sessionStorageCache.set(HAS_SESSION_CACHE_KEY, sessionData, expiresIn);
447
+ }
448
+ }
449
+
450
+ return _postProcess(sessionData ?? {});
451
+ };
452
+ this.hasSessionInProgress = _getSession().then(
453
+ (sessionData) => {
454
+ if (sessionChanged && typeof sessionData !== 'string') {
455
+ this.eventTarget.dispatchEvent(
456
+ new CustomEvent('sessionUpdated', { detail: sessionData }),
457
+ );
458
+ }
459
+ this.hasSessionInProgress = false;
460
+ if (typeof sessionData === 'string') {
461
+ this.window.location.href = sessionData;
462
+ }
463
+ return sessionData;
464
+ },
465
+ (err) => {
466
+ this.hasSessionInProgress = false;
467
+ throw new SDKError('HasSession failed', err);
468
+ },
469
+ );
470
+
471
+ return this.hasSessionInProgress;
472
+ }
473
+
474
+ /**
475
+ * @summary Allows the client app to check if the user is logged in to Schibsted account
476
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
477
+ * effect that it might perform an auto-login on the user
478
+ */
479
+ async isLoggedIn(): Promise<boolean> {
480
+ try {
481
+ const data = await this.hasSession();
482
+ return isSessionSuccessResponse(data);
483
+ } catch (_) {
484
+ return false;
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Enables or disables caching for subsequent session lookups.
490
+ *
491
+ * Changing this setting does not remove an existing cached session. Use
492
+ * {@link Identity#clearCachedUserSession} to clear the cache explicitly.
493
+ */
494
+ setSessionCachingEnabled(enabled: boolean): void {
495
+ this.enableSessionCaching = enabled;
496
+ }
497
+
498
+ /**
499
+ * Removes the cached user session.
500
+ */
501
+ clearCachedUserSession(): void {
502
+ this.sessionStorageCache.delete(HAS_SESSION_CACHE_KEY);
503
+ }
504
+
505
+ /**
506
+ * @summary Allows the caller to check if the current user is connected to the client_id in
507
+ * Schibsted account. Being connected means that the user has agreed for their account to be
508
+ * used by your web app and have accepted the required terms
509
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
510
+ * effect that it might perform an auto-login on the user
511
+ * @summary Check if the user is connected to the client_id
512
+ */
513
+ async isConnected(): Promise<boolean> {
514
+ try {
515
+ return isConnectedSessionResponse(await this.hasSession());
516
+ } catch (_) {
517
+ return false;
518
+ }
519
+ }
520
+
521
+ /**
522
+ * @summary Returns information about the user
523
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
524
+ * effect that it might perform an auto-login on the user
525
+ * @throws {SDKError} If the user isn't connected to the merchant
526
+ * @throws {SDKError} If we couldn't get the user
527
+ */
528
+ async getUser(): Promise<ConnectedSessionResponse> {
529
+ const user = await this.hasSession();
530
+ if (!isConnectedSessionResponse(user)) {
531
+ throw new SDKError('The user is not connected to this merchant');
532
+ }
533
+ return cloneDeep(user);
534
+ }
535
+ /**
536
+ * @summary
537
+ * In Schibsted account, there are multiple ways of identifying a user; the `userId` or `uuid`.
538
+ * There are reasons for them all to exist. The `userId` is a numeric identifier, but
539
+ * since Schibsted account is deployed separately in Norway and Sweden, there are a lot of
540
+ * duplicates. The `userId` was introduced early, so many sites still need to use them for
541
+ * legacy reasons. The `uuid` is universally unique, and so — if we could disregard a lot of
542
+ * Schibsted components depending on the numeric `userId` — it would be a good identifier to use
543
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
544
+ * effect that it might perform an auto-login on the user
545
+ * @throws {SDKError} If the user isn't connected to the merchant
546
+ * @return The `userId` field (not to be confused with the `uuid`)
547
+ */
548
+ async getUserId(): Promise<number> {
549
+ const user = await this.hasSession();
550
+ if (isConnectedSessionWithUserIdResponse(user)) {
551
+ return user.userId;
552
+ }
553
+ throw new SDKError('The user is not connected to this merchant');
554
+ }
555
+
556
+ /**
557
+ * @function
558
+ * @summary
559
+ * Retrieves the external identifier (`externalId`) for the authenticated user.
560
+ *
561
+ * In Schibsted Account there are multiple ways of identifying users, however for integrations with
562
+ * third-parties it's recommended to use `externalId` as it does not disclose
563
+ * any critical data whilst allowing for user identification.
564
+ *
565
+ * `externalId` is merchant-scoped using a pairwise identifier (`pairId`),
566
+ * meaning the same user's ID will differ between merchants.
567
+ * Additionally, this identifier is bound to the external party provided as argument.
568
+ *
569
+ * @param externalParty
570
+ * @param optionalSuffix
571
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
572
+ * effect that it might perform an auto-login on the user
573
+ * @throws {SDKError} If the `pairId` is missing in user session.
574
+ * @throws {SDKError} If the `externalParty` is not defined
575
+ * @return The merchant- and 3rd-party-specific `externalId`
576
+ */
577
+ async getExternalId(externalParty: string, optionalSuffix: string = ''): Promise<string> {
578
+ const user = await this.hasSession();
579
+ const pairId = isSessionSuccessResponse(user) ? user.pairId : undefined;
580
+
581
+ if (!pairId) throw new SDKError('pairId missing in user session!');
582
+
583
+ if (!externalParty || externalParty.length === 0) {
584
+ throw new SDKError('externalParty cannot be empty');
585
+ }
586
+ const _toHexDigest = (hashBuffer: ArrayBuffer) => {
587
+ // convert buffer to byte array
588
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
589
+ // convert bytes to hex string
590
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
591
+ };
592
+
593
+ const _getSha256Digest = (data: BufferSource) => {
594
+ return crypto.subtle.digest('SHA-256', data);
595
+ };
596
+
597
+ const _hashMessage = async (message: string) => {
598
+ const msgUint8 = new TextEncoder().encode(message);
599
+ return _getSha256Digest(msgUint8).then((it) => _toHexDigest(it));
600
+ };
601
+
602
+ const _constructMessage = (
603
+ pairId: string,
604
+ externalParty: string,
605
+ optionalSuffix: string,
606
+ ) => {
607
+ return optionalSuffix
608
+ ? `${pairId}:${externalParty}:${optionalSuffix}`
609
+ : `${pairId}:${externalParty}`;
610
+ };
611
+
612
+ return _hashMessage(_constructMessage(pairId, externalParty, optionalSuffix));
613
+ }
614
+
615
+ /**
616
+ * @summary Enables brands to programmatically get the current the SDRN based on the user's session.
617
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
618
+ * effect that it might perform an auto-login on the user
619
+ * @throws {SDKError} If the SDRN is missing in user session object.
620
+ */
621
+ async getUserSDRN(): Promise<string> {
622
+ const user = await this.hasSession();
623
+ if (isSessionSuccessResponse(user) && user.sdrn) {
624
+ return user.sdrn;
625
+ }
626
+ throw new SDKError('Failed to get SDRN from user session');
627
+ }
628
+
629
+ /**
630
+ * @summary In Schibsted account, there are two ways of identifying a user; the `userId` and the
631
+ * `uuid`. There are reasons for them both existing. The `userId` is a numeric identifier, but
632
+ * since Schibsted account is deployed separately in Norway and Sweden, there are a lot of
633
+ * duplicates. The `userId` was introduced early, so many sites still need to use them for
634
+ * legacy reasons. The `uuid` is universally unique, and so — if we could disregard a lot of
635
+ * Schibsted components depending on the numeric `userId` — it would be a good identifier to use
636
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
637
+ * effect that it might perform an auto-login on the user
638
+ * @throws {SDKError} If the user isn't connected to the merchant
639
+ * @return The `uuid` field (not to be confused with the `userId`)
640
+ */
641
+ async getUserUuid(): Promise<string> {
642
+ const user = await this.hasSession();
643
+ if (isSessionSuccessResponse(user) && user.uuid && user.result) {
644
+ return user.uuid;
645
+ }
646
+ throw new SDKError('The user is not connected to this merchant');
647
+ }
648
+
649
+ /**
650
+ * @summary Get basic information about any user currently logged-in to their Schibsted account
651
+ * in this browser. Can be used to provide context in a continue-as prompt.
652
+ * @remarks This function relies on the global Schibsted account user session cookie, which
653
+ * is a third-party cookie and hence might be blocked by the browser (for example due to ITP in
654
+ * Safari). So there's no guarantee any data is returned, even though a user is logged-in in
655
+ * the current browser.
656
+ */
657
+ async getUserContextData(): Promise<SimplifiedLoginData | null> {
658
+ try {
659
+ return await this.globalSessionService.get('user-context', undefined, (value) =>
660
+ isObject(value) && isStr(value.identifier)
661
+ ? {
662
+ identifier: value.identifier,
663
+ display_text: isStr(value.display_text) ? value.display_text : '',
664
+ client_name: isStr(value.client_name) ? value.client_name : '',
665
+ provider_id: isStr(value.provider_id) ? value.provider_id : undefined,
666
+ }
667
+ : null,
668
+ );
669
+ } catch (_) {
670
+ return null;
671
+ }
672
+ }
673
+
674
+ /**
675
+ * If a popup is desired, this function needs to be called in response to a user event (like
676
+ * click or tap) in order to work correctly. Otherwise the popup will be blocked by the
677
+ * browser's popup blockers and has to be explicitly authorized to be shown.
678
+ * @summary Perform a login, either using a full-page redirect or a popup
679
+ * @see https://tools.ietf.org/html/rfc6749#section-4.1.1
680
+ *
681
+ * @param options
682
+ * @param options.state
683
+ * @param options.acrValues
684
+ * @param options.scope
685
+ * @param options.redirectUri
686
+ * @param options.preferPopup
687
+ * @param options.loginHint
688
+ * @param options.tag
689
+ * @param options.teaser
690
+ * @param options.maxAge
691
+ * @param options.locale
692
+ * @param options.oneStepLogin
693
+ * @param options.prompt
694
+ * @param options.xDomainId
695
+ * @param options.xEnvironmentId
696
+ * @param options.originCampaign
697
+ * @fires Identity#loginRequested
698
+ * @return - Reference to popup window if created (or `null` otherwise)
699
+ */
700
+ login({
701
+ state,
702
+ acrValues,
703
+ scope = 'openid',
704
+ redirectUri = this.redirectUri,
705
+ preferPopup = false,
706
+ loginHint = '',
707
+ tag = '',
708
+ teaser = '',
709
+ maxAge = '',
710
+ locale,
711
+ oneStepLogin = false,
712
+ prompt = 'select_account',
713
+ xDomainId = '',
714
+ xEnvironmentId = '',
715
+ originCampaign = '',
716
+ }: LoginOptions): Window | null {
717
+ this._closePopup();
718
+ this.sessionStorageCache.delete(HAS_SESSION_CACHE_KEY);
719
+ const url = this.loginUrl({
720
+ state,
721
+ acrValues,
722
+ scope,
723
+ redirectUri,
724
+ loginHint,
725
+ tag,
726
+ teaser,
727
+ maxAge,
728
+ locale,
729
+ oneStepLogin,
730
+ prompt,
731
+ xDomainId,
732
+ xEnvironmentId,
733
+ originCampaign,
734
+ });
735
+
736
+ if (preferPopup) {
737
+ this.popup = popup.open(this.window, url, 'Schibsted account', {
738
+ width: 360,
739
+ height: 570,
740
+ });
741
+
742
+ if (this.popup) {
743
+ this.eventTarget.dispatchEvent(
744
+ new CustomEvent('loginRequested', {
745
+ detail: { url, method: 'popup' },
746
+ }),
747
+ );
748
+
749
+ return this.popup;
750
+ }
751
+ }
752
+
753
+ this.eventTarget.dispatchEvent(
754
+ new CustomEvent('loginRequested', {
755
+ detail: { url, method: 'default' },
756
+ }),
757
+ );
758
+ this.window.location.href = url;
759
+
760
+ return null;
761
+ }
762
+
763
+ /**
764
+ * @summary Retrieve the sp_id (Varnish ID)
765
+ * @remarks This function calls {@link Identity#hasSession} internally and thus has the side
766
+ * effect that it might perform an auto-login on the user
767
+ * @return - The sp_id string or null (if the server didn't return it)
768
+ */
769
+ async getSpId(): Promise<string | null> {
770
+ try {
771
+ const user = await this.hasSession();
772
+ if (isSessionSuccessResponse(user)) {
773
+ return user.sp_id || null;
774
+ }
775
+ const spId = isObject(user) && 'sp_id' in user ? user.sp_id : undefined;
776
+ return isStr(spId) ? spId : null;
777
+ } catch (_) {
778
+ return null;
779
+ }
780
+ }
781
+
782
+ /**
783
+ * @summary Logs the user out from the Identity platform
784
+ * @param redirectUri - Where to redirect the browser after logging out of Schibsted account
785
+ * @fires Identity#logoutRequested
786
+ */
787
+ logout(redirectUri: string = this.redirectUri): void {
788
+ this.sessionStorageCache.delete(HAS_SESSION_CACHE_KEY);
789
+ this._maybeClearVarnishCookie();
790
+ const url = this.logoutUrl(redirectUri);
791
+ this.eventTarget.dispatchEvent(
792
+ new CustomEvent('logoutRequested', {
793
+ detail: { url },
794
+ }),
795
+ );
796
+ this.window.location.href = url;
797
+ }
798
+
799
+ /**
800
+ * Generates the link to the new login page that'll be used in the popup or redirect flow
801
+ * @param options
802
+ * @param options.state
803
+ * @param options.acrValues
804
+ * @param options.scope
805
+ * @param options.redirectUri
806
+ * @param options.loginHint
807
+ * @param options.tag
808
+ * @param options.teaser
809
+ * @param options.maxAge
810
+ * @param options.locale
811
+ * @param options.oneStepLogin
812
+ * @param options.prompt
813
+ * @param options.xDomainId
814
+ * @param options.xEnvironmentId
815
+ * @param options.originCampaign
816
+ * @return - The url
817
+ */
818
+ loginUrl({
819
+ state,
820
+ acrValues = '',
821
+ scope = 'openid',
822
+ redirectUri = this.redirectUri,
823
+ loginHint = '',
824
+ tag = '',
825
+ teaser = '',
826
+ maxAge = '',
827
+ locale,
828
+ oneStepLogin = false,
829
+ prompt = 'select_account',
830
+ xDomainId = '',
831
+ xEnvironmentId = '',
832
+ originCampaign = '',
833
+ }: LoginOptions): string {
834
+ const isValidAcrValue = (acrValue: string) =>
835
+ isStrIn(
836
+ acrValue,
837
+ ['password', 'otp', 'sms', 'eid-dk', 'eid-no', 'eid-se', 'eid-fi', 'eid'],
838
+ true,
839
+ );
840
+ assert(
841
+ !acrValues ||
842
+ isStrIn(acrValues, ['', 'otp-email'], true) ||
843
+ acrValues.split(' ').every(isValidAcrValue),
844
+ `The acrValues parameter is not acceptable: ${acrValues}`,
845
+ );
846
+ assert(
847
+ isUrl(redirectUri),
848
+ `loginUrl(): redirectUri must be a valid url but is ${redirectUri}`,
849
+ );
850
+ assert(
851
+ isNonEmptyString(state),
852
+ `the state parameter should be a non empty string but it is ${state}`,
853
+ );
854
+
855
+ return this.oauthService.makeUrl('oauth/authorize', {
856
+ response_type: 'code',
857
+ redirect_uri: redirectUri,
858
+ scope,
859
+ state,
860
+ acr_values: acrValues,
861
+ login_hint: loginHint,
862
+ tag,
863
+ teaser,
864
+ max_age: maxAge,
865
+ locale,
866
+ one_step_login: oneStepLogin || '',
867
+ prompt: acrValues ? '' : prompt,
868
+ x_domain_id: xDomainId,
869
+ x_env_id: xEnvironmentId,
870
+ utm_campaign: originCampaign,
871
+ });
872
+ }
873
+
874
+ /**
875
+ * The url for logging the user out
876
+ * @param redirectUri
877
+ * @return url
878
+ */
879
+ logoutUrl(redirectUri: string = this.redirectUri): string {
880
+ assert(isUrl(redirectUri), `logoutUrl(): redirectUri is invalid`);
881
+ const params = { redirect_uri: redirectUri };
882
+ return this.sessionService.makeUrl('logout', params);
883
+ }
884
+
885
+ /**
886
+ * The account summary page url
887
+ * @param redirectUri
888
+ */
889
+ accountUrl(redirectUri: string = this.redirectUri): string {
890
+ return this.spid.makeUrl('profile-pages', {
891
+ response_type: 'code',
892
+ redirect_uri: redirectUri,
893
+ });
894
+ }
895
+
896
+ /**
897
+ * Function responsible for loading and displaying simplified login widget. How often
898
+ * widget will be display is up to you. Preferred way would be to show it once per user,
899
+ * and store that info in localStorage. Widget will be display only if user is logged in to SSO.
900
+ *
901
+ * @param loginParams - the same as `options` param for login function. Login will be called on user
902
+ * continue action. `state` might be string or async function.
903
+ * @param options - additional configuration of Simplified Login Widget
904
+ * @fires Identity#simplifiedLoginOpened
905
+ * @fires Identity#simplifiedLoginCancelled
906
+ * @throws {SDKError} - If user context data is missing or the widget script cannot be loaded
907
+ * @return - will resolve to true if widget will be displayed
908
+ */
909
+ async showSimplifiedLoginWidget(
910
+ loginParams: SimplifiedLoginWidgetLoginOptions,
911
+ options?: SimplifiedLoginWidgetOptions,
912
+ ): Promise<boolean> {
913
+ // getUserContextData doesn't throw exception
914
+ const userData = await this.getUserContextData();
915
+
916
+ const queryParams: Record<string, string> = { client_id: this.clientId };
917
+ if (options && options.encoding) {
918
+ queryParams.encoding = options.encoding;
919
+ }
920
+ const widgetUrl = this.bffService.makeUrl('simplified-login-widget', queryParams, false);
921
+
922
+ const prepareLoginParams = async (
923
+ loginParams: SimplifiedLoginWidgetLoginOptions,
924
+ ): Promise<LoginOptions> => {
925
+ const state =
926
+ typeof loginParams.state === 'function'
927
+ ? await loginParams.state()
928
+ : loginParams.state;
929
+
930
+ return {
931
+ ...loginParams,
932
+ state,
933
+ };
934
+ };
935
+
936
+ return new Promise<boolean>((resolve, reject) => {
937
+ if (!userData?.display_text || !userData.identifier) {
938
+ reject(new SDKError('Missing user data'));
939
+ return;
940
+ }
941
+
942
+ const initialParams: SimplifiedLoginWidgetInitialParams = {
943
+ displayText: userData.display_text,
944
+ env: this.env,
945
+ clientName: userData.client_name || '',
946
+ clientId: this.clientId,
947
+ providerId: userData.provider_id,
948
+ windowWidth: () => window.innerWidth,
949
+ windowOnResize: (f: () => void) => {
950
+ window.onresize = f;
951
+ },
952
+ };
953
+
954
+ if (options && options.locale) {
955
+ initialParams.locale = options.locale;
956
+ }
957
+
958
+ const loginHandler = async () => {
959
+ this.login(
960
+ Object.assign(await prepareLoginParams(loginParams), {
961
+ loginHint: userData.identifier,
962
+ }) as LoginOptions,
963
+ );
964
+ };
965
+
966
+ const loginNotYouHandler = async () => {
967
+ this.login(
968
+ Object.assign(await prepareLoginParams(loginParams), {
969
+ loginHint: userData.identifier,
970
+ prompt: 'login',
971
+ }) as LoginOptions,
972
+ );
973
+ };
974
+
975
+ const initHandler = () => {
976
+ /**
977
+ * Emitted when the simplified login widget is displayed on the screen
978
+ * @event Identity#simplifiedLoginOpened
979
+ */
980
+ this.eventTarget.dispatchEvent(new Event('simplifiedLoginOpened'));
981
+ };
982
+
983
+ const cancelLoginHandler = () => {
984
+ this.eventTarget.dispatchEvent(new Event('simplifiedLoginCancelled'));
985
+ };
986
+
987
+ if (hasOpenSimplifiedLoginWidget(window)) {
988
+ window.openSimplifiedLoginWidget(
989
+ initialParams,
990
+ loginHandler,
991
+ loginNotYouHandler,
992
+ initHandler,
993
+ cancelLoginHandler,
994
+ );
995
+ return resolve(true);
996
+ }
997
+
998
+ const simplifiedLoginWidget = document.createElement('script');
999
+ simplifiedLoginWidget.type = 'text/javascript';
1000
+ simplifiedLoginWidget.src = widgetUrl;
1001
+ simplifiedLoginWidget.onload = () => {
1002
+ if (!hasOpenSimplifiedLoginWidget(window)) {
1003
+ reject(new SDKError('Error when loading simplified login widget content'));
1004
+ return;
1005
+ }
1006
+ window.openSimplifiedLoginWidget(
1007
+ initialParams,
1008
+ loginHandler,
1009
+ loginNotYouHandler,
1010
+ initHandler,
1011
+ cancelLoginHandler,
1012
+ );
1013
+ resolve(true);
1014
+ };
1015
+ simplifiedLoginWidget.onerror = () => {
1016
+ reject(new SDKError('Error when loading simplified login widget content'));
1017
+ };
1018
+ document.getElementsByTagName('body')[0].appendChild(simplifiedLoginWidget);
1019
+ });
1020
+ }
1021
+ }
1022
+
1023
+ export default Identity;