@swell/apps-sdk 1.0.151 → 1.0.153

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/dist/index.js CHANGED
@@ -41,72 +41,6 @@
41
41
  var import_keyv = __require("keyv");
42
42
  var import_cache_manager = __require("cache-manager");
43
43
 
44
- // src/cache/cf-worker-kv-keyv-adapter.ts
45
- var CFWorkerKVKeyvAdapter = class {
46
- store;
47
- namespace;
48
- // magically passed in from Keyv
49
- opts;
50
- constructor(store) {
51
- this.store = store;
52
- this.opts = null;
53
- this.namespace = "dummy";
54
- }
55
- async has(key) {
56
- const stream = await this.store.get(key, "stream");
57
- if (stream !== null) {
58
- await stream.cancel();
59
- return true;
60
- }
61
- return false;
62
- }
63
- async get(key) {
64
- const value = await this.store.get(key);
65
- return value !== null ? value : void 0;
66
- }
67
- set(key, value, ttl) {
68
- if (typeof ttl === "number") {
69
- ttl = Math.max(60, ttl / 1e3);
70
- }
71
- return this.store.put(key, value, { expirationTtl: ttl });
72
- }
73
- async delete(key) {
74
- await this.store.delete(key);
75
- return true;
76
- }
77
- async clear() {
78
- let cursor = "";
79
- let complete = false;
80
- const prefix = `${this.namespace}:`;
81
- do {
82
- const response = await this.store.list({
83
- prefix,
84
- cursor: cursor || void 0
85
- });
86
- cursor = response.cursor ?? "";
87
- complete = response.list_complete;
88
- if (response.keys.length > 0) {
89
- await Promise.all(
90
- response.keys.map((key) => {
91
- return this.store.delete(key.name);
92
- })
93
- );
94
- }
95
- } while (!complete);
96
- }
97
- on(_event, _listener) {
98
- return this;
99
- }
100
- };
101
-
102
- // src/utils/index.ts
103
- var import_qs = __toESM(__require("qs"), 1);
104
- var import_lodash_es3 = __require("lodash-es");
105
- var import_json52 = __toESM(__require("json5"), 1);
106
-
107
- // src/resources.ts
108
- var import_lodash_es2 = __require("lodash-es");
109
-
110
44
  // src/utils/logger.ts
111
45
  var logLevels = {
112
46
  error: 0,
@@ -198,6 +132,80 @@
198
132
  return (hash >>> 0).toString(16);
199
133
  }
200
134
 
135
+ // src/cache/cf-worker-kv-keyv-adapter.ts
136
+ var CFWorkerKVKeyvAdapter = class {
137
+ store;
138
+ namespace;
139
+ // magically passed in from Keyv
140
+ opts;
141
+ constructor(store) {
142
+ this.store = store;
143
+ this.opts = null;
144
+ this.namespace = "dummy";
145
+ }
146
+ async has(key) {
147
+ const stream = await this.store.get(key, "stream");
148
+ if (stream !== null) {
149
+ await stream.cancel();
150
+ return true;
151
+ }
152
+ return false;
153
+ }
154
+ async get(key) {
155
+ const trace = createTraceId();
156
+ logger.debug("[SDK] kv.get", { key, trace });
157
+ const value = await this.store.get(key);
158
+ const result = value !== null ? value : void 0;
159
+ logger.debug(`[SDK] kv.get ${value !== null ? "HIT" : "MISS"}`, {
160
+ key,
161
+ trace
162
+ });
163
+ return result;
164
+ }
165
+ set(key, value, ttl) {
166
+ if (typeof ttl === "number") {
167
+ ttl = Math.max(60, ttl / 1e3);
168
+ }
169
+ logger.debug("[SDK] kv.set", { key });
170
+ return this.store.put(key, value, { expirationTtl: ttl });
171
+ }
172
+ async delete(key) {
173
+ await this.store.delete(key);
174
+ return true;
175
+ }
176
+ async clear() {
177
+ let cursor = "";
178
+ let complete = false;
179
+ const prefix = `${this.namespace}:`;
180
+ do {
181
+ const response = await this.store.list({
182
+ prefix,
183
+ cursor: cursor || void 0
184
+ });
185
+ cursor = response.cursor ?? "";
186
+ complete = response.list_complete;
187
+ if (response.keys.length > 0) {
188
+ await Promise.all(
189
+ response.keys.map((key) => {
190
+ return this.store.delete(key.name);
191
+ })
192
+ );
193
+ }
194
+ } while (!complete);
195
+ }
196
+ on(_event, _listener) {
197
+ return this;
198
+ }
199
+ };
200
+
201
+ // src/utils/index.ts
202
+ var import_qs = __toESM(__require("qs"), 1);
203
+ var import_lodash_es3 = __require("lodash-es");
204
+ var import_json52 = __toESM(__require("json5"), 1);
205
+
206
+ // src/resources.ts
207
+ var import_lodash_es2 = __require("lodash-es");
208
+
201
209
  // src/liquid/utils.ts
