Skip to content
Closed
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
ce9f550
use pipeline policy to ensure cdn request uses correct api version
zhiyuanliang-ms Nov 4, 2024
2adadc7
fix lint & add comments
zhiyuanliang-ms Nov 4, 2024
30108ab
update
zhiyuanliang-ms Nov 5, 2024
197db89
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 5, 2024
04d8a2c
update
zhiyuanliang-ms Nov 7, 2024
bf99b31
fix lint
zhiyuanliang-ms Nov 7, 2024
14364c5
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 10, 2024
62ac2b9
add request tracing for cdn
zhiyuanliang-ms Nov 10, 2024
bd1c875
only send conditional request when cdn is not used
zhiyuanliang-ms Nov 14, 2024
0856707
add testcase
zhiyuanliang-ms Nov 14, 2024
2b78b27
fix lint
zhiyuanliang-ms Nov 15, 2024
fdd30e2
refresh based on page etag
zhiyuanliang-ms Nov 18, 2024
c886936
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 18, 2024
2a7399b
merge preview
zhiyuanliang-ms Nov 18, 2024
b4afe7a
merge preview
zhiyuanliang-ms Nov 18, 2024
50fc5b6
remove watchAll & reorganize the code
zhiyuanliang-ms Nov 19, 2024
70bbdee
add testcase
zhiyuanliang-ms Nov 19, 2024
d8103a5
fix lint & update method name
zhiyuanliang-ms Nov 19, 2024
e123f0e
resolve merge conflict
zhiyuanliang-ms Nov 19, 2024
6958ad5
add comment
zhiyuanliang-ms Nov 19, 2024
635ca48
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Nov 19, 2024
61e4a65
not use conditional request
zhiyuanliang-ms Nov 19, 2024
4f36a1c
fix vulnerability
zhiyuanliang-ms Nov 20, 2024
d325127
resolve merge conflict
zhiyuanliang-ms Dec 2, 2024
647c7a6
resolve conflict
zhiyuanliang-ms Dec 2, 2024
b74e983
update variable name
zhiyuanliang-ms Dec 2, 2024
eec7114
merge
zhiyuanliang-ms Dec 2, 2024
01a7034
move public method
zhiyuanliang-ms Dec 2, 2024
0fb81da
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Dec 2, 2024
b0ab944
append etag to url
zhiyuanliang-ms Dec 3, 2024
c094801
update
zhiyuanliang-ms Dec 4, 2024
bbf1938
update
zhiyuanliang-ms Dec 4, 2024
f3ac831
add more comments
zhiyuanliang-ms Dec 5, 2024
e6fea3c
merge preview
zhiyuanliang-ms Dec 13, 2024
072bcdf
Merge branch 'zhiyuanliang/register-all-refresh' of https://github.co…
zhiyuanliang-ms Dec 13, 2024
356e664
fix lint
zhiyuanliang-ms Dec 13, 2024
2b95ab4
resolve merge conflict
zhiyuanliang-ms Dec 13, 2024
d3c2799
resolve merge conflict
zhiyuanliang-ms Dec 18, 2024
290d338
merge preview
zhiyuanliang-ms Dec 18, 2024
ea7709e
merge
zhiyuanliang-ms Dec 18, 2024
8ed48b5
merge preview
zhiyuanliang-ms Dec 19, 2024
6e34f62
resolve merge conflict
zhiyuanliang-ms Dec 19, 2024
14dbe96
update
zhiyuanliang-ms Dec 19, 2024
8441b32
resolve copilot comment
zhiyuanliang-ms Dec 19, 2024
6fe042a
fix lint
zhiyuanliang-ms Dec 19, 2024
33a5964
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Feb 12, 2025
f23ebe4
Merge branch 'preview' into zhiyuanliang/enforce-api-version-for-cdn
zhiyuanliang-ms Feb 20, 2025
e77d37d
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Apr 29, 2025
41ad554
fix lint
zhiyuanliang-ms Apr 29, 2025
334a047
update testcase
zhiyuanliang-ms Apr 29, 2025
9dec453
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms May 13, 2025
e457605
update
zhiyuanliang-ms Jun 3, 2025
12b0216
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Jun 3, 2025
c096a50
update
zhiyuanliang-ms Jun 4, 2025
811b0e4
fix lint
zhiyuanliang-ms Jun 4, 2025
03d956d
disable replica discovery for CDN
zhiyuanliang-ms Jun 4, 2025
96df9d9
WIP
zhiyuanliang-ms Jun 4, 2025
ac33118
update to latest design
zhiyuanliang-ms Jun 5, 2025
3528e87
update
zhiyuanliang-ms Jun 5, 2025
f688702
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Jun 5, 2025
19b70c2
update
zhiyuanliang-ms Jun 6, 2025
a30b421
update naming
zhiyuanliang-ms Jun 6, 2025
3f05285
update
zhiyuanliang-ms Jun 10, 2025
4f99512
rename api to loadFromAzureFrontDoor
zhiyuanliang-ms Jun 12, 2025
08f4732
update error message
zhiyuanliang-ms Jun 13, 2025
c0ae98a
add policy to remove authorization header
zhiyuanliang-ms Jun 26, 2025
c061b98
add test
zhiyuanliang-ms Jun 26, 2025
9254069
Merge branch 'zhiyuanliang/enforce-api-version-for-cdn' of https://gi…
zhiyuanliang-ms Jun 26, 2025
3005407
fix lint
zhiyuanliang-ms Jun 26, 2025
f367161
update
zhiyuanliang-ms Jun 26, 2025
d74fdd2
add cache break token test
zhiyuanliang-ms Jun 26, 2025
333eb3b
update request tracing testcase
zhiyuanliang-ms Jun 26, 2025
486ef1b
update
zhiyuanliang-ms Jul 10, 2025
26ff70f
add example
zhiyuanliang-ms Jul 30, 2025
047ad58
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Jul 30, 2025
5ede801
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Aug 7, 2025
4c331c4
update
zhiyuanliang-ms Aug 13, 2025
5b81582
fix lint
zhiyuanliang-ms Aug 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"uuid": "^9.0.1"
},
"dependencies": {
"@azure/app-configuration": "^1.6.1",
"@azure/app-configuration": "^1.9.0",
"@azure/identity": "^4.2.1",
"@azure/keyvault-secrets": "^4.7.0"
}
Expand Down
224 changes: 145 additions & 79 deletions src/AzureAppConfigurationImpl.ts

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions src/EtagUrlPipelinePolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { PipelinePolicy } from "@azure/core-rest-pipeline";

