@scayle/storefront-core 7.32.0 → 7.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @scayle/storefront-core
2
2
 
3
+ ## 7.34.0
4
+
5
+ ### Minor Changes
6
+
7
+ - We added support for Identity Provider (IDP), enhancing our authentication and identity management capabilities.
8
+
9
+ ## 7.33.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Previously the caching time for product RPCs was one hour which could easily lead to outdated product information for a long time.
14
+
15
+ The `getProductById` and `getProductsByIds` RPCs are now cached for 5 Minutes and `getProductsByCategory` is now cached for 15 Minutes.
16
+
3
17
  ## 7.32.0
4
18
 
5
19
  ### Minor Changes
@@ -40,6 +40,7 @@ class OAuthClient {
40
40
  headers;
41
41
  baseURL;
42
42
  logger;
43
+ clientId;
43
44
  constructor(options, logger) {
44
45
  const {
45
46
  clientId,
@@ -49,6 +50,7 @@ class OAuthClient {
49
50
  if (!clientId || !clientSecret) {
50
51
  throw new MissingCredentialsError();
51
52
  }
53
+ this.clientId = clientId;
52
54
  this.baseURL = `${apiHost}/v1`;
53
55
  this.logger = logger ? logger.space("auth-client") : void 0;
54
56
  const basicAuthHash = (0, _hash.encodeBase64)(`${clientId}:${clientSecret}`);
@@ -174,5 +176,19 @@ class OAuthClient {
174
176
  }
175
177
  }).then(emptyOAuthResponseHandler);
176
178
  }
179
+ /**
180
+ * Revoke all tokens for a user
181
+ * @param userId
182
+ */
183
+ async generateToken(code) {
184
+ return await fetch(`${this.baseURL}/oauth/token`, {
185
+ method: "POST",
186
+ headers: this.headers,
187
+ body: JSON.stringify({
188
+ grant_type: "authorization_code",
189
+ code
190
+ })
191
+ }).then(oauthResponseHandler);
192
+ }
177
193
  }
178
194
  exports.OAuthClient = OAuthClient;
@@ -15,6 +15,7 @@ export declare class OAuthClient {
15
15
  headers: HeadersInit;
16
16
  baseURL: string;
17
17
  logger?: Log;
18
+ clientId?: string;
18
19
  constructor(options: OAuthOptions, logger?: Log);
19
20
  /**
20
21
  * Register a user and retrieve a token set
@@ -64,4 +65,9 @@ export declare class OAuthClient {
64
65
  * @param accessToken
65
66
  */
66
67
  revokeToken(accessToken: string): Promise<void>;
68
+ /**
69
+ * Revoke all tokens for a user
70
+ * @param userId
71
+ */
72
+ generateToken(code: string): Promise<Oauth>;
67
73
  }
@@ -29,11 +29,13 @@ export class OAuthClient {
29
29
  headers;
30
30
  baseURL;
31
31
  logger;
32
+ clientId;
32
33
  constructor(options, logger) {
33
34
  const { clientId, clientSecret, apiHost } = options;
34
35
  if (!clientId || !clientSecret) {
35
36
  throw new MissingCredentialsError();
36
37
  }
38
+ this.clientId = clientId;
37
39
  this.baseURL = `${apiHost}/v1`;
38
40
  this.logger = logger ? logger.space("auth-client") : void 0;
39
41
  const basicAuthHash = encodeBase64(`${clientId}:${clientSecret}`);
@@ -159,4 +161,18 @@ export class OAuthClient {
159
161
  }
160
162
  }).then(emptyOAuthResponseHandler);
161
163
  }
164
+ /**
165
+ * Revoke all tokens for a user
166
+ * @param userId
167
+ */
168
+ async generateToken(code) {
169
+ return await fetch(`${this.baseURL}/oauth/token`, {
170
+ method: "POST",
171
+ headers: this.headers,
172
+ body: JSON.stringify({
173
+ grant_type: "authorization_code",
174
+ code
175
+ })
176
+ }).then(oauthResponseHandler);
177
+ }
162
178
  }
@@ -3,11 +3,12 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.Cached = void 0;
6
+ exports.MINUTE = exports.Cached = void 0;
7
7
  var _autobind = require("../helpers/autobind.cjs");
8
8
  var _utils = require("../utils/index.cjs");
