Skip to content

Commit 96df9d9

Browse files
WIP
1 parent 03d956d commit 96df9d9

File tree

2 files changed

+67
-50
lines changed

2 files changed

+67
-50
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
2323
import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./StartupOptions.js";
2424
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js";
2525
import { Disposable } from "./common/disposable.js";
26-
import { base64Helper, jsonSorter } from "./common/utils.js";
26+
import { base64Helper, jsonSorter, getCryptoModule } from "./common/utils.js";
2727
import {
2828
FEATURE_FLAGS_KEY_NAME,
2929
FEATURE_MANAGEMENT_KEY_NAME,
@@ -77,9 +77,10 @@ type SettingSelectorCollection = {
7777

7878
/**
7979
* This is used to append to the request url for breaking the CDN cache.
80-
* It uses the etag which has changed after the last refresh. It can either be a page etag or etag of a watched setting.
80+
* It is a hash value calculated from all page etags.
81+
* When the refresh is based on watched settings, the hash value will be calculated from the etags of all watched settings.
8182
*/
82-
cdnCacheBreakString?: string;
83+
version?: string;
8384
}
8485

8586
export class AzureAppConfigurationImpl implements AzureAppConfiguration {
@@ -418,21 +419,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
418419
// use the first page etag of the first kv selector
419420
const defaultSelector = this.#kvSelectorCollection.selectors.find(s => s.pageEtags !== undefined);
420421
if (defaultSelector && defaultSelector.pageEtags!.length > 0) {
421-
this.#kvSelectorCollection.cdnCacheBreakString = defaultSelector.pageEtags![0];
422+
this.#kvSelectorCollection.version = defaultSelector.pageEtags![0];
422423
} else {
423-
this.#kvSelectorCollection.cdnCacheBreakString = undefined;
424+
this.#kvSelectorCollection.version = undefined;
424425
}
425426
} else if (this.#refreshEnabled) { // watched settings based refresh
426427
// use the etag of the first watched setting (sentinel)
427-
this.#kvSelectorCollection.cdnCacheBreakString = this.#sentinels.find(s => s.etag !== undefined)?.etag;
428+
this.#kvSelectorCollection.version = this.#sentinels.find(s => s.etag !== undefined)?.etag;
428429
}
429430

430431
if (this.#featureFlagRefreshEnabled) {
431432
const defaultSelector = this.#ffSelectorCollection.selectors.find(s => s.pageEtags !== undefined);
432433
if (defaultSelector && defaultSelector.pageEtags!.length > 0) {
433-
this.#ffSelectorCollection.cdnCacheBreakString = defaultSelector.pageEtags![0];
434+
this.#ffSelectorCollection.version = defaultSelector.pageEtags![0];
434435
} else {
435-
this.#ffSelectorCollection.cdnCacheBreakString = undefined;
436+
this.#ffSelectorCollection.version = undefined;
436437
}
437438
}
438439
}
@@ -531,7 +532,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
531532
if (this.#isCdnUsed) {
532533
listOptions = {
533534
...listOptions,
534-
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.cdnCacheBreakString ?? "" }}
535+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.version ?? "" }}
535536
};
536537
}
537538

@@ -641,7 +642,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
641642
// If CDN is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
642643
let getOptions: GetConfigurationSettingOptions = {};
643644
if (this.#isCdnUsed) {
644-
getOptions = { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#kvSelectorCollection.cdnCacheBreakString ?? "" } } };
645+
getOptions = { requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#kvSelectorCollection.version ?? "" } } };
645646
}
646647
const response = await this.#getConfigurationSetting(sentinel, getOptions);
647648
sentinel.etag = response?.etag;
@@ -703,7 +704,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
703704
if (this.#isCdnUsed) {
704705
// If CDN is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
705706
getOptions = {
706-
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#kvSelectorCollection.cdnCacheBreakString ?? "" } },
707+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: this.#kvSelectorCollection.version ?? "" } },
707708
};
708709
} else {
709710
// if CDN is not used, send conditional request
@@ -717,7 +718,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
717718
(response === undefined && sentinel.etag !== undefined) // deleted
718719
) {
719720
sentinel.etag = response?.etag;// update etag of the sentinel
720-
this.#kvSelectorCollection.cdnCacheBreakString = sentinel.etag;
721+
this.#kvSelectorCollection.version = sentinel.etag;
721722
needRefresh = true;
722723
break;
723724
}
@@ -770,7 +771,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
770771
// If CDN is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL
771772
listOptions = {
772773
...listOptions,
773-
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.cdnCacheBreakString ?? "" } }
774+
requestOptions: { customHeaders: { [ETAG_LOOKUP_HEADER]: selectorCollection.version ?? "" } }
774775
};
775776
} else {
776777
// if CDN is not used, add page etags to the listOptions to send conditional request
@@ -787,7 +788,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
787788
).byPage();
788789

