@schibsted/account-sdk-browser 6.0.0-alpha.1 → 6.0.0-alpha.3

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