export const ETAG_LOOKUP_HEADER = "Etag-Lookup";

/**
* The pipeline policy that retrieves the etag from the request header and appends it to the request URL. After that the etag header is removed from the request.
* @remarks
* The policy position should be perCall.
* The App Configuration service will not recognize the etag query parameter in the url, but this can help to break the CDN cache as the cache entry is based on the URL.
*/
export class EtagUrlPipelinePolicy implements PipelinePolicy {
name: string = "AppConfigurationEtagUrlPolicy";

async sendRequest(request, next) {
if (request.headers.has(ETAG_LOOKUP_HEADER)) {
const etag = request.headers.get(ETAG_LOOKUP_HEADER);
request.headers.delete(ETAG_LOOKUP_HEADER);

const url = new URL(request.url);
url.searchParams.append("_", etag); // _ is a dummy query parameter to break the CDN cache
request.url = url.toString();
}

return next(request);
}
}
23 changes: 23 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export function getCryptoModule(): any {
let crypto;

// Check for browser environment
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
crypto = window.crypto;
}
// Check for Node.js environment
else if (typeof global !== "undefined" && global.crypto) {
crypto = global.crypto;
}
// Fallback to native Node.js crypto module
else {
try {
crypto = require("crypto");
} catch (error) {
console.error("Failed to load the crypto module:", error.message);
throw error;
}
}
return crypto;
}