9
9
  var _hash = require("../utils/hash.cjs");
10
10
  var _constants = require("../constants/index.cjs");
11
+ const MINUTE = exports.MINUTE = 60;
11
12
  const CACHE_NOT_INITIALIZED_MSG = "Cache is not initialized";
12
13
  class Cached {
13
14
  cache;
@@ -66,7 +67,7 @@ class Cached {
66
67
  if (!this.isCacheEnabled) {
67
68
  return;
68
69
  }
69
- const ttl = options?.ttl || 60 * 60;
70
+ const ttl = options?.ttl || 60 * MINUTE;
70
71
  if (ttl) {
71
72
  await (0, _utils.timeout)(_constants.CACHE_TIMEOUT, this.cache.set(cacheKey, value, ttl));
72
73
  }
@@ -1,5 +1,6 @@
1
1
  import { Log } from '../utils';
2
2
  import { Cache as CacheInterface } from './cache';
3
+ export declare const MINUTE = 60;
3
4
  export type CacheOptions = Partial<{
4
5
  ttl: number;
5
6
  cacheKey: string;
@@ -2,6 +2,7 @@ import { autobind } from "../helpers/autobind.mjs";
2
2
  import { timeout } from "../utils/index.mjs";
3
3
  import { sha256 } from "../utils/hash.mjs";
4
4
  import { CACHE_TIMEOUT } from "../constants/index.mjs";
5
+ export const MINUTE = 60;
5
6
  const CACHE_NOT_INITIALIZED_MSG = "Cache is not initialized";
6
7
  export class Cached {
7
8
  cache;
@@ -63,7 +64,7 @@ export class Cached {
63
64
  if (!this.isCacheEnabled) {
64
65
  return;
65
66
  }
66
- const ttl = options?.ttl || 60 * 60;
67
+ const ttl = options?.ttl || 60 * MINUTE;
67
68
  if (ttl) {
68
69
  await timeout(CACHE_TIMEOUT, this.cache.set(cacheKey, value, ttl));
69
70
  }
@@ -156,4 +156,15 @@ Object.keys(_promotion).forEach(function (key) {
156
156
  return _promotion[key];
157
157
  }
158
158
  });
159
+ });
160
+ var _idp = require("./oauth/idp.cjs");
161
+ Object.keys(_idp).forEach(function (key) {
162
+ if (key === "default" || key === "__esModule") return;
163
+ if (key in exports && exports[key] === _idp[key]) return;
164
+ Object.defineProperty(exports, key, {
165
+ enumerable: true,
166
+ get: function () {
167
+ return _idp[key];
168
+ }
169
+ });
159
170
  });
@@ -12,3 +12,4 @@ export * from './variants';
12
12
  export * from './navigationTrees';
13
13
  export * from './session';
14
14
  export * from './promotion';
15
+ export * from './oauth/idp';
@@ -12,3 +12,4 @@ export * from "./variants.mjs";
12
12
  export * from "./navigationTrees.mjs";
13
13
  export * from "./session.mjs";
14
14
  export * from "./promotion.mjs";
15
+ export * from "./oauth/idp.mjs";
@@ -0,0 +1,55 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.handleIDPLoginCallback = exports.getExternalIdpRedirect = void 0;
7
+ var _jose = require("jose");
8
+ var _oauth = require("../../../api/oauth.cjs");
9
+ const getExternalIdpRedirect = exports.getExternalIdpRedirect = async function getExternalIdpRedirect2(context) {
10
+ const shopId = context.shopId.toString();
11
+ const OAuthClient = (0, _oauth.getOAuthClient)(context);
12
+ const checkoutSecret = context.checkout.secret;
13
+ const isIDPEnabled = context.idp.enabled;
14
+ const IDPKeys = context.idp.idpKeys;
15
+ const IDPRedirectURL = context.idp.idpRedirectURL;
16
+ if (!isIDPEnabled) {
17
+ throw new Error("IDP disabled");
18
+ }
19
+ if (!IDPKeys.length) {
20
+ throw new Error("No IDP keys configured");
21
+ }
22
+ if (!IDPRedirectURL) {
23
+ throw new Error("No redirect URL");
24
+ }
25
+ if (!shopId) {
26
+ throw new Error("Missing shopId");
27
+ }
28
+ const secret = new TextEncoder().encode(checkoutSecret);
29
+ const results = await Promise.all(IDPKeys.map(async idpKey => {
30
+ const jwtPayload = await new _jose.SignJWT({
31
+ idpKey,
32
+ callbackUrl: IDPRedirectURL,
33
+ clientId: OAuthClient.clientId.toString()
34
+ }).setProtectedHeader({
35
+ alg: "HS256",
36
+ typ: "JWT"
37
+ }).setIssuedAt().setExpirationTime("2h").sign(secret);
38
+ return [idpKey, `${OAuthClient.baseURL}/auth/external/redirect?shopId=${shopId}&jwt=${jwtPayload}`];
39
+ }));
40
+ return Object.fromEntries(results);
41
+ };
42
+ const handleIDPLoginCallback = exports.handleIDPLoginCallback = async function handleIDPLoginCallback2(code, context) {
43
+ const OAuthClient = (0, _oauth.getOAuthClient)(context);
44
+ const {
45
+ access_token: accessToken,
46
+ refresh_token: refreshToken
47
+ } = await OAuthClient.generateToken(code);
48
+ context.updateTokens({
49
+ accessToken,
50
+ refreshToken
51
+ });
52
+ return {
53
+ message: "success"
54
+ };
55
+ };
@@ -0,0 +1,7 @@
1
+ import type { RpcContext } from '../../../types';
2
+ export declare const getExternalIdpRedirect: (context: RpcContext) => Promise<{
3
+ [k: string]: string;
4
+ }>;
5
+ export declare const handleIDPLoginCallback: (code: string, context: RpcContext) => Promise<{
6
+ message: string;
7
+ }>;
@@ -0,0 +1,48 @@
1
+ import { SignJWT } from "jose";
2
+ import { getOAuthClient } from "../../../api/oauth.mjs";
3
+ export const getExternalIdpRedirect = async function getExternalIdpRedirect2(context) {
4
+ const shopId = context.shopId.toString();
5
+ const OAuthClient = getOAuthClient(context);
6
+ const checkoutSecret = context.checkout.secret;
7
+ const isIDPEnabled = context.idp.enabled;
8
+ const IDPKeys = context.idp.idpKeys;
9
+ const IDPRedirectURL = context.idp.idpRedirectURL;
10
+ if (!isIDPEnabled) {
11
+ throw new Error("IDP disabled");
12
+ }
13
+ if (!IDPKeys.length) {
14
+ throw new Error("No IDP keys configured");
15
+ }
16
+ if (!IDPRedirectURL) {
17
+ throw new Error("No redirect URL");
18
+ }
19
+ if (!shopId) {
20
+ throw new Error("Missing shopId");
21
+ }
22
+ const secret = new TextEncoder().encode(checkoutSecret);
23
+ const results = await Promise.all(
24
+ IDPKeys.map(async (idpKey) => {
25
+ const jwtPayload = await new SignJWT({
26
+ idpKey,
27
+ callbackUrl: IDPRedirectURL,
28
+ clientId: OAuthClient.clientId.toString()
29
+ }).setProtectedHeader({ alg: "HS256", typ: "JWT" }).setIssuedAt().setExpirationTime("2h").sign(secret);
30
+ return [
31
+ idpKey,
32
+ `${OAuthClient.baseURL}/auth/external/redirect?shopId=${shopId}&jwt=${jwtPayload}`
33
+ ];
34
+ })
35
+ );
36
+ return Object.fromEntries(results);
37
+ };
38
+ export const handleIDPLoginCallback = async function handleIDPLoginCallback2(code, context) {
39
+ const OAuthClient = getOAuthClient(context);
40
+ const { access_token: accessToken, refresh_token: refreshToken } = await OAuthClient.generateToken(code);
41
+ context.updateTokens({
42
+ accessToken,
43
+ refreshToken
44
+ });
45
+ return {
46
+ message: "success"
47
+ };
48
+ };
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.getProductsCount = exports.getProductsByIds = exports.getProductsByCategory = exports.getProductById = exports.getFilters = exports.fetchAllFiltersForCategory = void 0;
7
7
  var _helpers = require("../../helpers/index.cjs");
8
8
  var _constants = require("../../constants/index.cjs");
9
+ var _cache = require("../../cache/index.cjs");
9
10
  const MAX_PER_PAGE = 100;
10
11
  const getProductById = exports.getProductById = async function getProductById2(options, {
11
12
  bapiClient,
@@ -14,7 +15,8 @@ const getProductById = exports.getProductById = async function getProductById2(o
14
15
  withParams
15
16
  }) {
16
17
  return await cached(bapiClient.products.getById, {
17
- cacheKeyPrefix: `getById-product-${options.id}`
18
+ cacheKeyPrefix: `getById-product-${options.id}`,
19
+ ttl: 5 * _cache.MINUTE
18
20
  })(options.id, {
19
21
  with: options.with ?? withParams?.product ?? _constants.MIN_WITH_PARAMS_PRODUCT,
20
22
  campaignKey,
@@ -29,7 +31,8 @@ const getProductsByIds = exports.getProductsByIds = async function getProductsBy
29
31
  withParams
30
32
  }) {
31
33
  return await cached(bapiClient.products.getByIds, {
32
- cacheKeyPrefix: "getByIds-products"
34
+ cacheKeyPrefix: "getByIds-products",
35
+ ttl: 5 * _cache.MINUTE
33
36
  })(options.ids, {
34
37
  with: options.with ?? withParams?.product ?? _constants.MIN_WITH_PARAMS_PRODUCT,
35
38
  campaignKey,
@@ -74,6 +77,7 @@ const getProductsByCategory = exports.getProductsByCategory = async function get
74
77
  pagination
75
78
  } = await cached(bapiClient.products.query, {
76
79
  ...cache,
80
+ ttl: 15 * _cache.MINUTE,
77
81
  cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
78
82
  })({
79
83
  where: {
@@ -155,6 +159,7 @@ const getFilters = exports.getFilters = async function getFilters2({
155
159
  including: includedFilters,
156
160
  orFiltersOperator
157
161
  }), cached(bapiClient.products.query, {
162
+ ttl: 15 * _cache.MINUTE,
158
163
  cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
159
164
  })({
160
165
  where: {
@@ -220,6 +225,7 @@ const getProductsCount = exports.getProductsCount = async function getProductsCo
220
225
  })(sanitizedPath);
221
226
  }
222
227
  const productCount = await cached(bapiClient.products.query, {
228
+ ttl: 15 * _cache.MINUTE,
223
229
  cacheKeyPrefix: "products-query"
224
230
  })({
225
231
  where: {
@@ -1,9 +1,11 @@
1
1
  import { splitAndRemoveEmpty } from "../../helpers/index.mjs";
2
2
  import { MIN_WITH_PARAMS_PRODUCT } from "../../constants/index.mjs";
3
+ import { MINUTE } from "../../cache/index.mjs";
3
4
  const MAX_PER_PAGE = 100;
4
5
  export const getProductById = async function getProductById2(options, { bapiClient, cached, campaignKey, withParams }) {
5
6
  return await cached(bapiClient.products.getById, {
6
- cacheKeyPrefix: `getById-product-${options.id}`
7
+ cacheKeyPrefix: `getById-product-${options.id}`,
8
+ ttl: 5 * MINUTE
7
9
  })(options.id, {
8
10
  with: options.with ?? withParams?.product ?? MIN_WITH_PARAMS_PRODUCT,
9
11
  campaignKey,
@@ -13,7 +15,8 @@ export const getProductById = async function getProductById2(options, { bapiClie
13
15
  };
14
16
  export const getProductsByIds = async function getProductsByIds2(options, { bapiClient, cached, campaignKey, withParams }) {
15
17
  return await cached(bapiClient.products.getByIds, {
16
- cacheKeyPrefix: "getByIds-products"
18
+ cacheKeyPrefix: "getByIds-products",
19
+ ttl: 5 * MINUTE
17
20
  })(options.ids, {
18
21
  with: options.with ?? withParams?.product ?? MIN_WITH_PARAMS_PRODUCT,
19
22
  campaignKey,
@@ -49,6 +52,7 @@ export const getProductsByCategory = async function getProductsByCategory2({
49
52
  bapiClient.products.query,
50
53
  {
51
54
  ...cache,
55
+ ttl: 15 * MINUTE,
52
56
  cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
53
57
  }
54
58
  )({
@@ -128,6 +132,7 @@ export const getFilters = async function getFilters2({
128
132
  orFiltersOperator
129
133
  }),
130
134
  cached(bapiClient.products.query, {
135
+ ttl: 15 * MINUTE,
131
136
  cacheKeyPrefix: `products-query-category-${category === "/" ? "root" : category}`
132
137
  })({
133
138
  where: {
@@ -184,6 +189,7 @@ export const getProductsCount = async function getProductsCount2({
184
189
  })(sanitizedPath);
185
190
  }
186
191
  const productCount = await cached(bapiClient.products.query, {
192
+ ttl: 15 * MINUTE,
187
193
  cacheKeyPrefix: "products-query"
188
194
  })({
189
195
  where: {
@@ -69,4 +69,14 @@ export interface Oauth {
69
69
  access_token: string;
70
70
  refresh_token: string;
71
71
  }
72
+ /**
73
+ * This adds `string` so it's flexible enough to support
74
+ * multiple IDPs since we don't know what they are.
75
+ * More narrow type can be defined in the nuxt modules.
76
+ */
77
+ export interface IDPConfig {
78
+ enabled: boolean;
79
+ idpKeys: string[];
80
+ idpRedirectURL: string;
81
+ }
72
82
  export {};
@@ -3,7 +3,7 @@ import { Log } from '../../utils';
3
3
  import { CachedType } from '../../cache/cached';
4
4
  import { ShopUser } from '../user';
5
5
  import type { WishlistWithOptions, BasketWithOptions, ProductWith, VariantWith, SearchWith, CategoryWith, ProductCategoryWith } from '../';
6
- import { OAuthTokens } from './auth';
6
+ import { OAuthTokens, IDPConfig } from './auth';
7
7
  export type WithParams = Partial<{
8
8
  basket: BasketWithOptions;
9
9
  wishlist: WishlistWithOptions;
@@ -62,4 +62,5 @@ export interface RpcContext {
62
62
  clientSecret: string;
63
63
  };
64
64
  runtimeConfiguration: RuntimeConfiguration;
65
+ idp?: IDPConfig;
65
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/storefront-core",
3
- "version": "7.32.0",
3
+ "version": "7.34.0",
4
4
  "description": "Collection of essential utilities to work with the Storefront API",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -40,6 +40,7 @@
40
40
  "dist",
41
41
  "utils"
42
42
  ],
43
+ "type": "module",
43
44
  "engines": {
44
45
  "node": ">= 18.15.0"
45
46
  },
@@ -52,8 +53,9 @@
52
53
  "lint": "eslint . --format gitlab",
53
54
  "lint:fix": "eslint . --fix",
54
55
  "package:lint": "publint",
55
- "test": "jest --passWithNoTests --maxWorkers=50%",
56
- "test:ci": "jest --passWithNoTests --runInBand --coverage --reporters=default --reporters=jest-junit"
56
+ "test:watch": "vitest --passWithNoTests",
57
+ "test": "vitest --run --passWithNoTests",
58
+ "test:ci": "vitest --run --passWithNoTests --coverage --reporter=default --reporter=junit"
57
59
  },
58
60
  "dependencies": {
59
61
  "@aboutyou/backbone": "16.0.1",
@@ -69,21 +71,19 @@
69
71
  "@scayle/eslint-config-storefront": "3.2.6",
70
72
  "@scayle/prettier-config-storefront": "2.0.2",
71
73
  "@types/crypto-js": "4.2.1",
72
- "@types/jest": "29.5.11",
73
74
  "@types/node": "20.10.5",
74
75
  "@types/webpack-env": "1.18.4",
75
- "unbuild": "2.0.0",
76
+ "@vitest/coverage-v8": "1.1.0",
76
77
  "eslint": "8.56.0",
77
78
  "eslint-formatter-gitlab": "5.1.0",
78
- "jest": "29.7.0",
79
- "jest-junit": "16.0.0",
80
79
  "prettier": "3.0.0",
81
80
  "publint": "0.2.6",
82
81
  "rimraf": "5.0.5",
83
- "ts-jest": "29.1.1",
84
82
  "ts-node": "10.9.2",
85
83
  "typescript": "5.3.3",
86
- "unstorage": "1.10.1"
84
+ "unbuild": "2.0.0",
85
+ "unstorage": "1.10.1",
86
+ "vitest": "1.1.0"
87
87
  },
88
88
  "optionalDependencies": {
89
89
  "redis": "4"