@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.
- package/README.md +46 -301
- package/dist/account-sdk.d.ts +22 -0
- package/dist/account.d.ts +179 -0
- package/dist/cache.d.ts +65 -0
- package/dist/config.d.ts +86 -0
- package/dist/globals.d.ts +11 -0
- package/dist/has-access-response.d.ts +2 -0
- package/dist/identity.d.ts +302 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +828 -0
- package/dist/index.js.map +1 -0
- package/dist/monetization.d.ts +39 -0
- package/{src → dist}/object.d.ts +4 -9
- package/dist/popup.d.ts +9 -0
- package/dist/resolve-browser-window.d.ts +1 -0
- package/dist/rest-client.d.ts +102 -0
- package/dist/rest-clients.d.ts +10 -0
- package/dist/sdk-error.d.ts +29 -0
- package/dist/session-response.d.ts +54 -0
- package/{src/spidTalk.d.ts → dist/spid-talk.d.ts} +4 -6
- package/dist/types/account.d.ts +48 -0
- package/dist/types/common.d.ts +28 -0
- package/dist/types/identity.d.ts +23 -0
- package/dist/types/login.d.ts +125 -0
- package/dist/types/monetization.d.ts +26 -0
- package/dist/types/session.d.ts +146 -0
- package/dist/url.d.ts +8 -0
- package/dist/validate.d.ts +50 -0
- package/dist/version.d.ts +2 -0
- package/package.json +34 -52
- package/src/account-sdk.ts +92 -0
- package/src/account.ts +332 -0
- package/src/{cache.js → cache.ts} +53 -38
- package/src/{config.js → config.ts} +7 -32
- package/src/globals.ts +8 -0
- package/src/has-access-response.ts +35 -0
- package/src/identity.ts +1023 -0
- package/src/index.ts +40 -0
- package/src/monetization.ts +128 -0
- package/src/{object.js → object.ts} +9 -16
- package/src/popup.ts +74 -0
- package/src/resolve-browser-window.ts +11 -0
- package/src/rest-client.ts +253 -0
- package/src/rest-clients.ts +30 -0
- package/src/sdk-error.ts +61 -0
- package/src/session-response.ts +100 -0
- package/src/{spidTalk.js → spid-talk.ts} +10 -12
- package/src/types/account.ts +55 -0
- package/src/types/common.ts +32 -0
- package/src/types/identity.ts +26 -0
- package/src/types/login.ts +150 -0
- package/src/types/monetization.ts +29 -0
- package/src/types/session.ts +169 -0
- package/src/{url.js → url.ts} +6 -10
- package/src/{validate.js → validate.ts} +27 -43
- package/src/{version.js → version.ts} +1 -2
- package/es5/global.js +0 -12968
- package/es5/global.js.map +0 -1
- package/es5/global.min.js +0 -2
- package/es5/global.min.js.map +0 -1
- package/es5/identity.js +0 -12212
- package/es5/identity.js.map +0 -1
- package/es5/identity.min.js +0 -2
- package/es5/identity.min.js.map +0 -1
- package/es5/index.js +0 -12940
- package/es5/index.js.map +0 -1
- package/es5/index.min.js +0 -2
- package/es5/index.min.js.map +0 -1
- package/es5/monetization.js +0 -9176
- package/es5/monetization.js.map +0 -1
- package/es5/monetization.min.js +0 -2
- package/es5/monetization.min.js.map +0 -1
- package/es5/payment.js +0 -8853
- package/es5/payment.js.map +0 -1
- package/es5/payment.min.js +0 -2
- package/es5/payment.min.js.map +0 -1
- package/identity.d.ts +0 -1
- package/identity.js +0 -5
- package/index.d.ts +0 -4
- package/index.js +0 -9
- package/monetization.d.ts +0 -1
- package/monetization.js +0 -5
- package/payment.d.ts +0 -1
- package/payment.js +0 -5
- package/src/RESTClient.d.ts +0 -89
- package/src/RESTClient.js +0 -193
- package/src/SDKError.d.ts +0 -16
- package/src/SDKError.js +0 -55
- package/src/__mocks__/.eslintrc.yml +0 -8
- package/src/__mocks__/RESTClient.js +0 -54
- package/src/cache.d.ts +0 -64
- package/src/config.d.ts +0 -34
- package/src/es5/global.js +0 -13
- package/src/es5/identity.js +0 -10
- package/src/es5/index.js +0 -13
- package/src/es5/monetization.js +0 -10
- package/src/es5/payment.js +0 -10
- package/src/global-registry.js +0 -20
- package/src/identity.d.ts +0 -679
- package/src/identity.js +0 -1157
- package/src/monetization.d.ts +0 -80
- package/src/monetization.js +0 -172
- package/src/payment.d.ts +0 -115
- package/src/payment.js +0 -211
- package/src/popup.d.ts +0 -10
- package/src/popup.js +0 -59
- package/src/url.d.ts +0 -10
- package/src/validate.d.ts +0 -64
- package/src/version.d.ts +0 -2
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
export { Account } from './account.js';
|
|
6
|
+
export { createAccountSdk, getAccountSdk } from './account-sdk.js';
|
|
7
|
+
export * from './globals.js';
|
|
8
|
+
export { default as SDKError } from './sdk-error.js';
|
|
9
|
+
|
|
10
|
+
/** session response type guards */
|
|
11
|
+
export {
|
|
12
|
+
isConnectedSessionResponse,
|
|
13
|
+
isConnectedSessionWithUserIdResponse,
|
|
14
|
+
isSessionFailureResponse,
|
|
15
|
+
isSessionSuccessResponse,
|
|
16
|
+
} from './session-response.js';
|
|
17
|
+
|
|
18
|
+
/* public available types */
|
|
19
|
+
export type {
|
|
20
|
+
AccountConfig,
|
|
21
|
+
AccountEventListener,
|
|
22
|
+
AccountEventName,
|
|
23
|
+
} from './types/account.js';
|
|
24
|
+
export type { AccountEnvironment, LogFunction } from './types/common.js';
|
|
25
|
+
export type {
|
|
26
|
+
LoginOptions,
|
|
27
|
+
SimplifiedLoginData,
|
|
28
|
+
SimplifiedLoginWidgetLoginOptions,
|
|
29
|
+
SimplifiedLoginWidgetOptions,
|
|
30
|
+
} from './types/login.js';
|
|
31
|
+
export type { HasAccessResult } from './types/monetization.js';
|
|
32
|
+
export type {
|
|
33
|
+
ConnectedSessionResponse,
|
|
34
|
+
ConnectedSessionWithUserIdResponse,
|
|
35
|
+
RedirectSessionResponse,
|
|
36
|
+
SessionFailureResponse,
|
|
37
|
+
SessionResponse,
|
|
38
|
+
SessionSuccessResponse,
|
|
39
|
+
SessionUserId,
|
|
40
|
+
} from './types/session.js';
|
|
@@ -0,0 +1,128 @@
|
|
|
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 { parseHasAccessResult } from './has-access-response.js';
|
|
7
|
+
import type RESTClient from './rest-client.js';
|
|
8
|
+
import { buildClientSdrn, createAccountRestClient } from './rest-clients.js';
|
|
9
|
+
import SDKError from './sdk-error.js';
|
|
10
|
+
import * as spidTalk from './spid-talk.js';
|
|
11
|
+
import type { HasAccessResult, MonetizationOptions } from './types/monetization.js';
|
|
12
|
+
import type { SessionUserId } from './types/session.js';
|
|
13
|
+
import { assert, isNonEmptyString, isStr, isUrl } from './validate.js';
|
|
14
|
+
import version from './version.js';
|
|
15
|
+
|
|
16
|
+
const globalWindow = () => window;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Provides features related to monetization
|
|
20
|
+
*/
|
|
21
|
+
export class Monetization {
|
|
22
|
+
private readonly cache: Cache;
|
|
23
|
+
private readonly sessionService?: RESTClient;
|
|
24
|
+
private readonly pendingHasAccessRequests: Record<string, Promise<HasAccessResult>>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param options - Monetization configuration
|
|
28
|
+
* @throws {SDKError} - If any of options are invalid
|
|
29
|
+
*/
|
|
30
|
+
constructor({
|
|
31
|
+
clientId,
|
|
32
|
+
redirectUri,
|
|
33
|
+
env = 'PRE',
|
|
34
|
+
sessionDomain,
|
|
35
|
+
window = globalWindow(),
|
|
36
|
+
}: MonetizationOptions) {
|
|
37
|
+
spidTalk.emulate(window);
|
|
38
|
+
assert(isNonEmptyString(clientId), 'clientId parameter is required');
|
|
39
|
+
assert(isStr(env), `env parameter is invalid: ${env}`);
|
|
40
|
+
const clientSdrn = buildClientSdrn(env, clientId);
|
|
41
|
+
|
|
42
|
+
this.cache = new Cache(() => window && window.sessionStorage);
|
|
43
|
+
this.pendingHasAccessRequests = {};
|
|
44
|
+
|
|
45
|
+
if (sessionDomain) {
|
|
46
|
+
assert(isUrl(sessionDomain), 'sessionDomain parameter is not a valid URL');
|
|
47
|
+
this.sessionService = createAccountRestClient({
|
|
48
|
+
serverUrl: sessionDomain,
|
|
49
|
+
defaultParams: {
|
|
50
|
+
client_sdrn: clientSdrn,
|
|
51
|
+
redirect_uri: redirectUri,
|
|
52
|
+
sdk_version: version,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Checks if the user has access to a set of products or features.
|
|
60
|
+
* @param productIds - which products/features to check
|
|
61
|
+
* @param userId - id of currently logged in user
|
|
62
|
+
* @throws {SDKError} - If the input is incorrect, or a network call fails in any way
|
|
63
|
+
* (this will happen if, say, the user is not logged in)
|
|
64
|
+
* @returns The data object returned from Schibsted account (or `null` if the user
|
|
65
|
+
* doesn't have access to any of the given products/features)
|
|
66
|
+
*/
|
|
67
|
+
async hasAccess(productIds: string[], userId: SessionUserId): Promise<HasAccessResult | null> {
|
|
68
|
+
if (!this.sessionService) {
|
|
69
|
+
throw new SDKError(`hasAccess can only be called if 'sessionDomain' is configured`);
|
|
70
|
+
}
|
|
71
|
+
if (!userId) {
|
|
72
|
+
throw new SDKError(`'userId' must be specified`);
|
|
73
|
+
}
|
|
74
|
+
if (!Array.isArray(productIds)) {
|
|
75
|
+
throw new SDKError(`'productIds' must be an array`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sortedIds = [...productIds].sort();
|
|
79
|
+
const cacheKey = this.accessCacheKey(sortedIds, userId);
|
|
80
|
+
let data = this.cache.get(cacheKey) as HasAccessResult | null;
|
|
81
|
+
if (!data) {
|
|
82
|
+
if (!this.pendingHasAccessRequests[cacheKey]) {
|
|
83
|
+
this.pendingHasAccessRequests[cacheKey] = this.sessionService.get(
|
|
84
|
+
`/hasAccess/${sortedIds.join(',')}`,
|
|
85
|
+
undefined,
|
|
86
|
+
parseHasAccessResult,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
const promise = this.pendingHasAccessRequests[cacheKey];
|
|
90
|
+
try {
|
|
91
|
+
data = await promise;
|
|
92
|
+
const expiresSeconds = data.ttl;
|
|
93
|
+
this.cache.set(cacheKey, data, expiresSeconds * 1000);
|
|
94
|
+
} finally {
|
|
95
|
+
if (this.pendingHasAccessRequests[cacheKey] === promise) {
|
|
96
|
+
delete this.pendingHasAccessRequests[cacheKey];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!data.entitled) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Removes the cached access result.
|
|
110
|
+
* @param productIds - which products/features to check
|
|
111
|
+
* @param userId - id of currently logged in user
|
|
112
|
+
*/
|
|
113
|
+
clearCachedAccessResult(productIds: string[], userId: SessionUserId): void {
|
|
114
|
+
this.cache.delete(this.accessCacheKey(productIds, userId));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Compute "has access" cache key for the given product ids and user id.
|
|
119
|
+
* @private
|
|
120
|
+
* @param productIds - which products/features to check
|
|
121
|
+
* @param userId - id of currently logged in user
|
|
122
|
+
*/
|
|
123
|
+
private accessCacheKey(productIds: string[], userId: SessionUserId): string {
|
|
124
|
+
return `prd_${[...productIds].sort()}_${userId}`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export default Monetization;
|
|
@@ -2,41 +2,36 @@
|
|
|
2
2
|
* See LICENSE.md in the project root.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
'use strict';
|
|
6
|
-
|
|
7
5
|
/**
|
|
8
6
|
* @summary Some routines that work on javascript objects
|
|
9
7
|
* @private
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
|
-
import
|
|
13
|
-
import
|
|
10
|
+
import SDKError from './sdk-error.js';
|
|
11
|
+
import { assert, isNonEmptyObj, isObject } from './validate.js';
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* Similar to Object.assign({}, src) but only clones the keys of an object that have non-undefined
|
|
17
15
|
* values.
|
|
18
16
|
* Please note that the values in the rightmost parameters can overwrite the values of the earlier
|
|
19
17
|
* parameters so the order of the parameters matters.
|
|
20
|
-
* @memberof core
|
|
21
18
|
* @example
|
|
22
19
|
* cloneDefined({foo: 1, bar: 2}, {foo: 2}) // returns {foo: 2, bar: 2}
|
|
23
20
|
*
|
|
24
|
-
* @param
|
|
21
|
+
* @param sources - one or more sources. Their defined properties will override the ones
|
|
25
22
|
* that came before it. For example if `source1.foo = 'bar'` and `source2.foo = 'baz'`, the
|
|
26
23
|
* result will include `{ foo: 'baz' }`
|
|
27
|
-
* @return {object} a new object that is similar to src with all the key/values where the
|
|
28
|
-
* keys for undefined values are removed.
|
|
29
24
|
*/
|
|
30
|
-
export function cloneDefined(...sources) {
|
|
31
|
-
const result = {};
|
|
25
|
+
export function cloneDefined(...sources: object[]): Record<string, unknown> {
|
|
26
|
+
const result: Record<string, unknown> = {};
|
|
32
27
|
if (!(sources && sources.length)) {
|
|
33
28
|
throw new SDKError('No objects to clone');
|
|
34
29
|
}
|
|
35
|
-
sources.forEach(source => {
|
|
30
|
+
sources.forEach((source) => {
|
|
36
31
|
assert(isObject(source));
|
|
37
32
|
if (isNonEmptyObj(source)) {
|
|
38
33
|
Object.entries(source).forEach(([key, value]) => {
|
|
39
|
-
if (typeof value !== 'undefined'
|
|
34
|
+
if (typeof value !== 'undefined') {
|
|
40
35
|
result[key] = isObject(value) ? cloneDeep(value) : value;
|
|
41
36
|
}
|
|
42
37
|
});
|
|
@@ -47,13 +42,11 @@ export function cloneDefined(...sources) {
|
|
|
47
42
|
|
|
48
43
|
/**
|
|
49
44
|
* Deep copies an object. This is handy for immutability.
|
|
50
|
-
* @
|
|
51
|
-
* @param {object} obj - An object, array or null.
|
|
52
|
-
* @return {object} - an exact copy of the object but deep copied
|
|
45
|
+
* @param obj - An object, array or null.
|
|
53
46
|
* @throws {SDKError} - if the obj is not an accepted type or is not
|
|
54
47
|
* stringifiable by JSON for example if it has loops
|
|
55
48
|
*/
|
|
56
|
-
export function cloneDeep(obj) {
|
|
49
|
+
export function cloneDeep<T extends object | null>(obj: T): T {
|
|
57
50
|
assert(typeof obj === 'object', `obj should be an object (even null) but it is ${obj}`);
|
|
58
51
|
return JSON.parse(JSON.stringify(obj)) || obj;
|
|
59
52
|
}
|
package/src/popup.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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 { cloneDefined } from './object.js';
|
|
6
|
+
import { assert, isFunction, isObject, isUrl } from './validate.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serializes an object to string.
|
|
10
|
+
* @private
|
|
11
|
+
* @param obj - for example {a: 'b', c: 1}
|
|
12
|
+
*/
|
|
13
|
+
function serialize(obj: Record<string, unknown>): string {
|
|
14
|
+
assert(isObject(obj), `Object must be an object but it is '${obj}'`);
|
|
15
|
+
return Object.keys(obj)
|
|
16
|
+
.map((key) => `${key}=${obj[key]}`)
|
|
17
|
+
.join(',');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultWindowFeatures = {
|
|
21
|
+
scrollbars: 'yes',
|
|
22
|
+
location: 'yes',
|
|
23
|
+
status: 'no',
|
|
24
|
+
menubar: 'no',
|
|
25
|
+
toolbar: 'no',
|
|
26
|
+
resizable: 'yes',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Opens a popup
|
|
31
|
+
* @param parentWindow - A reference to the window that will open the popup
|
|
32
|
+
* @param url - The URL that the popup will open
|
|
33
|
+
* @param windowName - A name for the window
|
|
34
|
+
* @param windowFeatures - Window features for the popup (default ones are usually ok)
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
export function open(
|
|
38
|
+
parentWindow: Window,
|
|
39
|
+
url: string,
|
|
40
|
+
windowName = '',
|
|
41
|
+
windowFeatures: Record<string, string | number> = {},
|
|
42
|
+
): Window | null {
|
|
43
|
+
assert(isObject(parentWindow), `window was supposed to be an object but it is ${parentWindow}`);
|
|
44
|
+
assert(
|
|
45
|
+
isObject(parentWindow.screen),
|
|
46
|
+
`window should be a valid Window object but it lacks a 'screen' property`,
|
|
47
|
+
);
|
|
48
|
+
assert(
|
|
49
|
+
isFunction(parentWindow.open),
|
|
50
|
+
`window should be a valid Window object but it lacks an 'open' function`,
|
|
51
|
+
);
|
|
52
|
+
assert(isUrl(url), 'Invalid URL for popup');
|
|
53
|
+
|
|
54
|
+
const { height, width } = parentWindow.screen;
|
|
55
|
+
|
|
56
|
+
const mergedFeatures = cloneDefined(defaultWindowFeatures, windowFeatures);
|
|
57
|
+
const { width: featureWidth, height: featureHeight } = mergedFeatures;
|
|
58
|
+
if (
|
|
59
|
+
typeof featureWidth === 'number' &&
|
|
60
|
+
Number.isFinite(featureWidth) &&
|
|
61
|
+
Number.isFinite(width)
|
|
62
|
+
) {
|
|
63
|
+
mergedFeatures.left = (width - featureWidth) / 2;
|
|
64
|
+
}
|
|
65
|
+
if (
|
|
66
|
+
typeof featureHeight === 'number' &&
|
|
67
|
+
Number.isFinite(featureHeight) &&
|
|
68
|
+
Number.isFinite(height)
|
|
69
|
+
) {
|
|
70
|
+
mergedFeatures.top = (height - featureHeight) / 2;
|
|
71
|
+
}
|
|
72
|
+
const features = serialize(mergedFeatures);
|
|
73
|
+
return parentWindow.open(url, windowName, features);
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
export const resolveBrowserWindow = (providedWindow?: Window): Window | undefined => {
|
|
6
|
+
if (providedWindow) {
|
|
7
|
+
return providedWindow;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return typeof window === 'undefined' ? undefined : window;
|
|
11
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
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 { cloneDefined } from './object.js';
|
|
6
|
+
import SDKError from './sdk-error.js';
|
|
7
|
+
import type { LogFunction } from './types/common.js';
|
|
8
|
+
import { urlMapper } from './url.js';
|
|
9
|
+
import { assert, isFunction, isNonEmptyString, isObject, isStr } from './validate.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Converts a series of parameters of various types to a string that's suitable for logging.
|
|
13
|
+
* @private
|
|
14
|
+
* @param msg - The values to format; objects are stringified to JSON
|
|
15
|
+
*/
|
|
16
|
+
const logString = (msg: unknown[]): string =>
|
|
17
|
+
msg.map((m) => (isObject(m) ? JSON.stringify(m, null, 2) : m)).join(' ');
|
|
18
|
+
|
|
19
|
+
const logFn = (fn: LogFunction | undefined, ...msg: unknown[]): void => {
|
|
20
|
+
if (fn) {
|
|
21
|
+
fn(logString(msg));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Encode a string like URLSearchParams would do
|
|
27
|
+
* @private
|
|
28
|
+
* @param str - The input
|
|
29
|
+
*/
|
|
30
|
+
function encode(str: string): string {
|
|
31
|
+
const replace: Record<string, string> = {
|
|
32
|
+
'!': '%21',
|
|
33
|
+
"'": '%27',
|
|
34
|
+
'(': '%28',
|
|
35
|
+
')': '%29',
|
|
36
|
+
'~': '%7E',
|
|
37
|
+
'%20': '+',
|
|
38
|
+
'%00': '\x00',
|
|
39
|
+
};
|
|
40
|
+
return encodeURIComponent(str).replace(/[!'()~]|%20|%00/g, (match) => replace[match]);
|
|
41
|
+
}
|
|
42
|
+
type FetchFunction = (input: RequestInfo, init?: RequestInit) => Promise<Response>;
|
|
43
|
+
const globalFetch = () => window.fetch && window.fetch.bind(window);
|
|
44
|
+
const noFetch: FetchFunction = () => {
|
|
45
|
+
throw new SDKError('fetch is not available in this environment');
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The value of a single query parameter; `undefined` entries are skipped.
|
|
50
|
+
*/
|
|
51
|
+
type QueryValue = string | number | boolean | undefined;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Query parameters appended to a request URL.
|
|
55
|
+
*/
|
|
56
|
+
type QueryParams = Record<string, QueryValue>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* HTTP method used for a request.
|
|
60
|
+
*/
|
|
61
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
|
62
|
+
|
|
63
|
+
type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
64
|
+
|
|
65
|
+
type ResponseParser<T> = (value: JsonValue) => T;
|
|
66
|
+
|
|
67
|
+
type RESTRequestOptions<T = JsonValue> = {
|
|
68
|
+
method: HttpMethod;
|
|
69
|
+
pathname: string;
|
|
70
|
+
data?: QueryParams;
|
|
71
|
+
headers?: Record<string, string>;
|
|
72
|
+
useDefaultParams?: boolean;
|
|
73
|
+
fetchOptions?: RequestInit;
|
|
74
|
+
parseResponse?: ResponseParser<T>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This class can be used for creating a wrapper around a server and all its endpoints.
|
|
79
|
+
* Its functionality is extended by {@link JSONPClient}
|
|
80
|
+
* Creates a client to a REST server. While useful stand-alone, it's also used for some other
|
|
81
|
+
* types of client that change some functionalities.
|
|
82
|
+
* @throws {SDKError} - If any of options are invalid
|
|
83
|
+
* @summary The simplest way to communicate to a REST endpoint without any
|
|
84
|
+
* special authentication
|
|
85
|
+
* @private
|
|
86
|
+
*/
|
|
87
|
+
export class RESTClient {
|
|
88
|
+
url: URL;
|
|
89
|
+
defaultParams: QueryParams;
|
|
90
|
+
log?: LogFunction;
|
|
91
|
+
fetch: FetchFunction;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param options
|
|
95
|
+
* @param options.serverUrl - The URL to the server eg.
|
|
96
|
+
* https://login.schibsted.com or a URL key like 'DEV' in combination with {@link envDic}.
|
|
97
|
+
* @param options.envDic - A dictionary that will be used for looking up
|
|
98
|
+
* {@link serverUrl} keys. If serverUrl is always a URL, you don't need this.
|
|
99
|
+
* @param options.fetch - The fetch function to use. It can be native
|
|
100
|
+
* or a polyfill
|
|
101
|
+
* @param options.log - A function that will be called with log messages about
|
|
102
|
+
* request and response
|
|
103
|
+
* @param options.defaultParams - Query parameters added to every request
|
|
104
|
+
*/
|
|
105
|
+
constructor({
|
|
106
|
+
serverUrl = 'PRE',
|
|
107
|
+
envDic,
|
|
108
|
+
fetch = globalFetch() ?? noFetch,
|
|
109
|
+
log,
|
|
110
|
+
defaultParams = {},
|
|
111
|
+
}: {
|
|
112
|
+
serverUrl?: string;
|
|
113
|
+
envDic?: Record<string, string>;
|
|
114
|
+
fetch?: FetchFunction;
|
|
115
|
+
log?: LogFunction;
|
|
116
|
+
defaultParams?: QueryParams;
|
|
117
|
+
}) {
|
|
118
|
+
assert(isObject(defaultParams), `defaultParams should be a non-null object`);
|
|
119
|
+
|
|
120
|
+
const mappedUrl = urlMapper(serverUrl, envDic ?? {});
|
|
121
|
+
const handledServerUrl = mappedUrl.endsWith('/') ? mappedUrl : `${mappedUrl}/`;
|
|
122
|
+
this.url = new URL(handledServerUrl);
|
|
123
|
+
|
|
124
|
+
this.defaultParams = defaultParams;
|
|
125
|
+
|
|
126
|
+
if (log) {
|
|
127
|
+
assert(isFunction(log), `log must be a function but it is ${log}`);
|
|
128
|
+
this.log = log;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.fetch = fetch;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Makes the actual call to the server and deals with headers, data objects and the edge cases.
|
|
136
|
+
* Please note that this method expects the response to be in JSON format. However, it'll not
|
|
137
|
+
* parse the response if its code is not in the 200 range.
|
|
138
|
+
* @param options - An obligatory options object
|
|
139
|
+
* @param options.method - The HTTP request method, e.g. 'GET' or 'POST'
|
|
140
|
+
* @param options.pathname - The path to the endpoint like 'api/2/endpoint-name'
|
|
141
|
+
* @param options.data - Query parameters added to the request URL
|
|
142
|
+
* @param options.headers - Request headers as a plain key/value map
|
|
143
|
+
* @param options.useDefaultParams - Should we add the defaultParams to the query?
|
|
144
|
+
* @param options.fetchOptions - Additional fetch options
|
|
145
|
+
* @throws {SDKError} - If the call can't be made for whatever reason.
|
|
146
|
+
*/
|
|
147
|
+
// Overloads describe the two supported modes:
|
|
148
|
+
// - without a parser: return the parsed JSON response as-is;
|
|
149
|
+
// - with a parser: return the parser's typed result.
|
|
150
|
+
async go(options: RESTRequestOptions): Promise<JsonValue>;
|
|
151
|
+
async go<T>(options: RESTRequestOptions<T> & { parseResponse: ResponseParser<T> }): Promise<T>;
|
|
152
|
+
|
|
153
|
+
// TypeScript requires one implementation signature for the overloads above.
|
|
154
|
+
async go<T = JsonValue>(options: RESTRequestOptions<T>): Promise<JsonValue | T> {
|
|
155
|
+
const {
|
|
156
|
+
method,
|
|
157
|
+
headers,
|
|
158
|
+
pathname,
|
|
159
|
+
data = {},
|
|
160
|
+
useDefaultParams = true,
|
|
161
|
+
fetchOptions = { method: options.method, credentials: 'include' },
|
|
162
|
+
parseResponse,
|
|
163
|
+
} = options;
|
|
164
|
+
assert(isNonEmptyString(method), `Method must be a non empty string but it is "${method}"`);
|
|
165
|
+
assert(isNonEmptyString(pathname), `Pathname must be string but it is "${pathname}"`);
|
|
166
|
+
assert(isObject(data), `data must be a non-null object`);
|
|
167
|
+
|
|
168
|
+
fetchOptions.headers = isObject(headers) ? headers : {};
|
|
169
|
+
const fullUrl = this.makeUrl(pathname, data, useDefaultParams);
|
|
170
|
+
|
|
171
|
+
logFn(this.log, 'Request:', method.toUpperCase(), fullUrl);
|
|
172
|
+
logFn(this.log, 'Request Headers:', fetchOptions.headers);
|
|
173
|
+
logFn(this.log, 'Request Body:', fetchOptions.body);
|
|
174
|
+
try {
|
|
175
|
+
const response = await this.fetch(fullUrl, fetchOptions);
|
|
176
|
+
logFn(this.log, 'Response Code:', response.status, response.statusText);
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
// status code not in range 200-299
|
|
179
|
+
throw new SDKError(response.statusText, { code: response.status });
|
|
180
|
+
}
|
|
181
|
+
const responseObject: JsonValue = await response.json();
|
|
182
|
+
logFn(this.log, 'Response Parsed:', responseObject);
|
|
183
|
+
return parseResponse ? parseResponse(responseObject) : responseObject;
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
let msg = isStr(err) ? err : 'Unknown RESTClient error';
|
|
186
|
+
if (isObject(err) && isStr(err.message)) {
|
|
187
|
+
msg = err.message;
|
|
188
|
+
}
|
|
189
|
+
const errorObject = isObject(err) ? err : undefined;
|
|
190
|
+
throw new SDKError(`Failed to '${method}' '${fullUrl}': '${msg}'`, errorObject);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Creates a url that points to an endpoint in the server
|
|
196
|
+
* @param pathname - WHATWG pathname ie. 'api/2/endpoint-name'
|
|
197
|
+
* @param query - Query parameters to serialize into the query string
|
|
198
|
+
* @param useDefaultParams - Should we add the defaultParams to the query?
|
|
199
|
+
*/
|
|
200
|
+
makeUrl(pathname = '', query: QueryParams = {}, useDefaultParams = true): string {
|
|
201
|
+
const handledPathname = pathname.startsWith('/') ? pathname.slice(1) : pathname;
|
|
202
|
+
|
|
203
|
+
const url = new URL(handledPathname, this.url);
|
|
204
|
+
url.search = RESTClient.search(query, useDefaultParams, this.defaultParams);
|
|
205
|
+
return url.href;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Make a GET request
|
|
210
|
+
* @param pathname - WHATWG pathname ie. 'api/2/endpoint-name'
|
|
211
|
+
* @param data - Query parameters
|
|
212
|
+
*/
|
|
213
|
+
// Mirrors `go()` overloads for the common GET helper.
|
|
214
|
+
get(pathname: string, data?: QueryParams): Promise<JsonValue>;
|
|
215
|
+
get<T>(
|
|
216
|
+
pathname: string,
|
|
217
|
+
data: QueryParams | undefined,
|
|
218
|
+
parseResponse: ResponseParser<T>,
|
|
219
|
+
): Promise<T>;
|
|
220
|
+
|
|
221
|
+
// TypeScript requires one implementation signature for the overloads above.
|
|
222
|
+
get<T>(
|
|
223
|
+
pathname: string,
|
|
224
|
+
data?: QueryParams,
|
|
225
|
+
parseResponse?: ResponseParser<T>,
|
|
226
|
+
): Promise<JsonValue | T> {
|
|
227
|
+
if (parseResponse) {
|
|
228
|
+
return this.go({ method: 'GET', pathname, data, parseResponse });
|
|
229
|
+
}
|
|
230
|
+
return this.go({ method: 'GET', pathname, data });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Construct query string for WHATWG urls
|
|
235
|
+
* @private
|
|
236
|
+
* @param query - Query parameters to generate the query string from
|
|
237
|
+
* @param useDefaultParams - Use defaultParams or not
|
|
238
|
+
* @param defaultParams - Default params
|
|
239
|
+
*/
|
|
240
|
+
private static search(
|
|
241
|
+
query: QueryParams,
|
|
242
|
+
useDefaultParams: boolean,
|
|
243
|
+
defaultParams: QueryParams,
|
|
244
|
+
): string {
|
|
245
|
+
const params = useDefaultParams ? cloneDefined(defaultParams, query) : cloneDefined(query);
|
|
246
|
+
return Object.keys(params)
|
|
247
|
+
.filter((p) => params[p] !== '')
|
|
248
|
+
.map((p) => `${encode(p)}=${encode(String(params[p]))}`)
|
|
249
|
+
.join('&');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default RESTClient;
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
import { NAMESPACE } from './config.js';
|
|
6
|
+
import RESTClient from './rest-client.js';
|
|
7
|
+
import type { LogFunction } from './types/common.js';
|
|
8
|
+
import { assert, isNonEmptyString } from './validate.js';
|
|
9
|
+
|
|
10
|
+
type AccountRestClientOptions = {
|
|
11
|
+
serverUrl: string;
|
|
12
|
+
log?: LogFunction;
|
|
13
|
+
defaultParams: Record<string, string | number | boolean | undefined>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const isKnownNamespace = (env: string): env is keyof typeof NAMESPACE =>
|
|
17
|
+
Object.hasOwn(NAMESPACE, env);
|
|
18
|
+
|
|
19
|
+
export const buildClientSdrn = (env: string, clientId: string): string => {
|
|
20
|
+
assert(isNonEmptyString(clientId), 'clientId parameter is required');
|
|
21
|
+
assert(isKnownNamespace(env), `env parameter is invalid: ${env}`);
|
|
22
|
+
|
|
23
|
+
return `sdrn:${NAMESPACE[env]}:client:${clientId}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const createAccountRestClient = ({
|
|
27
|
+
serverUrl,
|
|
28
|
+
log,
|
|
29
|
+
defaultParams,
|
|
30
|
+
}: AccountRestClientOptions): RESTClient => new RESTClient({ serverUrl, log, defaultParams });
|
package/src/sdk-error.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
/*
|
|
6
|
+
* Note: this module can't have any internal dependencies because it's used in ./validate which
|
|
7
|
+
* in turn is used as a dependency to a lot of other modules. Doing so may create a circular
|
|
8
|
+
* dependency that's hard to debug in Node.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const STRINGIFY_TYPES = ['boolean', 'number', 'string'];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom error class thrown by all SDK methods on failure.
|
|
15
|
+
*
|
|
16
|
+
* All rejected promises from API calls reject with an instance of this class.
|
|
17
|
+
*
|
|
18
|
+
* @group Errors
|
|
19
|
+
*/
|
|
20
|
+
export default class SDKError extends Error {
|
|
21
|
+
/** HTTP status code from the server error payload;
|
|
22
|
+
* declared explicitly to allow numeric comparisons without type-casting.
|
|
23
|
+
*/
|
|
24
|
+
code?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Properties copied from the server error object.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param message - Human-readable error message
|
|
33
|
+
* @param errorObject - Optional server error payload. All enumerable properties are
|
|
34
|
+
* shallow-copied onto this instance.
|
|
35
|
+
*/
|
|
36
|
+
constructor(message: string, errorObject?: Record<string, unknown>) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'SDKError';
|
|
39
|
+
|
|
40
|
+
if (errorObject) {
|
|
41
|
+
try {
|
|
42
|
+
Object.assign(this, errorObject);
|
|
43
|
+
} catch {
|
|
44
|
+
// silent
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Returns a multi-line string with the error name, message, and any other properties copied
|
|
51
|
+
* from the server error payload.
|
|
52
|
+
*/
|
|
53
|
+
toString(): string {
|
|
54
|
+
const ret = `${this.name}: ${this.message}`;
|
|
55
|
+
const additionalInfo = Object.keys(this)
|
|
56
|
+
.filter((key) => key !== 'name' && STRINGIFY_TYPES.includes(typeof this[key]))
|
|
57
|
+
.map((key) => ` ${key}: ${this[key]}`)
|
|
58
|
+
.join('\n');
|
|
59
|
+
return additionalInfo ? `${ret}\n${additionalInfo}` : ret;
|
|
60
|
+
}
|
|
61
|
+
}
|