export function base64Helper(str: string): string {
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
let chars = "";
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

export { AzureAppConfiguration } from "./AzureAppConfiguration.js";
export { Disposable } from "./common/disposable.js";
export { load } from "./load.js";
export { load, loadFromCdn } from "./load.js";
export { KeyFilter, LabelFilter } from "./types.js";
export { VERSION } from "./version.js";
41 changes: 40 additions & 1 deletion src/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ import { AzureAppConfiguration } from "./AzureAppConfiguration.js";
import { AzureAppConfigurationImpl } from "./AzureAppConfigurationImpl.js";
import { AzureAppConfigurationOptions } from "./AzureAppConfigurationOptions.js";
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
import { EtagUrlPipelinePolicy } from "./EtagUrlPipelinePolicy.js";
import { instanceOfTokenCredential } from "./common/utils.js";
import { ArgumentError } from "./common/error.js";

const MIN_DELAY_FOR_UNHANDLED_ERROR: number = 5_000; // 5 seconds

// Empty token credential to be used when loading from CDN
const emptyTokenCredential: TokenCredential = {
getToken: async () => ({ token: "", expiresOnTimestamp: 0 })
};

/**
* Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration.
* @param connectionString The connection string for the App Configuration store.
Expand Down Expand Up @@ -42,7 +49,8 @@ export async function load(
}

try {
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options);
const isCdnUsed: boolean = credentialOrOptions === emptyTokenCredential;
const appConfiguration = new AzureAppConfigurationImpl(clientManager, options, isCdnUsed);
await appConfiguration.load();
return appConfiguration;
} catch (error) {
Expand All @@ -56,3 +64,34 @@ export async function load(
throw error;
}
}

/**
* Loads the data from a CDN and returns an instance of AzureAppConfiguration.
* @param cdnEndpoint The URL to the CDN.
* @param appConfigOptions Optional parameters.
*/
export async function loadFromCdn(cdnEndpoint: URL | string, options?: AzureAppConfigurationOptions): Promise<AzureAppConfiguration>;

export async function loadFromCdn(
cdnEndpoint: string | URL,
appConfigOptions?: AzureAppConfigurationOptions
): Promise<AzureAppConfiguration> {
if (appConfigOptions === undefined) {
appConfigOptions = {
replicaDiscoveryEnabled: false // Disable replica discovery for CDN
};
} else if (appConfigOptions.replicaDiscoveryEnabled) {
throw new ArgumentError("Replica discovery is not supported when loading from CDN.");
}

appConfigOptions.clientOptions = {
...appConfigOptions.clientOptions,
// Add etag url policy to append etag to the request url for breaking CDN cache
additionalPolicies: [
...(appConfigOptions.clientOptions?.additionalPolicies || []),
{ policy: new EtagUrlPipelinePolicy(), position: "perCall" }
]
};

return await load(cdnEndpoint, emptyTokenCredential, appConfigOptions);
}
1 change: 1 addition & 0 deletions src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const REPLICA_COUNT_KEY = "ReplicaCount";

// Tag names
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
export const CDN_USED_TAG = "CDN";
export const FAILOVER_REQUEST_TAG = "Failover";

// Compact feature tags
Expand Down
7 changes: 7 additions & 0 deletions src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
HOST_TYPE_KEY,
HostType,
KEY_VAULT_CONFIGURED_TAG,
CDN_USED_TAG,
KUBERNETES_ENV_VAR,
NODEJS_DEV_ENV_VAL,
NODEJS_ENV_VAR,
Expand All @@ -42,6 +43,7 @@ export interface RequestTracingOptions {
initialLoadCompleted: boolean;
replicaCount: number;
isFailoverRequest: boolean;
isCdnUsed: boolean;
featureFlagTracing: FeatureFlagTracingOptions | undefined;
fmVersion: string | undefined;
aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
Expand Down Expand Up @@ -91,6 +93,7 @@ function applyRequestTracing<T extends OperationOptions>(requestTracingOptions:
const actualOptions = { ...operationOptions };
if (requestTracingOptions.enabled) {
actualOptions.requestOptions = {
...actualOptions.requestOptions,
customHeaders: {
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions)
}
Expand All @@ -111,6 +114,7 @@ function createCorrelationContextHeader(requestTracingOptions: RequestTracingOpt
FFFeatures: Seed+Telemetry
UsersKeyVault
Failover
CDN
*/
const keyValues = new Map<string, string | undefined>();
const tags: string[] = [];
Expand Down Expand Up @@ -139,6 +143,9 @@ function createCorrelationContextHeader(requestTracingOptions: RequestTracingOpt
if (requestTracingOptions.isFailoverRequest) {
tags.push(FAILOVER_REQUEST_TAG);
}
if (requestTracingOptions.isCdnUsed) {
tags.push(CDN_USED_TAG);
}
if (requestTracingOptions.replicaCount > 0) {
keyValues.set(REPLICA_COUNT_KEY, requestTracingOptions.replicaCount.toString());
}
Expand Down
2 changes: 1 addition & 1 deletion test/exportedApi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export { load } from "../src";
export { load, loadFromCdn } from "../src";
15 changes: 10 additions & 5 deletions test/loadBalance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ import * as chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
const expect = chai.expect;
import { load } from "./exportedApi.js";
import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
import { MAX_TIME_OUT, restoreMocks, createMockedConnectionString, createMockedKeyValue, sleepInMs, createMockedEndpoint, mockConfigurationManagerGetClients, mockAppConfigurationClientLoadBalanceMode } from "./utils/testHelper.js";
import { AppConfigurationClient } from "@azure/app-configuration";
import { ConfigurationClientWrapper } from "../src/ConfigurationClientWrapper.js";

const mockedKVs = [
{ value: "red", key: "app.settings.fontColor" },
{ value: "40", key: "app.settings.fontSize" },
{ value: "30", key: "app.settings.fontSize", label: "prod" }
].map(createMockedKeyValue);
const fakeEndpoint_1 = createMockedEndpoint("fake_1");
const fakeEndpoint_2 = createMockedEndpoint("fake_2");
const fakeClientWrapper_1 = new ConfigurationClientWrapper(fakeEndpoint_1, new AppConfigurationClient(createMockedConnectionString(fakeEndpoint_1)));
Expand All @@ -29,8 +34,8 @@ describe("load balance", function () {

it("should load balance the request when loadBalancingEnabled", async () => {
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_2, clientRequestCounter_2);

const connectionString = createMockedConnectionString();
const settings = await load(connectionString, {
Expand Down Expand Up @@ -66,8 +71,8 @@ describe("load balance", function () {
clientRequestCounter_1.count = 0;
clientRequestCounter_2.count = 0;
mockConfigurationManagerGetClients([fakeClientWrapper_1, fakeClientWrapper_2], false);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode(fakeClientWrapper_2, clientRequestCounter_2);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_1, clientRequestCounter_1);
mockAppConfigurationClientLoadBalanceMode([mockedKVs], fakeClientWrapper_2, clientRequestCounter_2);

const connectionString = createMockedConnectionString();
// loadBalancingEnabled is default to false
Expand Down
32 changes: 31 additions & 1 deletion test/requestTracing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ chai.use(chaiAsPromised);
const expect = chai.expect;
import { MAX_TIME_OUT, HttpRequestHeadersPolicy, createMockedConnectionString, createMockedKeyValue, createMockedFeatureFlag, createMockedTokenCredential, mockAppConfigurationClientListConfigurationSettings, restoreMocks, sinon, sleepInMs } from "./utils/testHelper.js";
import { ConfigurationClientManager } from "../src/ConfigurationClientManager.js";
import { load } from "./exportedApi.js";
import { load, loadFromCdn } from "./exportedApi.js";

const CORRELATION_CONTEXT_HEADER_NAME = "Correlation-Context";

Expand Down Expand Up @@ -111,6 +111,36 @@ describe("request tracing", function () {
sinon.restore();
});

it("should have cdn tag in correlation-context header when loadFromCdn is used", async () => {
try {
await loadFromCdn(fakeEndpoint, {
clientOptions,
startupOptions: {
timeoutInMs: 1
}
});
} catch (e) { /* empty */ }
expect(headerPolicy.headers).not.undefined;
const correlationContext = headerPolicy.headers.get("Correlation-Context");
expect(correlationContext).not.undefined;
expect(correlationContext.includes("CDN")).eq(true);
});

it("should not have cdn tag in correlation-context header when load is used", async () => {
try {
await load(createMockedConnectionString(fakeEndpoint), {
clientOptions,
startupOptions: {
timeoutInMs: 1
}
});
} catch (e) { /* empty */ }
expect(headerPolicy.headers).not.undefined;
const correlationContext = headerPolicy.headers.get("Correlation-Context");
expect(correlationContext).not.undefined;
expect(correlationContext.includes("CDN")).eq(false);
});

it("should detect env in correlation-context header", async () => {
process.env.NODE_ENV = "development";
try {
Expand Down
7 changes: 3 additions & 4 deletions test/utils/testHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,11 @@ function mockAppConfigurationClientListConfigurationSettings(pages: Configuratio
});
}

function mockAppConfigurationClientLoadBalanceMode(clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
const emptyPages: ConfigurationSetting[][] = [];
function mockAppConfigurationClientLoadBalanceMode(pages: ConfigurationSetting[][], clientWrapper: ConfigurationClientWrapper, countObject: { count: number }) {
sinon.stub(clientWrapper.client, "listConfigurationSettings").callsFake((listOptions) => {
countObject.count += 1;
const kvs = _filterKVs(emptyPages.flat(), listOptions);
return getMockedIterator(emptyPages, kvs, listOptions);
const kvs = _filterKVs(pages.flat(), listOptions);
return getMockedIterator(pages, kvs, listOptions);
});
}

Expand Down