202
210
  var import_liquidjs = __require("liquidjs");
203
211
 
@@ -841,6 +849,7 @@
841
849
  this._collection,
842
850
  this._query,
843
851
  this._swell.queryParams,
852
+ // TODO: consider to exlcude
844
853
  this._getterHash
845
854
  ],
846
855
  getter,
@@ -969,6 +978,7 @@
969
978
  this._id,
970
979
  this._query,
971
980
  this._swell.queryParams,
981
+ // TODO: consider to exlcude
972
982
  this._getterHash
973
983
  ],
974
984
  getter,
@@ -7197,8 +7207,6 @@
7197
7207
  * This will always return the cached value immediately if exists
7198
7208
  */
7199
7209
  async fetchSWR(key, fetchFn, ttl = DEFAULT_SWR_TTL, isCacheble = true) {
7200
- const trace = createTraceId();
7201
- logger.debug("[SDK] Cache fetch start", { key, trace });
7202
7210
  const cacheValue = isCacheble ? await this.client.get(key) : void 0;
7203
7211
  let promise = SWR_PROMISE_MAP.get(key);
7204
7212
  if (promise === void 0) {
@@ -7206,7 +7214,6 @@
7206
7214
  const isNull = value === null || value === void 0;
7207
7215
  if (isCacheble) {
7208
7216
  await this.client.set(key, isNull ? NULL_VALUE : value, ttl);
7209
- logger.debug("[SDK] Cache update done", { key, trace });
7210
7217
  }
7211
7218
  return value;
7212
7219
  }).finally(() => {
@@ -7218,12 +7225,9 @@
7218
7225
  this.workerCtx.waitUntil(promise);
7219
7226
  }
7220
7227
  if (cacheValue !== void 0) {
7221
- logger.debug("[SDK] Cache check done", { status: "HIT", key, trace });
7222
7228
  return cacheValue === NULL_VALUE ? null : cacheValue;
7223
7229
  }
7224
- logger.debug("[SDK] Cache check done", { status: "MISS", key, trace });
7225
7230
  const result = await promise;
7226
- logger.debug("[SDK] Cache fetch end", { key, trace });
7227
7231
  return result;
7228
7232
  }
7229
7233
  async get(key) {
@@ -7730,6 +7734,384 @@
7730
7734
  }
7731
7735
  };
7732
7736
 
