diff --git a/examples/console-app/.env.template b/examples/console-app/.env.template index daf09ed8..304f7fff 100644 --- a/examples/console-app/.env.template +++ b/examples/console-app/.env.template @@ -10,3 +10,5 @@ APPCONFIG_ENDPOINT= AZURE_TENANT_ID= AZURE_CLIENT_ID= AZURE_CLIENT_SECRET= + +AZURE_FRONT_DOOR_ENDPOINT= \ No newline at end of file diff --git a/examples/console-app/azureFrontDoor.mjs b/examples/console-app/azureFrontDoor.mjs new file mode 100644 index 00000000..14a26044 --- /dev/null +++ b/examples/console-app/azureFrontDoor.mjs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as dotenv from "dotenv"; +import { promisify } from "util"; +dotenv.config(); +const sleepInMs = promisify(setTimeout); + +/** + * This example retrives all settings with key following pattern "app.settings.*", i.e. starting with "app.settings.". + * With the option `trimKeyPrefixes`, it trims the prefix "app.settings." from keys for simplicity. + * Value of config "app.settings.message" will be printed. + * + * Below environment variables are required for this example: + * - APPCONFIG_CONNECTION_STRING + */ + +import { loadFromAzureFrontDoor } from "@azure/app-configuration-provider"; +const endpoint = process.env.AZURE_FRONT_DOOR_ENDPOINT; +const settings = await loadFromAzureFrontDoor(endpoint, { + selectors: [{ + keyFilter: "CDN.*" + }], + trimKeyPrefixes: ["CDN."], + refreshOptions: { + enabled: true, + refreshIntervalInMs: 15_000 + } +}); + +while (true) { + await settings.refresh(); + console.log(`Message from Azure Front Door: ${settings.get("Message")}`); + // wait for 30 seconds + await sleepInMs(30_000); +} \ No newline at end of file diff --git a/examples/console-app/helloworld.mjs b/examples/console-app/helloworld.mjs index 5a3558c7..630d386a 100644 --- a/examples/console-app/helloworld.mjs +++ b/examples/console-app/helloworld.mjs @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as dotenv from "dotenv"; -dotenv.config() +dotenv.config(); /** * This example retrives all settings with key following pattern "app.settings.*", i.e. starting with "app.settings.". diff --git a/examples/console-app/helloworld_aad.mjs b/examples/console-app/helloworld_aad.mjs index 37ac15e8..55a4474d 100644 --- a/examples/console-app/helloworld_aad.mjs +++ b/examples/console-app/helloworld_aad.mjs @@ -2,7 +2,7 @@ // Licensed under the MIT license. import * as dotenv from "dotenv"; -dotenv.config() +dotenv.config(); /** * This example retrives all settings with key following pattern "app.settings.*", i.e. starting with "app.settings.". diff --git a/examples/console-app/package-lock.json b/examples/console-app/package-lock.json index 43ee0f24..35c6c7f4 100644 --- a/examples/console-app/package-lock.json +++ b/examples/console-app/package-lock.json @@ -1,17 +1,19 @@ { - "name": "examples", + "name": "console-app", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { - "@azure/app-configuration-provider": "../", + "@azure/app-configuration-provider": "../../", "@azure/identity": "^4.1.0", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "express": "^4.21.2" } }, "..": { "version": "2.0.0", + "extraneous": true, "license": "MIT", "dependencies": { "@azure/app-configuration": "^1.6.1", @@ -41,6 +43,38 @@ "uuid": "^9.0.1" } }, + "../..": { + "name": "@azure/app-configuration-provider", + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "@azure/app-configuration": "^1.9.0", + "@azure/identity": "^4.2.1", + "@azure/keyvault-secrets": "^4.7.0" + }, + "devDependencies": { + "@rollup/plugin-typescript": "^11.1.2", + "@types/mocha": "^10.0.4", + "@types/node": "^22.7.7", + "@types/sinon": "^17.0.1", + "@types/uuid": "^9.0.7", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "chai": "^4.3.7", + "chai-as-promised": "^7.1.1", + "dotenv": "^16.3.1", + "eslint": "^8.48.0", + "mocha": "^10.2.0", + "nock": "^13.3.3", + "rimraf": "^5.0.1", + "rollup": "^3.29.5", + "rollup-plugin-dts": "^5.3.0", + "sinon": "^15.2.0", + "tslib": "^2.6.0", + "typescript": "^5.6.3", + "uuid": "^9.0.1" + } + }, "node_modules/@azure/abort-controller": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", @@ -53,7 +87,7 @@ } }, "node_modules/@azure/app-configuration-provider": { - "resolved": "..", + "resolved": "../..", "link": true }, "node_modules/@azure/core-auth": { @@ -194,6 +228,19 @@ "node": ">=16" } }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", @@ -205,11 +252,130 @@ "node": ">= 14" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -234,6 +400,25 @@ "node": ">=8" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -245,6 +430,20 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -253,6 +452,66 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -261,6 +520,216 @@ "node": ">=0.8.x" } }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -285,6 +754,33 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -404,11 +900,113 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -425,6 +1023,73 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -444,6 +1109,12 @@ } ] }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -455,6 +1126,156 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stoppable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", @@ -464,11 +1285,51 @@ "npm": ">=6" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tslib": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -476,6 +1337,15 @@ "bin": { "uuid": "dist/bin/uuid" } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } } } } diff --git a/examples/console-app/package.json b/examples/console-app/package.json index 9358ff07..176dd352 100644 --- a/examples/console-app/package.json +++ b/examples/console-app/package.json @@ -1,6 +1,6 @@ { "dependencies": { - "@azure/app-configuration-provider": "latest", + "@azure/app-configuration-provider": "../../", "@azure/identity": "^4.1.0", "dotenv": "^16.3.1", "express": "^4.21.2" diff --git a/package-lock.json b/package-lock.json index e30a06e4..9766ab82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.2.0", "license": "MIT", "dependencies": { - "@azure/app-configuration": "^1.8.0", + "@azure/app-configuration": "^1.9.0", "@azure/identity": "^4.2.1", "@azure/keyvault-secrets": "^4.7.0", "jsonc-parser": "^3.3.1" @@ -61,6 +61,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@azure/app-configuration/-/app-configuration-1.9.0.tgz", "integrity": "sha512-X0AVDQygL4AGLtplLYW+W0QakJpJ417sQldOacqwcBQ882tAPdUVs6V3mZ4jUjwVsgr+dV1v9zMmijvsp6XBxA==", + "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", diff --git a/package.json b/package.json index d5934885..1e661373 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "uuid": "^9.0.1" }, "dependencies": { - "@azure/app-configuration": "^1.8.0", + "@azure/app-configuration": "^1.9.0", "@azure/identity": "^4.2.1", "@azure/keyvault-secrets": "^4.7.0", "jsonc-parser": "^3.3.1" diff --git a/rollup.config.mjs b/rollup.config.mjs index 0df0e168..b20d604a 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -7,6 +7,7 @@ export default [ external: [ "@azure/app-configuration", "@azure/keyvault-secrets", + "@azure/core-client", "@azure/core-rest-pipeline", "@azure/identity", "crypto", diff --git a/src/AzureAppConfigurationImpl.ts b/src/AzureAppConfigurationImpl.ts index 980780f2..d85c8424 100644 --- a/src/AzureAppConfigurationImpl.ts +++ b/src/AzureAppConfigurationImpl.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { OperationOptions } from "@azure/core-client"; import { AppConfigurationClient, ConfigurationSetting, @@ -24,7 +25,7 @@ import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./StartupOptions.js"; import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js"; import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "./keyvault/KeyVaultOptions.js"; import { Disposable } from "./common/disposable.js"; -import { base64Helper, jsonSorter } from "./common/utils.js"; +import { base64Helper, jsonSorter, getCryptoModule } from "./common/utils.js"; import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, @@ -63,6 +64,7 @@ import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOp import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js"; import { KeyFilter, LabelFilter, SettingSelector } from "./types.js"; import { ConfigurationClientManager } from "./ConfigurationClientManager.js"; +import { CDN_TOKEN_LOOKUP_HEADER, calculateResourceDeletedCacheConsistencyToken } from "./azureFrontDoor/cdnRequestPipelinePolicy.js"; import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common/backoffUtils.js"; import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError } from "./common/error.js"; @@ -72,6 +74,18 @@ type PagedSettingSelector = SettingSelector & { pageEtags?: string[]; }; +type SettingSelectorCollection = { + selectors: PagedSettingSelector[]; + + /** + * This is used to append to the request url for breaking the CDN cache. + * It uses the etag which has changed after the last refresh. + * It can either be the page etag or etag of a watched setting depending on the refresh monitoring strategy. + * When a watched setting is deleted, the token value will be SHA-256 hash of `ResourceDeleted\n{previous-etag}`. + */ + cdnToken?: string; +} + export class AzureAppConfigurationImpl implements AzureAppConfiguration { /** * Hosting key-value pairs in the configuration store. @@ -121,20 +135,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { /** * Selectors of key-values obtained from @see AzureAppConfigurationOptions.selectors */ - #kvSelectors: PagedSettingSelector[] = []; + #kvSelectorCollection: SettingSelectorCollection = { selectors: [] }; /** * Selectors of feature flags obtained from @see AzureAppConfigurationOptions.featureFlagOptions.selectors */ - #ffSelectors: PagedSettingSelector[] = []; + #ffSelectorCollection: SettingSelectorCollection = { selectors: [] }; // Load balancing #lastSuccessfulEndpoint: string = ""; + // CDN + #isCdnUsed: boolean; + constructor( clientManager: ConfigurationClientManager, options: AzureAppConfigurationOptions | undefined, + isCdnUsed: boolean ) { this.#options = options; + this.#isCdnUsed = isCdnUsed; this.#clientManager = clientManager; // enable request tracing if not opt-out @@ -148,9 +167,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a)); } - // if no selector is specified, always load key values using the default selector: key="*" and label="\0" - this.#kvSelectors = getValidKeyValueSelectors(options?.selectors); - if (options?.refreshOptions?.enabled === true) { this.#refreshEnabled = true; const { refreshIntervalInMs, watchedSettings } = options.refreshOptions; @@ -178,11 +194,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { this.#kvRefreshTimer = new RefreshTimer(this.#kvRefreshInterval); } + // if no selector is specified, always load key values using the default selector: key="*" and label="\0" + this.#kvSelectorCollection.selectors = getValidKeyValueSelectors(options?.selectors); + // feature flag options - if (options?.featureFlagOptions?.enabled === true) { + if (options?.featureFlagOptions?.enabled) { this.#featureFlagEnabled = true; // validate feature flag selectors, only load feature flags when enabled - this.#ffSelectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors); + this.#ffSelectorCollection.selectors = getValidFeatureFlagSelectors(options.featureFlagOptions.selectors); if (options.featureFlagOptions.refresh?.enabled === true) { this.#featureFlagRefreshEnabled = true; @@ -221,6 +240,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { initialLoadCompleted: this.#isInitialLoadCompleted, replicaCount: this.#clientManager.getReplicaCount(), isFailoverRequest: this.#isFailoverRequest, + isCdnUsed: this.#isCdnUsed, featureFlagTracing: this.#featureFlagTracing, fmVersion: this.#fmVersion, aiConfigurationTracing: this.#aiConfigurationTracing @@ -263,6 +283,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { /** * Loads the configuration store for the first time. + * @internal */ async load() { const startTimestamp = Date.now(); @@ -487,14 +508,13 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { * If false, loads key-value using the key-value selectors. Defaults to false. */ async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise { - const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors; + const selectorCollection = loadFeatureFlag ? this.#ffSelectorCollection : this.#kvSelectorCollection; const funcToExecute = async (client) => { const loadedSettings: ConfigurationSetting[] = []; // deep copy selectors to avoid modification if current client fails - const selectorsToUpdate = JSON.parse( - JSON.stringify(selectors) + const selectorsToUpdate: PagedSettingSelector[] = JSON.parse( + JSON.stringify(selectorCollection.selectors) ); - for (const selector of selectorsToUpdate) { if (selector.snapshotName === undefined) { const listOptions: ListConfigurationSettingsOptions = { @@ -502,6 +522,12 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { labelFilter: selector.labelFilter, tagsFilter: selector.tagFilters }; + + // If CDN is used, add etag to request header so that the pipeline policy can retrieve and append it to the request URL + if (this.#isCdnUsed && selectorCollection.cdnToken) { + this.#addCdnTokenLookupHeader(listOptions, selectorCollection.cdnToken); + } + const pageEtags: string[] = []; const pageIterator = listConfigurationSettingsWithTrace( this.#requestTraceOptions, @@ -517,6 +543,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } } + + if (pageEtags.length === 0) { + console.warn(`No page is found in the response of listing key-value selector: key=${selector.keyFilter} and label=${selector.labelFilter}.`); + } + selector.pageEtags = pageEtags; } else { // snapshot selector const snapshot = await this.#getSnapshot(selector.snapshotName); @@ -542,11 +573,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } - if (loadFeatureFlag) { - this.#ffSelectors = selectorsToUpdate; - } else { - this.#kvSelectors = selectorsToUpdate; - } + selectorCollection.selectors = selectorsToUpdate; return loadedSettings; }; @@ -592,22 +619,23 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } /** - * Updates etag of watched settings from loaded data. If a watched setting is not covered by any selector, a request will be sent to retrieve it. + * Updates etag of watched settings from loaded data. + * If a watched setting is not covered by any selector, a request will be sent to retrieve it. + * If there is no watched setting(sentinel key), this method does nothing. */ - async #updateWatchedKeyValuesEtag(existingSettings: ConfigurationSetting[]): Promise { + async #updateWatchedKeyValuesEtag(loadedSettings: ConfigurationSetting[]): Promise { for (const sentinel of this.#sentinels) { - const matchedSetting = existingSettings.find(s => s.key === sentinel.key && s.label === sentinel.label); - if (matchedSetting) { - sentinel.etag = matchedSetting.etag; + const loaded = loadedSettings.find(s => s.key === sentinel.key && s.label === sentinel.label); + if (loaded) { + sentinel.etag = loaded.etag; } else { - // Send a request to retrieve key-value since it may be either not loaded or loaded with a different label or different casing - const { key, label } = sentinel; - const response = await this.#getConfigurationSetting({ key, label }); - if (response) { - sentinel.etag = response.etag; - } else { - sentinel.etag = undefined; + // Send a request to retrieve watched key-value since it may be either not loaded or loaded with a different selector + const getOptions: GetConfigurationSettingOptions = {}; + if (this.#isCdnUsed && this.#kvSelectorCollection.cdnToken) { + this.#addCdnTokenLookupHeader(getOptions, this.#kvSelectorCollection.cdnToken); } + const response = await this.#getConfigurationSetting(sentinel, getOptions); + sentinel.etag = response?.etag; } } } @@ -657,17 +685,29 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { // try refresh if any of watched settings is changed. let needRefresh = false; if (this.#watchAll) { - needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectors); + needRefresh = await this.#checkConfigurationSettingsChange(this.#kvSelectorCollection); } + // if watchAll is true, there should be no sentinels for (const sentinel of this.#sentinels.values()) { - const response = await this.#getConfigurationSetting(sentinel, { - onlyIfChanged: true - }); + const getOptions: GetConfigurationSettingOptions = { + // send conditional request only when CDN is not used + onlyIfChanged: !this.#isCdnUsed + }; + if (this.#isCdnUsed && this.#kvSelectorCollection.cdnToken) { + this.#addCdnTokenLookupHeader(getOptions, this.#kvSelectorCollection.cdnToken); + } + const response = await this.#getConfigurationSetting(sentinel, getOptions); - if (response?.statusCode === 200 // created or changed - || (response === undefined && sentinel.etag !== undefined) // deleted + if ((response?.statusCode === 200 && sentinel.etag !== response?.etag) || + (response === undefined && sentinel.etag !== undefined) // deleted ) { - sentinel.etag = response?.etag;// update etag of the sentinel + if (response === undefined) { + this.#kvSelectorCollection.cdnToken = + await calculateResourceDeletedCacheConsistencyToken(sentinel.etag!); + } else { + this.#kvSelectorCollection.cdnToken = response.etag; + } + sentinel.etag = response?.etag; // update etag of the sentinel needRefresh = true; break; } @@ -694,7 +734,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { return Promise.resolve(false); } - const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors); + const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectorCollection); if (needRefresh) { await this.#loadFeatureFlags(); } @@ -724,32 +764,56 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { /** * Checks whether the key-value collection has changed. - * @param selectors - The @see PagedSettingSelector of the kev-value collection. + * @param selectorCollection - The @see SettingSelectorCollection of the kev-value collection. * @returns true if key-value collection has changed, false otherwise. */ - async #checkConfigurationSettingsChange(selectors: PagedSettingSelector[]): Promise { + async #checkConfigurationSettingsChange(selectorCollection: SettingSelectorCollection): Promise { const funcToExecute = async (client) => { - for (const selector of selectors) { + for (const selector of selectorCollection.selectors) { if (selector.snapshotName) { // skip snapshot selector continue; } const listOptions: ListConfigurationSettingsOptions = { keyFilter: selector.keyFilter, labelFilter: selector.labelFilter, - tagsFilter: selector.tagFilters, - pageEtags: selector.pageEtags + tagsFilter: selector.tagFilters }; + if (!this.#isCdnUsed) { + // if CDN is not used, add page etags to the listOptions to send conditional request + listOptions.pageEtags = selector.pageEtags; + } else if (selectorCollection.cdnToken) { + this.#addCdnTokenLookupHeader(listOptions, selectorCollection.cdnToken); + } + const pageIterator = listConfigurationSettingsWithTrace( this.#requestTraceOptions, client, listOptions ).byPage(); + if (selector.pageEtags === undefined || selector.pageEtags.length === 0) { + return true; // no etag is retrieved from previous request, always refresh + } + + let i = 0; for await (const page of pageIterator) { - if (page._response.status === 200) { // created or changed + if (i >= selector.pageEtags.length || // new page + (page._response.status === 200 && page.etag !== selector.pageEtags[i])) { // page changed + // 100 kvs will return two pages, one page with 100 items and another empty page + // kv collection change will always be detected by page etag change + if (this.#isCdnUsed) { + selectorCollection.cdnToken = page.etag; + } return true; } + i++; + } + if (i !== selector.pageEtags.length) { // page removed + if (this.#isCdnUsed) { + selectorCollection.cdnToken = selector.pageEtags[i]; + } + return true; } } return false; @@ -1034,57 +1098,39 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration { } } - 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 { - if (typeof module !== "undefined" && module.exports) { - crypto = require("crypto"); - } - else { - crypto = await import("crypto"); - } - } catch (error) { - console.error("Failed to load the crypto module:", error.message); - throw error; - } - } - + const crypto = getCryptoModule(); // Convert to UTF-8 encoded bytes - const data = new TextEncoder().encode(rawAllocationId); - - // In the browser, use crypto.subtle.digest + const payload = new TextEncoder().encode(rawAllocationId); + // In the browser or Node.js 18+, use crypto.subtle.digest if (crypto.subtle) { - const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashBuffer = await crypto.subtle.digest("SHA-256", payload); const hashArray = new Uint8Array(hashBuffer); // Only use the first 15 bytes const first15Bytes = hashArray.slice(0, 15); - - // btoa/atob is also available in Node.js 18+ const base64String = btoa(String.fromCharCode(...first15Bytes)); const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return base64urlString; } - // In Node.js, use the crypto module's hash function + // Use the crypto module's hash function else { - const hash = crypto.createHash("sha256").update(data).digest(); + const hash = crypto.createHash("sha256").update(payload).digest(); // Only use the first 15 bytes const first15Bytes = hash.slice(0, 15); - return first15Bytes.toString("base64url"); } } + + #addCdnTokenLookupHeader(operationOptions: OperationOptions, cdnToken: string): void { + if (!operationOptions.requestOptions) { + operationOptions.requestOptions = {}; + } + if (!operationOptions.requestOptions.customHeaders) { + operationOptions.requestOptions.customHeaders = {}; + } + operationOptions.requestOptions.customHeaders[CDN_TOKEN_LOOKUP_HEADER] = cdnToken; + } } function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] { diff --git a/src/azureFrontDoor/cdnRequestPipelinePolicy.ts b/src/azureFrontDoor/cdnRequestPipelinePolicy.ts new file mode 100644 index 00000000..50bf947d --- /dev/null +++ b/src/azureFrontDoor/cdnRequestPipelinePolicy.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { PipelinePolicy } from "@azure/core-rest-pipeline"; +import { getCryptoModule } from "../common/utils.js"; + +const CDN_TOKEN_QUERY_PARAMETER = "_"; +const RESOURCE_DELETED_PREFIX = "ResourceDeleted"; + +export const CDN_TOKEN_LOOKUP_HEADER = "cdn-token-lookup"; + +/** + * The pipeline policy that retrieves the CDN token from the request header and appends it to the request URL. After that the lookup header is removed from the request. + * @remarks + * The policy position should be perCall. + * The App Configuration service will not recognize the CDN token 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 CdnTokenPipelinePolicy implements PipelinePolicy { + name: string = "AppConfigurationCdnTokenPolicy"; + + async sendRequest(request, next) { + if (request.headers.has(CDN_TOKEN_LOOKUP_HEADER)) { + const token = request.headers.get(CDN_TOKEN_LOOKUP_HEADER); + request.headers.delete(CDN_TOKEN_LOOKUP_HEADER); + + const url = new URL(request.url); + url.searchParams.append(CDN_TOKEN_QUERY_PARAMETER, token); // _ is a dummy query parameter to break the CDN cache + request.url = url.toString(); + } + + return next(request); + } +} + +/** + * Calculates a cache consistency token for a deleted resource based on its previous ETag. + * @param etag - The previous ETag of the deleted resource. + */ +export async function calculateResourceDeletedCacheConsistencyToken(etag: string): Promise { + const crypto = getCryptoModule(); + const rawString = `${RESOURCE_DELETED_PREFIX}\n${etag}`; + const payload = new TextEncoder().encode(rawString); + // In the browser or Node.js 18+, use crypto.subtle.digest + if (crypto.subtle) { + const hashBuffer = await crypto.subtle.digest("SHA-256", payload); + const hashArray = new Uint8Array(hashBuffer); + const base64String = btoa(String.fromCharCode(...hashArray)); + const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); + return base64urlString; + } + // Use the crypto module's hash function + else { + const hash = crypto.createHash("sha256").update(payload).digest(); + return hash.toString("base64url"); + } +} + +/** + * The pipeline policy that remove the authorization header from the request to allow anonymous access to the Azure Front Door. + * @remarks + * The policy position should be perRetry, since it should be executed after the "Sign" phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/src/serviceClient.ts + */ +export class AnonymousRequestPipelinePolicy implements PipelinePolicy { + name: string = "AppConfigurationAnonymousRequestPolicy"; + + async sendRequest(request, next) { + if (request.headers.has("authorization")) { + request.headers.delete("authorization"); + } + return next(request); + } +} diff --git a/src/common/utils.ts b/src/common/utils.ts index 0264a72e..79e85f56 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -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 = ""; diff --git a/src/index.ts b/src/index.ts index 92836b9f..e4c4b818 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,6 @@ export { AzureAppConfiguration } from "./AzureAppConfiguration.js"; export { Disposable } from "./common/disposable.js"; -export { load } from "./load.js"; +export { load, loadFromAzureFrontDoor } from "./load.js"; export { KeyFilter, LabelFilter } from "./types.js"; export { VERSION } from "./version.js"; diff --git a/src/load.ts b/src/load.ts index 15f88218..afc22f7a 100644 --- a/src/load.ts +++ b/src/load.ts @@ -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 { CdnTokenPipelinePolicy, AnonymousRequestPipelinePolicy } from "./azureFrontDoor/cdnRequestPipelinePolicy.js"; import { instanceOfTokenCredential } from "./common/utils.js"; +import { ArgumentError } from "./common/error.js"; const MIN_DELAY_FOR_UNHANDLED_ERROR_IN_MS: number = 5_000; +// Empty token credential to be used when loading from Azure Front Door +const emptyTokenCredential: TokenCredential = { + getToken: async () => ({ token: "", expiresOnTimestamp: Number.MAX_SAFE_INTEGER }) +}; + /** * Loads the data from Azure App Configuration service and returns an instance of AzureAppConfiguration. * @param connectionString The connection string for the App Configuration store. @@ -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) { @@ -56,3 +64,35 @@ export async function load( throw error; } } + +/** + * Loads the data from Azure Front Door (CDN) and returns an instance of AzureAppConfiguration. + * @param endpoint The URL to the Azure Front Door. + * @param appConfigOptions Optional parameters. + */ +export async function loadFromAzureFrontDoor(endpoint: URL | string, options?: AzureAppConfigurationOptions): Promise; + +export async function loadFromAzureFrontDoor( + endpoint: string | URL, + appConfigOptions: AzureAppConfigurationOptions = {} +): Promise { + if (appConfigOptions.replicaDiscoveryEnabled) { + throw new ArgumentError("Replica discovery is not supported when loading from Azure Front Door."); + } + if (appConfigOptions.loadBalancingEnabled) { + throw new ArgumentError("Load balancing is not supported when loading from Azure Front Door."); + } + appConfigOptions.replicaDiscoveryEnabled = false; // Disable replica discovery when loading from Azure Front Door + + 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 CdnTokenPipelinePolicy(), position: "perCall" }, + { policy: new AnonymousRequestPipelinePolicy(), position: "perRetry" } + ] + }; + + return await load(endpoint, emptyTokenCredential, appConfigOptions); +} diff --git a/src/requestTracing/constants.ts b/src/requestTracing/constants.ts index 6f9311b4..896d7abd 100644 --- a/src/requestTracing/constants.ts +++ b/src/requestTracing/constants.ts @@ -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 KEY_VAULT_REFRESH_CONFIGURED_TAG = "RefreshesKeyVault"; export const FAILOVER_REQUEST_TAG = "Failover"; diff --git a/src/requestTracing/utils.ts b/src/requestTracing/utils.ts index ada90382..53f7b7e4 100644 --- a/src/requestTracing/utils.ts +++ b/src/requestTracing/utils.ts @@ -19,6 +19,7 @@ import { HOST_TYPE_KEY, HostType, KEY_VAULT_CONFIGURED_TAG, + CDN_USED_TAG, KEY_VAULT_REFRESH_CONFIGURED_TAG, KUBERNETES_ENV_VAR, NODEJS_DEV_ENV_VAL, @@ -43,6 +44,7 @@ export interface RequestTracingOptions { initialLoadCompleted: boolean; replicaCount: number; isFailoverRequest: boolean; + isCdnUsed: boolean; featureFlagTracing: FeatureFlagTracingOptions | undefined; fmVersion: string | undefined; aiConfigurationTracing: AIConfigurationTracingOptions | undefined; @@ -92,7 +94,9 @@ function applyRequestTracing(requestTracingOptions: const actualOptions = { ...operationOptions }; if (requestTracingOptions.enabled) { actualOptions.requestOptions = { + ...actualOptions.requestOptions, customHeaders: { + ...actualOptions.requestOptions?.customHeaders, [CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(requestTracingOptions) } }; @@ -112,6 +116,7 @@ function createCorrelationContextHeader(requestTracingOptions: RequestTracingOpt FFFeatures: Seed+Telemetry UsersKeyVault Failover + CDN */ const keyValues = new Map(); const tags: string[] = []; @@ -143,6 +148,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()); } diff --git a/test/exportedApi.ts b/test/exportedApi.ts index 8a0e4757..74ff44f2 100644 --- a/test/exportedApi.ts +++ b/test/exportedApi.ts @@ -1,4 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -export { load } from "../src"; +export { load, loadFromAzureFrontDoor } from "../src"; +export { CDN_TOKEN_LOOKUP_HEADER } from "../src/azureFrontDoor/cdnRequestPipelinePolicy.js"; diff --git a/test/loadBalance.test.ts b/test/loadBalance.test.ts index 59bdf0f8..66179aa1 100644 --- a/test/loadBalance.test.ts +++ b/test/loadBalance.test.ts @@ -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))); @@ -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, { @@ -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 diff --git a/test/loadFromAzureFrontDoor.test.ts b/test/loadFromAzureFrontDoor.test.ts new file mode 100644 index 00000000..b1b546cc --- /dev/null +++ b/test/loadFromAzureFrontDoor.test.ts @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import * as chai from "chai"; +import * as chaiAsPromised from "chai-as-promised"; +chai.use(chaiAsPromised); +const expect = chai.expect; +import { CDN_TOKEN_LOOKUP_HEADER, loadFromAzureFrontDoor } from "./exportedApi.js"; +import { MAX_TIME_OUT, mockAppConfigurationClientListConfigurationSettings, mockAppConfigurationClientGetConfigurationSetting, restoreMocks, createMockedEndpoint, createMockedKeyValue, sleepInMs } from "./utils/testHelper.js"; +import * as uuid from "uuid"; +import { ListConfigurationSettingsOptions, GetConfigurationSettingOptions } from "@azure/app-configuration"; + +let mockedKVs: any[] = []; + +function updateSetting(key: string, value: any) { + const setting = mockedKVs.find(elem => elem.key === key); + if (setting) { + setting.value = value; + setting.etag = uuid.v4(); + } +} + +describe("load from Azure Front Door", function () { + this.timeout(MAX_TIME_OUT); + + before(() => { + mockedKVs = [ + { value: "red", key: "app.settings.fontColor" }, + { value: "40", key: "app.settings.fontSize" } + ].map(createMockedKeyValue); + mockAppConfigurationClientListConfigurationSettings([mockedKVs]); + }); + + after(() => { + restoreMocks(); + }); + + it("should load data from Azure Front Door", async () => { + const endpoint = createMockedEndpoint(); + const settings = await loadFromAzureFrontDoor(endpoint); + expect(settings).not.undefined; + expect(settings.get("app.settings.fontColor")).eq("red"); + expect(settings.get("app.settings.fontSize")).eq("40"); + }); + + it("should throw error when replica discovery is enabled", async () => { + const endpoint = createMockedEndpoint(); + return expect(loadFromAzureFrontDoor(endpoint, { + replicaDiscoveryEnabled: true + })).eventually.rejectedWith("Replica discovery is not supported when loading from Azure Front Door."); + }); + + it("should throw error when load balancing is enabled", async () => { + const endpoint = createMockedEndpoint(); + return expect(loadFromAzureFrontDoor(endpoint, { + loadBalancingEnabled: true + })).eventually.rejectedWith("Load balancing is not supported when loading from Azure Front Door."); + }); +}); + +let cdnTokenLookup; +const listKvFromAfdCallback = (options: ListConfigurationSettingsOptions) => { + cdnTokenLookup = options.requestOptions?.customHeaders?.[CDN_TOKEN_LOOKUP_HEADER]; +}; +const getKvFromAfdCallback = (options: GetConfigurationSettingOptions) => { + cdnTokenLookup = options.requestOptions?.customHeaders?.[CDN_TOKEN_LOOKUP_HEADER]; +}; +describe("dynamic refresh when loading from Azure Front Door", function () { + this.timeout(MAX_TIME_OUT); + + beforeEach(() => { + mockedKVs = [ + { value: "red", key: "app.settings.fontColor" }, + { value: "40", key: "app.settings.fontSize" } + ].map(createMockedKeyValue); + mockAppConfigurationClientListConfigurationSettings([mockedKVs], listKvFromAfdCallback); + mockAppConfigurationClientGetConfigurationSetting(mockedKVs, getKvFromAfdCallback); + }); + + afterEach(() => { + restoreMocks(); + }); + + it("should append cdn token to the watch request", async () => { + const endpoint = createMockedEndpoint(); + const settings = await loadFromAzureFrontDoor(endpoint, { + refreshOptions: { + enabled: true, + refreshIntervalInMs: 2_000, + watchedSettings: [ + { key: "app.settings.fontColor" } + ] + } + }); + expect(settings).not.undefined; + expect(settings.get("app.settings.fontColor")).eq("red"); + expect(settings.get("app.settings.fontSize")).eq("40"); + + updateSetting("app.settings.fontColor", "blue"); + + await settings.refresh(); + expect(settings.get("app.settings.fontColor")).eq("red"); + expect(cdnTokenLookup).is.undefined; + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + expect(settings.get("app.settings.fontColor")).eq("blue"); + expect(cdnTokenLookup).is.not.undefined; + const previousCdnToken = cdnTokenLookup; + + updateSetting("app.settings.fontColor", "green"); + + await sleepInMs(2 * 1000 + 1); + await settings.refresh(); + expect(settings.get("app.settings.fontColor")).eq("green"); + expect(cdnTokenLookup).is.not.undefined; + expect(cdnTokenLookup).to.not.eq(previousCdnToken); + }); +}); diff --git a/test/requestTracing.test.ts b/test/requestTracing.test.ts index 942b329b..824a568b 100644 --- a/test/requestTracing.test.ts +++ b/test/requestTracing.test.ts @@ -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, loadFromAzureFrontDoor } from "./exportedApi.js"; const CORRELATION_CONTEXT_HEADER_NAME = "Correlation-Context"; @@ -111,6 +111,37 @@ describe("request tracing", function () { sinon.restore(); }); + it("should have cdn tag in correlation-context header when loadFromAzureFrontDoor is used", async () => { + try { + await loadFromAzureFrontDoor(fakeEndpoint, { + clientOptions, + startupOptions: { + timeoutInMs: 1 + } + }); + } catch (e) { /* empty */ } + expect(headerPolicy.headers).not.undefined; + expect(headerPolicy.headers.get("User-Agent")).satisfy((ua: string) => ua.startsWith("javascript-appconfiguration-provider")); + 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 { diff --git a/test/utils/testHelper.ts b/test/utils/testHelper.ts index de0d2470..fd0ed8c6 100644 --- a/test/utils/testHelper.ts +++ b/test/utils/testHelper.ts @@ -121,12 +121,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); }); }