789790
if (selector.pageEtags === undefined || selector.pageEtags.length === 0) {
790-
selectorCollection.cdnCacheBreakString = undefined;
791+
selectorCollection.version = undefined;
791792
return true; // no etag is retrieved from previous request, always refresh
792793
}
793794

@@ -796,15 +797,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
796797
if (i >= selector.pageEtags.length || // new page
797798
(page._response.status === 200 && page.etag !== selector.pageEtags[i])) { // page changed
798799
if (this.#isCdnUsed) {
799-
selectorCollection.cdnCacheBreakString = page.etag;
800+
selectorCollection.version = page.etag;
800801
}
801802
return true;
802803
}
803804
i++;
804805
}
805806
if (i !== selector.pageEtags.length) { // page removed
806807
if (this.#isCdnUsed) {
807-
selectorCollection.cdnCacheBreakString = selector.pageEtags[i];
808+
selectorCollection.version = selector.pageEtags[i];
808809
}
809810
return true;
810811
}
@@ -1070,57 +1071,50 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
10701071
}
10711072
}
10721073

1073-
let crypto;
1074-
1075-
// Check for browser environment
1076-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
1077-
crypto = window.crypto;
1078-
}
1079-
// Check for Node.js environment
1080-
else if (typeof global !== "undefined" && global.crypto) {
1081-
crypto = global.crypto;
1082-
}
1083-
// Fallback to native Node.js crypto module
1084-
else {
1085-
try {
1086-
if (typeof module !== "undefined" && module.exports) {
1087-
crypto = require("crypto");
1088-
}
1089-
else {
1090-
crypto = await import("crypto");
1091-
}
1092-
} catch (error) {
1093-
console.error("Failed to load the crypto module:", error.message);
1094-
throw error;
1095-
}
1096-
}
1097-
1074+
const crypto = getCryptoModule();
10981075
// Convert to UTF-8 encoded bytes
1099-
const data = new TextEncoder().encode(rawAllocationId);
1100-
1101-
// In the browser, use crypto.subtle.digest
1076+
const payload = new TextEncoder().encode(rawAllocationId);
1077+
// In the browser or Node.js 18+, use crypto.subtle.digest
11021078
if (crypto.subtle) {
1103-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1079+
const hashBuffer = await crypto.subtle.digest("SHA-256", payload);
11041080
const hashArray = new Uint8Array(hashBuffer);
11051081

11061082
// Only use the first 15 bytes
11071083
const first15Bytes = hashArray.slice(0, 15);
1108-
1109-
// btoa/atob is also available in Node.js 18+
11101084
const base64String = btoa(String.fromCharCode(...first15Bytes));
11111085
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
11121086
return base64urlString;
11131087
}
1114-
// In Node.js, use the crypto module's hash function
1088+
// Use the crypto module's hash function
11151089
else {
1116-
const hash = crypto.createHash("sha256").update(data).digest();
1090+
const hash = crypto.createHash("sha256").update(payload).digest();
11171091

11181092
// Only use the first 15 bytes
11191093
const first15Bytes = hash.slice(0, 15);
1120-
11211094
return first15Bytes.toString("base64url");
11221095
}
11231096
}
1097+
1098+
async #calculteCacheConsistencyToken(etags: string[]): Promise<string> {
1099+
const crypto = getCryptoModule();
1100+
const sortedEtags = etags.sort();
1101+
const rawString = "CacheConsistency\n" + sortedEtags.join("\n");
1102+
// Convert to UTF-8 encoded bytes
1103+
const payload = new TextEncoder().encode(rawString);
1104+
// In the browser or Node.js 18+, use crypto.subtle.digest
1105+
if (crypto.subtle) {
1106+
const hashBuffer = await crypto.subtle.digest("SHA-256", payload);
1107+
const hashArray = new Uint8Array(hashBuffer);
1108+
const base64String = btoa(String.fromCharCode(...hashArray));
1109+
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1110+
return base64urlString;
1111+
}
1112+
// Use the crypto module's hash function
1113+
else {
1114+
const hash = crypto.createHash("sha256").update(payload).digest();
1115+
return hash.toString("base64url");
1116+
}
1117+
}
11241118
}
11251119

11261120
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {

src/common/utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4+
export function getCryptoModule(): any {
5+
let crypto;
6+
7+
// Check for browser environment
8+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
9+
crypto = window.crypto;
10+
}
11+
// Check for Node.js environment
12+
else if (typeof global !== "undefined" && global.crypto) {
13+
crypto = global.crypto;
14+
}
15+
// Fallback to native Node.js crypto module
16+
else {
17+
try {
18+
crypto = require("crypto");
19+
} catch (error) {
20+
console.error("Failed to load the crypto module:", error.message);
21+
throw error;
22+
}
23+
}
24+
return crypto;
25+
}
26+
427
export function base64Helper(str: string): string {
528
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
629
let chars = "";

0 commit comments

Comments
 (0)