7737
+ // src/cache/worker-html-cache.ts
7738
+ var CACHE_NAME2 = "swell-html-v1";
7739
+ var CACHE_KEY_ORIGIN2 = "https://cache.swell.store";
7740
+ var TTL_CONFIG = {
7741
+ LIVE: {
7742
+ DEFAULT: 300,
7743
+ // 5 minutes
7744
+ HOME: 300,
7745
+ // 5 minutes
7746
+ PRODUCT: 600,
7747
+ // 10 minutes
7748
+ COLLECTION: 900,
7749
+ // 15 minutes
7750
+ PAGE: 3600,
7751
+ // 1 hour
7752
+ BLOG: 1800,
7753
+ // 30 minutes
7754
+ SWR: 3600
7755
+ // 1 hour stale-while-revalidate
7756
+ },
7757
+ PREVIEW: {
7758
+ DEFAULT: 5,
7759
+ // 1 minute - faster updates in preview
7760
+ HOME: 5,
7761
+ // 1 minute
7762
+ PRODUCT: 5,
7763
+ // 2 minutes
7764
+ COLLECTION: 5,
7765
+ // 3 minutes
7766
+ PAGE: 5,
7767
+ // 5 minutes
7768
+ BLOG: 5,
7769
+ // 5 minutes
7770
+ SWR: 600
7771
+ // 10 minutes stale-while-revalidate
7772
+ }
7773
+ };
7774
+ var WorkerHtmlCache = class {
7775
+ epoch;
7776
+ constructor(epoch) {
7777
+ this.epoch = epoch;
7778
+ }
7779
+ async get(request) {
7780
+ const trace = createTraceId();
7781
+ if (request.method !== "GET") {
7782
+ logger.debug("[SDK Html-cache] non-cacheable", { trace });
7783
+ return { found: false, cacheable: false };
7784
+ }
7785
+ if (!this.isCacheable(request)) {
7786
+ logger.debug("[SDK Html-cache] non-cacheable", { trace });
7787
+ return { found: false, cacheable: false };
7788
+ }
7789
+ try {
7790
+ const cache = await caches.open(CACHE_NAME2 + this.epoch);
7791
+ const cacheKey = this.buildCacheKey(request);
7792
+ const cached = await cache.match(cacheKey);
7793
+ if (!cached) {
7794
+ logger.debug("[SDK Html-cache] cacheable, MISS", { trace });
7795
+ return { found: false, cacheable: true };
7796
+ }
7797
+ const age = this.getResponseAge(cached);
7798
+ const ttl = parseInt(cached.headers.get("X-Original-TTL") || "") || this.getTTLForRequest(request);
7799
+ const swr = parseInt(cached.headers.get("X-Original-SWR") || "") || this.getSWRForRequest(request);
7800
+ const isStale = age >= ttl;
7801
+ const isExpired = age >= ttl + swr;
7802
+ if (!isExpired) {
7803
+ logger.debug("[SDK Html-cache] cacheable, HIT", {
7804
+ stale: isStale,
7805
+ age,
7806
+ trace
7807
+ });
7808
+ const clientResponse = this.buildClientResponse(
7809
+ cached,
7810
+ ttl,
7811
+ swr,
7812
+ isStale,
7813
+ age
7814
+ );
7815
+ return {
7816
+ found: true,
7817
+ stale: isStale,
7818
+ response: clientResponse,
7819
+ cacheable: true,
7820
+ age: Math.floor(age)
7821
+ };
7822
+ }
7823
+ logger.debug("[SDK Html-cache] cacheable, hit, expired", { trace });
7824
+ return { found: false, cacheable: true };
7825
+ } catch (_) {
7826
+ logger.warn("[SDK Html-cache] no get support", { trace });
7827
+ return null;
7828
+ }
7829
+ }
7830
+ // 304 support
7831
+ async getWithConditionals(request) {
7832
+ const result = await this.get(request);
7833
+ if (!result?.found || result.stale) {
7834
+ return result;
7835
+ }
7836
+ const ifModifiedSince = request.headers.get("If-Modified-Since");
7837
+ const ifNoneMatch = request.headers.get("If-None-Match");
7838
+ if ((ifModifiedSince || ifNoneMatch) && result.response) {
7839
+ const lastModified = result.response.headers.get("Last-Modified");
7840
+ const etag = result.response.headers.get("ETag");
7841
+ if (this.checkNotModified(ifModifiedSince, ifNoneMatch, lastModified, etag)) {
7842
+ result.notModified = true;
7843
+ result.conditional304 = new Response(null, {
7844
+ status: 304,
7845
+ headers: {
7846
+ "Last-Modified": lastModified || "",
7847
+ ETag: etag || "",
7848
+ "Cache-Control": result.response.headers.get("Cache-Control") || "",
7849
+ "Cloudflare-CDN-Cache-Control": result.response.headers.get("Cloudflare-CDN-Cache-Control") || "",
7850
+ "X-Cache-Status": "HIT-304"
7851
+ }
7852
+ });
7853
+ }
7854
+ }
7855
+ return result;
7856
+ }
7857
+ checkNotModified(ifModifiedSince, ifNoneMatch, lastModified, etag) {
7858
+ if (ifNoneMatch && etag) {
7859
+ return ifNoneMatch === etag;
7860
+ }
7861
+ if (ifModifiedSince && lastModified) {
7862
+ const ifModDate = new Date(ifModifiedSince);
7863
+ const lastModDate = new Date(lastModified);
7864
+ return !isNaN(ifModDate.getTime()) && !isNaN(lastModDate.getTime()) && ifModDate >= lastModDate;
7865
+ }
7866
+ return false;
7867
+ }
7868
+ createRevalidationRequest(request) {
7869
+ const headers = new Headers(request.headers);
7870
+ headers.set("X-Cache-Bypass", "revalidation");
7871
+ headers.delete("If-None-Match");
7872
+ headers.delete("If-Modified-Since");
7873
+ headers.delete("Cache-Control");
7874
+ headers.delete("Pragma");
7875
+ return new Request(request.url, {
7876
+ method: "GET",
7877
+ headers
7878
+ });
7879
+ }
7880
+ async put(request, response) {
7881
+ const trace = createTraceId();
7882
+ if (request.method !== "GET" || !response.ok) {
7883
+ logger.debug("[SDK Html-cache] put skipped", { trace });
7884
+ return;
7885
+ }
7886
+ if (!this.isCacheable(request) || !this.isResponseCacheable(response)) {
7887
+ logger.debug("[SDK Html-cache] put skipped, non-cacheable", {
7888
+ trace
7889
+ });
7890
+ return;
7891
+ }
7892
+ try {
7893
+ const cache = await caches.open(CACHE_NAME2 + this.epoch);
7894
+ const cacheKey = this.buildCacheKey(request);
7895
+ await cache.delete(cacheKey);
7896
+ const ttl = this.getTTLForRequest(request);
7897
+ const swr = this.getSWRForRequest(request);
7898
+ const headers = new Headers(response.headers);
7899
+ const existingCacheControl = response.headers.get("Cache-Control");
7900
+ if (!existingCacheControl || existingCacheControl === "public") {
7901
+ const internalMaxAge = ttl + swr;
7902
+ headers.set("Cache-Control", `public, max-age=${internalMaxAge}`);
7903
+ }
7904
+ const cacheTime = (/* @__PURE__ */ new Date()).toISOString();
7905
+ headers.set("X-Cache-Time", cacheTime);
7906
+ headers.set("X-Original-TTL", ttl.toString());
7907
+ headers.set("X-Original-SWR", swr.toString());
7908
+ if (!headers.get("Last-Modified")) {
7909
+ headers.set("Last-Modified", new Date(cacheTime).toUTCString());
7910
+ }
7911
+ const cacheableResponse = new Response(response.body, {
7912
+ status: response.status,
7913
+ statusText: response.statusText,
7914
+ headers
7915
+ });
7916
+ await cache.put(cacheKey, cacheableResponse);
7917
+ logger.debug("[SDK Html-cache] put done", { trace });
7918
+ } catch (_) {
7919
+ logger.warn("[SDK Html-cache] no put support", { trace });
7920
+ }
7921
+ }
7922
+ buildClientResponse(cachedResponse, ttl, swr, isStale, age) {
7923
+ const headers = new Headers(cachedResponse.headers);
7924
+ headers.set(
7925
+ "Cache-Control",
7926
+ `public, max-age=${ttl}, stale-while-revalidate=${swr}`
7927
+ );
7928
+ headers.set(
7929
+ "Cloudflare-CDN-Cache-Control",
7930
+ `public, s-maxage=${ttl}, stale-while-revalidate=${swr}, stale-if-error=60`
7931
+ );
7932
+ const cacheTime = headers.get("X-Cache-Time");
7933
+ if (cacheTime) {
7934
+ const lastModified = new Date(cacheTime).toUTCString();
7935
+ headers.set("Last-Modified", lastModified);
7936
+ }
7937
+ headers.set("X-Cache-Status", isStale ? "STALE" : "HIT");
7938
+ headers.set("X-Cache-Age", Math.floor(age).toString());
7939
+ headers.delete("X-Original-TTL");
7940
+ headers.delete("X-Original-SWR");
7941
+ headers.delete("X-Cache-Time");
7942
+ return new Response(cachedResponse.body, {
7943
+ status: cachedResponse.status,
7944
+ statusText: cachedResponse.statusText,
7945
+ headers
7946
+ });
7947
+ }
7948
+ buildCacheKey(request) {
7949
+ const url = new URL(request.url);
7950
+ const versionHash = this.generateVersionHash(request.headers);
7951
+ const normalizedQuery = this.normalizeSearchParams(url.searchParams);
7952
+ const cacheKeyPath = `${versionHash}${url.pathname}`;
7953
+ const keyUrl = new URL(`${CACHE_KEY_ORIGIN2}${cacheKeyPath}`);
7954
+ if (normalizedQuery) {
7955
+ keyUrl.search = `?${normalizedQuery}`;
7956
+ }
7957
+ const sanitizedHeaders = this.sanitizeHeaders(request.headers);
7958
+ return new Request(keyUrl.toString(), {
7959
+ method: "GET",
7960
+ headers: sanitizedHeaders
7961
+ });
7962
+ }
7963
+ sanitizeHeaders(originalHeaders) {
7964
+ const CACHE_RELEVANT_HEADERS = [
7965
+ // Content negotiation (affects response format)
7966
+ "accept",
7967
+ "accept-language"
7968
+ ];
7969
+ const sanitized = new Headers();
7970
+ CACHE_RELEVANT_HEADERS.forEach((header) => {
7971
+ const value = originalHeaders.get(header);
7972
+ if (value) {
7973
+ sanitized.set(header, value);
7974
+ }
7975
+ });
7976
+ return sanitized;
7977
+ }
7978
+ generateVersionHash(headers) {
7979
+ const swellData = this.extractSwellData(headers);
7980
+ const versionFactors = {
7981
+ store: headers.get("swell-storefront-id") || "",
7982
+ auth: headers.get("swell-access-token") || "",
7983
+ theme: headers.get("swell-theme-version-hash") || "",
7984
+ modified: headers.get("swell-cache-modified") || "",
7985
+ currency: swellData["swell-currency"] || "USD",
7986
+ locale: headers.get("x-locale") || headers.get("accept-language")?.split(",")[0] || "default",
7987
+ context: headers.get("swell-storefront-context"),
7988
+ epoch: this.epoch
7989
+ };
7990
+ return md5(JSON.stringify(versionFactors));
7991
+ }
7992
+ extractSwellData(headers) {
7993
+ const cookie = headers.get("cookie");
7994
+ if (!cookie) return {};
7995
+ const swellDataMatch = cookie.match(/swell-data=([^;]+)/);
7996
+ if (!swellDataMatch) return {};
7997
+ try {
7998
+ const parsed = JSON.parse(decodeURIComponent(swellDataMatch[1]));
7999
+ if (typeof parsed === "object" && parsed !== null) {
8000
+ return parsed;
8001
+ }
8002
+ return {};
8003
+ } catch {
8004
+ return {};
8005
+ }
8006
+ }
8007
+ isCacheable(request) {
8008
+ const url = new URL(request.url);
8009
+ const headers = request.headers;
8010
+ if (headers.get("swell-deployment-mode") === "editor") {
8011
+ return false;
8012
+ }
8013
+ const skipPaths = ["/checkout"];
8014
+ if (skipPaths.some((path) => url.pathname.startsWith(path))) {
8015
+ return false;
8016
+ }
8017
+ if (headers.get("cache-control")?.includes("no-cache")) {
8018
+ return false;
8019
+ }
8020
+ return true;
8021
+ }
8022
+ isResponseCacheable(response) {
8023
+ const contentType = response.headers.get("content-type");
8024
+ if (!contentType?.includes("text/html")) {
8025
+ return false;
8026
+ }
8027
+ if (response.headers.get("set-cookie")) {
8028
+ return false;
8029
+ }
8030
+ const cacheControl = response.headers.get("cache-control");
8031
+ if (cacheControl?.includes("no-store") || cacheControl?.includes("private")) {
8032
+ return false;
8033
+ }
8034
+ return true;
8035
+ }
8036
+ getDeploymentMode(headers) {
8037
+ const mode = headers.get("swell-deployment-mode");
8038
+ if (mode === "preview" || mode === "editor") {
8039
+ return mode;
8040
+ }
8041
+ return "live";
8042
+ }
8043
+ getTTLForRequest(request) {
8044
+ const url = new URL(request.url);
8045
+ const path = url.pathname;
8046
+ const mode = this.getDeploymentMode(request.headers);
8047
+ if (mode === "editor") {
8048
+ return 0;
8049
+ }
8050
+ const config = mode === "preview" ? TTL_CONFIG.PREVIEW : TTL_CONFIG.LIVE;
8051
+ if (path === "/") {
8052
+ return config.HOME;
8053
+ }
8054
+ if (path.startsWith("/products/")) {
8055
+ return config.PRODUCT;
8056
+ }
8057
+ if (path.startsWith("/categories/")) {
8058
+ return config.COLLECTION;
8059
+ }
8060
+ if (path.startsWith("/pages/")) {
8061
+ return config.PAGE;
8062
+ }
8063
+ if (path.startsWith("/blogs/")) {
8064
+ return config.BLOG;
8065
+ }
8066
+ return config.DEFAULT;
8067
+ }
8068
+ getSWRForRequest(request) {
8069
+ const mode = this.getDeploymentMode(request.headers);
8070
+ if (mode === "editor") {
8071
+ return 0;
8072
+ }
8073
+ return mode === "preview" ? TTL_CONFIG.PREVIEW.SWR : TTL_CONFIG.LIVE.SWR;
8074
+ }
8075
+ getResponseAge(response) {
8076
+ const cacheTime = response.headers.get("X-Cache-Time");
8077
+ if (!cacheTime) {
8078
+ return Infinity;
8079
+ }
8080
+ const cacheDate = new Date(cacheTime);
8081
+ if (isNaN(cacheDate.getTime())) {
8082
+ return Infinity;
8083
+ }
8084
+ const age = (Date.now() - cacheDate.getTime()) / 1e3;
8085
+ return Math.max(0, age);
8086
+ }
8087
+ normalizeSearchParams(searchParams) {
8088
+ const ignoredParams = [
8089
+ "utm_source",
8090
+ "utm_medium",
8091
+ "utm_campaign",
8092
+ "utm_content",
8093
+ "utm_term",
8094
+ "fbclid",
8095
+ "gclid",
8096
+ "gbraid",
8097
+ "wbraid",
8098
+ "ref",
8099
+ "source",
8100
+ "mc_cid",
8101
+ "mc_eid"
8102
+ ];
8103
+ const relevantParams = [];
8104
+ searchParams.forEach((value, key) => {
8105
+ if (!ignoredParams.includes(key)) {
8106
+ relevantParams.push(
8107
+ `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
8108
+ );
8109
+ }
8110
+ });
8111
+ return relevantParams.sort().join("&");
8112
+ }
8113
+ };
8114
+
7733
8115
  // src/resources/addresses.ts
7734
8116
  var SwellAddresses = class extends SwellStorefrontCollection {
7735
8117
  constructor(swell, query) {
@@ -8557,13 +8939,19 @@
8557
8939
  const storefrontRequest = storefront.request;
8558
8940
  return (method, url, id, data, opt) => {
8559
8941
  if (this.isStorefrontRequestCacheable(method, url, opt)) {
8560
- return this.getRequestCache().fetchSWR(
8561
- getCacheKey("request", [this.instanceId, method, url, id, data, opt]),
8562
- () => {
8563
- logger.info("[SDK] Storefront request", { method, url, id, data });
8564
- return storefrontRequest(method, url, id, data, opt);
8565
- }
8566
- );
8942
+ const key = getCacheKey("request", [
8943
+ this.instanceId,
8944
+ method,
8945
+ url,
8946
+ id,
8947
+ data,
8948
+ opt
8949
+ ]);
8950
+ return this.getRequestCache().fetchSWR(key, () => {
8951
+ const requestUrl = id ? `${url}/${id}` : url;
8952
+ logger.debug("[SDK] Cacheable API request", { url: requestUrl, key });
8953
+ return storefrontRequest(method, url, id, data, opt);
8954
+ });
8567
8955
  }
8568
8956
  switch (method) {
8569
8957
  case "delete":
@@ -8610,7 +8998,9 @@
8610
8998
  };
8611
8999
  function getCacheKey(key, args) {
8612
9000
  if (Array.isArray(args) && args.length > 0) {
8613
- return `${key}_${md5(JSON.stringify(args))}`;
9001
+ const fullKey = `${key}_${md5(JSON.stringify(args))}`;
9002
+ logger.debug(`[SDK] make cache key: ${fullKey}`);
9003
+ return fullKey;
8614
9004
  }
8615
9005
  return key;
8616
9006
  }