From 2aa298ce9ef6fcbb4f020888b6d4b78186e275fa Mon Sep 17 00:00:00 2001 From: Albert Ilagan Date: Mon, 17 Mar 2025 23:02:54 +0800 Subject: [PATCH 1/4] feat: add unleash server provider Signed-off-by: Albert Ilagan --- .release-please-manifest.json | 3 +- libs/providers/unleash/.eslintrc.json | 30 + libs/providers/unleash/README.md | 15 + libs/providers/unleash/babel.config.json | 3 + libs/providers/unleash/jest.config.ts | 9 + libs/providers/unleash/package-lock.json | 1259 +++++++++++++++++ libs/providers/unleash/package.json | 18 + libs/providers/unleash/project.json | 74 + libs/providers/unleash/src/index.ts | 1 + libs/providers/unleash/src/lib/test-logger.ts | 39 + libs/providers/unleash/src/lib/testdata.json | 96 ++ .../unleash/src/lib/unleash-provider.spec.ts | 156 ++ .../unleash/src/lib/unleash-provider.ts | 221 +++ libs/providers/unleash/tsconfig.json | 25 + libs/providers/unleash/tsconfig.lib.json | 10 + libs/providers/unleash/tsconfig.spec.json | 10 + release-please-config.json | 7 + tsconfig.base.json | 1 + 18 files changed, 1976 insertions(+), 1 deletion(-) create mode 100644 libs/providers/unleash/.eslintrc.json create mode 100644 libs/providers/unleash/README.md create mode 100644 libs/providers/unleash/babel.config.json create mode 100644 libs/providers/unleash/jest.config.ts create mode 100644 libs/providers/unleash/package-lock.json create mode 100644 libs/providers/unleash/package.json create mode 100644 libs/providers/unleash/project.json create mode 100644 libs/providers/unleash/src/index.ts create mode 100644 libs/providers/unleash/src/lib/test-logger.ts create mode 100644 libs/providers/unleash/src/lib/testdata.json create mode 100644 libs/providers/unleash/src/lib/unleash-provider.spec.ts create mode 100644 libs/providers/unleash/src/lib/unleash-provider.ts create mode 100644 libs/providers/unleash/tsconfig.json create mode 100644 libs/providers/unleash/tsconfig.lib.json create mode 100644 libs/providers/unleash/tsconfig.spec.json diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 25fc3aaa2..dd5961e5c 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -20,5 +20,6 @@ "libs/providers/config-cat-web": "0.1.5", "libs/shared/config-cat-core": "0.1.1", "libs/providers/unleash-web": "0.1.1", - "libs/providers/growthbook": "0.1.2" + "libs/providers/growthbook": "0.1.2", + "libs/providers/unleash": "0.1.0" } diff --git a/libs/providers/unleash/.eslintrc.json b/libs/providers/unleash/.eslintrc.json new file mode 100644 index 000000000..ca1930a13 --- /dev/null +++ b/libs/providers/unleash/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] + } + } + ] +} diff --git a/libs/providers/unleash/README.md b/libs/providers/unleash/README.md new file mode 100644 index 000000000..b1077d045 --- /dev/null +++ b/libs/providers/unleash/README.md @@ -0,0 +1,15 @@ +# unleash Provider + +## Installation + +``` +$ npm install @openfeature/unleash-provider +``` + +## Building + +Run `nx package providers-unleash` to build the library. + +## Running unit tests + +Run `nx test providers-unleash` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/providers/unleash/babel.config.json b/libs/providers/unleash/babel.config.json new file mode 100644 index 000000000..d7bf474d1 --- /dev/null +++ b/libs/providers/unleash/babel.config.json @@ -0,0 +1,3 @@ +{ + "presets": [["minify", { "builtIns": false }]] +} diff --git a/libs/providers/unleash/jest.config.ts b/libs/providers/unleash/jest.config.ts new file mode 100644 index 000000000..3027cbf6a --- /dev/null +++ b/libs/providers/unleash/jest.config.ts @@ -0,0 +1,9 @@ +export default { + displayName: 'providers-unleash', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/providers/unleash-web', +}; diff --git a/libs/providers/unleash/package-lock.json b/libs/providers/unleash/package-lock.json new file mode 100644 index 000000000..fafc958bc --- /dev/null +++ b/libs/providers/unleash/package-lock.json @@ -0,0 +1,1259 @@ +{ + "name": "@openfeature/unleash-provider", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openfeature/unleash-provider", + "version": "0.0.1", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@openfeature/server-sdk": "^1.17.0", + "unleash-client": "^6.6.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "peer": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "license": "ISC", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "license": "ISC", + "peer": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@openfeature/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@openfeature/core/-/core-1.7.2.tgz", + "integrity": "sha512-pbpREdO/8D6ocFYWIzAJ24iJ1td04Anl+TehUzx9r1xR8vKJab+O4TlC/XP7s9JuD5hRPBfmo5gDha/KWVSG4A==", + "license": "Apache-2.0", + "peer": true + }, + "node_modules/@openfeature/server-sdk": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@openfeature/server-sdk/-/server-sdk-1.17.1.tgz", + "integrity": "sha512-z5MaVvSNnk1SpRSuU02usnoX9rY9BtnLBNp9T08JOitwiuXs4byR8R5Es4WpsGRnnzBSoBQJL1iGIIYEeeEyxw==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@openfeature/core": "^1.7.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT", + "peer": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "peer": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT", + "peer": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT", + "peer": true + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT", + "peer": true + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "license": "MIT", + "peer": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "license": "MIT", + "peer": true + }, + "node_modules/launchdarkly-eventsource": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/launchdarkly-eventsource/-/launchdarkly-eventsource-2.0.3.tgz", + "integrity": "sha512-VhFjppK7jXlcEKaS7bxdoibB5j01NKyeDR7a8XfssdDGNWCTsbF0/5IExSmPi44eDncPhkoPNxlSZhEZvrbD5w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC", + "peer": true + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "license": "ISC", + "peer": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "peer": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "peer": true + }, + "node_modules/murmurhash3js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/murmurhash3js/-/murmurhash3js-3.0.1.tgz", + "integrity": "sha512-KL8QYUaxq7kUbcl0Yto51rMcYt7E/4N4BG3/c96Iqw1PQrTRspu8Cpx4TZ4Nunib1d4bEkIH3gjCYlP2RLBdow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0", + "peer": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "peer": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "peer": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT", + "peer": true + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "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", + "optional": true, + "peer": true + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", + "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", + "peer": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "peer": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "peer": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "license": "ISC", + "peer": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "license": "ISC", + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unleash-client": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/unleash-client/-/unleash-client-6.6.0.tgz", + "integrity": "sha512-xsxWobHY83d5n80aGVQVTXOVdxRw9vZxsXUeys5oucYRMzW3ymiy+QY0QBEWDORq0AffsP2suF5uj2M6On2qGw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "ip-address": "^9.0.5", + "launchdarkly-eventsource": "2.0.3", + "make-fetch-happen": "^13.0.1", + "murmurhash3js": "^3.0.1", + "proxy-from-env": "^1.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "peer": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "peer": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC", + "peer": true + } + } +} diff --git a/libs/providers/unleash/package.json b/libs/providers/unleash/package.json new file mode 100644 index 000000000..6a9733e7e --- /dev/null +++ b/libs/providers/unleash/package.json @@ -0,0 +1,18 @@ +{ + "name": "@openfeature/unleash-provider", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "main": "./src/index.js", + "typings": "./src/index.d.ts", + "scripts": { + "publish-if-not-exists": "cp $NPM_CONFIG_USERCONFIG .npmrc && if [ \"$(npm show $npm_package_name@$npm_package_version version)\" = \"$(npm run current-version -s)\" ]; then echo 'already published, skipping'; else npm publish --access public; fi", + "current-version": "echo $npm_package_version" + }, + "license": "Apache-2.0", + "peerDependencies": { + "@openfeature/server-sdk": "^1.17.0", + "unleash-client": "^6.6.0" + } +} diff --git a/libs/providers/unleash/project.json b/libs/providers/unleash/project.json new file mode 100644 index 000000000..a28682306 --- /dev/null +++ b/libs/providers/unleash/project.json @@ -0,0 +1,74 @@ +{ + "name": "providers-unleash", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/providers/unleash/src", + "projectType": "library", + "targets": { + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "npm run publish-if-not-exists", + "cwd": "dist/libs/providers/unleash" + }, + "dependsOn": [ + { + "target": "package" + } + ] + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"], + "dependsOn": [ + { + "target": "generate" + } + ] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/providers/unleash"], + "options": { + "jestConfig": "libs/providers/unleash/jest.config.ts" + }, + "dependsOn": [ + { + "target": "generate" + } + ] + }, + "package": { + "executor": "@nx/rollup:rollup", + "outputs": ["{options.outputPath}"], + "options": { + "project": "libs/providers/unleash/package.json", + "outputPath": "dist/libs/providers/unleash", + "entryFile": "libs/providers/unleash/src/index.ts", + "tsConfig": "libs/providers/unleash/tsconfig.lib.json", + "compiler": "tsc", + "generateExportsField": true, + "umdName": "unleash", + "external": "all", + "format": ["cjs", "esm"], + "assets": [ + { + "glob": "package.json", + "input": "./assets", + "output": "./src/" + }, + { + "glob": "LICENSE", + "input": "./", + "output": "./" + }, + { + "glob": "README.md", + "input": "./libs/providers/unleash", + "output": "./" + } + ] + } + } + }, + "tags": [] +} diff --git a/libs/providers/unleash/src/index.ts b/libs/providers/unleash/src/index.ts new file mode 100644 index 000000000..1e41fe05d --- /dev/null +++ b/libs/providers/unleash/src/index.ts @@ -0,0 +1 @@ +export * from './lib/unleash-provider'; diff --git a/libs/providers/unleash/src/lib/test-logger.ts b/libs/providers/unleash/src/lib/test-logger.ts new file mode 100644 index 000000000..2ae8efd19 --- /dev/null +++ b/libs/providers/unleash/src/lib/test-logger.ts @@ -0,0 +1,39 @@ +/** + * TestLogger is a logger build for testing purposes. + * This is not ready to be production ready, so please avoid using it. + */ +export default class TestLogger { + public inMemoryLogger: Record = { + error: [], + warn: [], + info: [], + debug: [], + }; + + error(...args: unknown[]): void { + this.inMemoryLogger['error'].push(args.join(' ')); + } + + warn(...args: unknown[]): void { + this.inMemoryLogger['warn'].push(args.join(' ')); + } + + info(...args: unknown[]): void { + console.log(args); + this.inMemoryLogger['info'].push(args.join(' ')); + } + + debug(...args: unknown[]): void { + console.log(args); + this.inMemoryLogger['debug'].push(args.join(' ')); + } + + reset() { + this.inMemoryLogger = { + error: [], + warn: [], + info: [], + debug: [], + }; + } +} diff --git a/libs/providers/unleash/src/lib/testdata.json b/libs/providers/unleash/src/lib/testdata.json new file mode 100644 index 000000000..de4d1aab3 --- /dev/null +++ b/libs/providers/unleash/src/lib/testdata.json @@ -0,0 +1,96 @@ +{ + "features": [ + { + "name": "simpleToggle", + "enabled": true, + "impressionData": true, + "strategies": [] + }, + { + "name": "disabledToggle", + "enabled": false, + "impressionData": true, + "strategies": [] + }, + { + "name": "variantToggleString", + "enabled": true, + "impressionData": true, + "strategies": [], + "variants": [ + { + "name": "string", + "weight": 1, + "payload": { + "type": "string", + "value": "some-text" + } + } + ] + }, + { + "name": "variantToggleJson", + "enabled": true, + "impressionData": true, + "strategies": [], + "variants": [ + { + "name": "json", + "weight": 1, + "payload": { + "type": "json", + "value": "{hello: world}" + } + } + ] + }, + { + "name": "variantToggleCsv", + "enabled": true, + "strategies": [], + "variants": [ + { + "name": "csv", + "weight": 1, + "payload": { + "type": "csv", + "value": "1,2,3,4" + } + } + ], + "impressionData": false + }, + { + "name": "variantToggleInteger", + "enabled": true, + "strategies": [], + "variants": [ + { + "name": "number", + "weight": 1, + "payload": { + "type": "number", + "value": "3" + } + } + ], + "impressionData": false + }, + { + "name": "variantToggleDouble", + "enabled": true, + "strategies": [], + "variants": [ + { + "name": "number", + "weight": 1, + "payload": { + "type": "number", + "value": "1.2" + } + } + ], + "impressionData": false + } + ] +} diff --git a/libs/providers/unleash/src/lib/unleash-provider.spec.ts b/libs/providers/unleash/src/lib/unleash-provider.spec.ts new file mode 100644 index 000000000..57293e3c5 --- /dev/null +++ b/libs/providers/unleash/src/lib/unleash-provider.spec.ts @@ -0,0 +1,156 @@ +import { UnleashProvider } from './unleash-provider'; +import { TypeMismatchError } from '@openfeature/server-sdk'; +import testdata from './testdata.json'; +import TestLogger from './test-logger'; + +const endpoint = 'http://localhost:4242/api/'; +const logger = new TestLogger(); +const valueProperty = 'value'; + +jest.mock('make-fetch-happen', () => + jest.fn(async (url, options) => { + console.log(`Intercepted fetch to: ${url}`, options); // Debugging output + + return { + status: 200, + statusCode: 200, + ok: true, + json: async () => testdata, + headers: { + get: jest.fn(() => 'application/json'), // Mock header retrieval + }, + }; + }), +); + +describe('UnleashProvider', () => { + let provider: UnleashProvider; + + it('should be an instance of UnleashProvider', async () => { + provider = new UnleashProvider( + { + url: endpoint, + appName: 'test', + customHeaders: { Authorization: 'test' }, + disableMetrics: true, + }, + logger, + ); + await provider.initialize(); + expect(provider).toBeInstanceOf(UnleashProvider); + }); +}); + +describe('UnleashProvider evaluations', () => { + let provider: UnleashProvider; + + beforeAll(async () => { + provider = new UnleashProvider( + { + url: endpoint, + appName: 'test', + customHeaders: { Authorization: 'test' }, + disableMetrics: true, + }, + logger, + ); + await provider.initialize(); + }); + + describe('method resolveBooleanEvaluation', () => { + it('should return false for missing toggle', async () => { + const evaluation = await provider.resolveBooleanEvaluation('nonExistent'); + expect(evaluation).toHaveProperty(valueProperty, false); + }); + + it('should return true if enabled toggle exists', async () => { + const evaluation = await provider.resolveBooleanEvaluation('simpleToggle'); + expect(evaluation).toHaveProperty(valueProperty, true); + }); + + it('should return false if a disabled toggle exists', async () => { + const evaluation = await provider.resolveBooleanEvaluation('disabledToggle'); + expect(evaluation).toHaveProperty(valueProperty, false); + }); + }); + + describe('method resolveStringEvaluation', () => { + it('should return default value for missing value', async () => { + const evaluation = await provider.resolveStringEvaluation('nonExistent', 'defaultValue'); + expect(evaluation).toHaveProperty(valueProperty, 'defaultValue'); + }); + + it('should return right value if variant toggle exists and is enabled', async () => { + const evaluation = await provider.resolveStringEvaluation('variantToggleString', 'variant1'); + expect(evaluation).toHaveProperty(valueProperty, 'some-text'); + }); + + it('should return default value if a toggle is disabled', async () => { + const evaluation = await provider.resolveStringEvaluation('disabledVariant', 'defaultValue'); + expect(evaluation).toHaveProperty(valueProperty, 'defaultValue'); + }); + + it('should throw TypeMismatchError if requested variant type is not a string', async () => { + await expect(provider.resolveStringEvaluation('variantToggleJson', 'default string')).rejects.toThrow( + TypeMismatchError, + ); + }); + }); + + describe('method resolveNumberEvaluation', () => { + it('should return default value for missing value', async () => { + const evaluation = await provider.resolveNumberEvaluation('nonExistent', 5); + expect(evaluation).toHaveProperty(valueProperty, 5); + }); + + it('should return integer value if variant toggle exists and is enabled', async () => { + const evaluation = await provider.resolveNumberEvaluation('variantToggleInteger', 0); + expect(evaluation).toHaveProperty(valueProperty, 3); + }); + + it('should return double value if variant toggle exists and is enabled', async () => { + const evaluation = await provider.resolveNumberEvaluation('variantToggleDouble', 0); + expect(evaluation).toHaveProperty(valueProperty, 1.2); + }); + + it('should return default value if a toggle is disabled', async () => { + const evaluation = await provider.resolveNumberEvaluation('disabledVariant', 0); + expect(evaluation).toHaveProperty(valueProperty, 0); + }); + + it('should throw TypeMismatchError if requested variant type is not a number', async () => { + await expect(provider.resolveNumberEvaluation('variantToggleCsv', 0)).rejects.toThrow(TypeMismatchError); + }); + }); + + describe('method resolveObjectEvaluation', () => { + it('should return default value for missing value', async () => { + const defaultValue = '{"notFound" : true}'; + const evaluation = await provider.resolveObjectEvaluation('nonExistent', JSON.parse(defaultValue)); + expect(evaluation).toHaveProperty(valueProperty, JSON.parse(defaultValue)); + }); + + it('should return json value if variant toggle exists and is enabled', async () => { + const expectedVariant = '{hello: world}'; + const evaluation = await provider.resolveObjectEvaluation('variantToggleJson', JSON.parse('{"default": false}')); + expect(evaluation).toHaveProperty(valueProperty, expectedVariant); + }); + + it('should return csv value if variant toggle exists and is enabled', async () => { + const evaluation = await provider.resolveObjectEvaluation('variantToggleCsv', 'a,b,c,d'); + expect(evaluation).toHaveProperty(valueProperty, '1,2,3,4'); + }); + + it('should return default value if a toggle is disabled', async () => { + const defaultValue = '{foo: bar}'; + const evaluation = await provider.resolveObjectEvaluation('disabledVariant', defaultValue); + expect(evaluation).toHaveProperty(valueProperty, defaultValue); + }); + + it('should throw TypeMismatchError if requested variant type is not json or csv', async () => { + await expect(provider.resolveObjectEvaluation('variantToggleInteger', 'a,b,c,d')).rejects.toThrow( + TypeMismatchError, + ); + }); + }); +}); diff --git a/libs/providers/unleash/src/lib/unleash-provider.ts b/libs/providers/unleash/src/lib/unleash-provider.ts new file mode 100644 index 000000000..4bf9683a7 --- /dev/null +++ b/libs/providers/unleash/src/lib/unleash-provider.ts @@ -0,0 +1,221 @@ +import { + EvaluationContext, + Logger, + Provider, + JsonValue, + ResolutionDetails, + OpenFeatureEventEmitter, + ProviderFatalError, + ProviderEvents, + FlagNotFoundError, + TypeMismatchError, +} from '@openfeature/server-sdk'; +import { Unleash, initialize, UnleashConfig, Context } from 'unleash-client'; + +export class UnleashProvider implements Provider { + metadata = { + name: UnleashProvider.name, + }; + + public readonly events = new OpenFeatureEventEmitter(); + + private _logger?: Logger; + + // config is the Unleash config provided to the provider + private _config?: UnleashConfig; + + // client is the Unleash client reference + private _client?: Unleash; + + private _context?: Context; + + readonly runsOn = 'server'; + + hooks = []; + + constructor(config: UnleashConfig, logger?: Logger) { + this._config = config; + this._logger = logger; + this._client = initialize(config); + } + + public get unleashClient() { + return this._client; + } + + async initialize(): Promise { + await this.initializeClient(); + this._logger?.debug('UnleashProvider initialized'); + } + + private async initializeClient() { + try { + this._logger?.debug('Initializing Unleash Client'); + this.registerEventListeners(); + await this._client?.start(); + } catch (e) { + throw new ProviderFatalError(getErrorMessage(e)); + } + } + + private registerEventListeners() { + this._client?.on('synchronized', () => { + this._logger?.debug('Unleash ready event received'); + this.events.emit(ProviderEvents.Ready, { + message: 'Ready', + }); + }); + this._client?.on('ready', () => { + this._logger?.debug('Unleash ready event received'); + this.events.emit(ProviderEvents.Ready, { + message: 'Ready', + }); + }); + this._client?.on('update', () => { + this._logger?.debug('Unleash update event received'); + this.events.emit(ProviderEvents.ConfigurationChanged, { + message: 'Flags changed', + }); + }); + this._client?.on('error', (err) => { + this._logger?.debug('Unleash error event received', err); + this.events.emit(ProviderEvents.Error, { + message: 'Error', + }); + }); + this._client?.on('recovered', () => { + this._logger?.debug('Unleash recovered event received'); + this.events.emit(ProviderEvents.Ready, { + message: 'Recovered', + }); + }); + } + + async onContextChange(_oldContext: EvaluationContext, newContext: EvaluationContext): Promise { + const unleashContext = new Map(); + const properties = new Map(); + Object.keys(newContext).forEach((key) => { + switch (key) { + case 'appName': + case 'userId': + case 'environment': + case 'remoteAddress': + case 'sessionId': + case 'currentTime': + unleashContext.set(key, newContext[key]); + break; + default: + properties.set(key, newContext[key]); + break; + } + }); + if (properties.size > 0) { + unleashContext.set('properties', Object.fromEntries(properties)); + } + this._context = Object.fromEntries(unleashContext); + this._logger?.debug('Unleash context updated'); + } + + async onClose() { + this._logger?.debug('closing Unleash client'); + this._client?.destroy(); + } + + async resolveBooleanEvaluation( + flagKey: string, + defaultValue?: boolean, + context?: EvaluationContext, + ): Promise> { + const ctx = context as unknown as Context; + const resp = this._client?.isEnabled(flagKey, ctx, defaultValue); + if (typeof resp === 'undefined') { + throw new FlagNotFoundError(); + } + return { + value: resp, + }; + } + + async resolveStringEvaluation( + flagKey: string, + defaultValue?: string, + context?: EvaluationContext, + ): Promise> { + const ctx = context as unknown as Context; + return this.evaluate(flagKey, defaultValue, ctx, 'string'); + } + + async resolveNumberEvaluation( + flagKey: string, + defaultValue?: number, + context?: EvaluationContext, + ): Promise> { + const ctx = context as unknown as Context; + return this.evaluate(flagKey, defaultValue, ctx, 'number'); + } + + async resolveObjectEvaluation( + flagKey: string, + defaultValue?: U, + context?: EvaluationContext, + ): Promise> { + const ctx = context as unknown as Context; + return this.evaluate(flagKey, defaultValue, ctx, 'object'); + } + + private throwTypeMismatchError(variant: string, variantType: string, flagType: string) { + throw new TypeMismatchError( + `Type of requested variant ${variant} is of type ${variantType} but requested flag type of ${flagType}`, + ); + } + + private evaluate( + flagKey: string, + defaultValue: T | undefined, + context: Context, + flagType: string, + ): ResolutionDetails { + const evaluatedVariant = this._client?.getVariant(flagKey, context); + let value; + let variant; + if (typeof evaluatedVariant === 'undefined') { + throw new FlagNotFoundError(); + } + + if (evaluatedVariant.name === 'disabled' || typeof evaluatedVariant.payload === 'undefined') { + value = defaultValue; + } else { + variant = evaluatedVariant.name; + value = evaluatedVariant.payload?.value; + + const variantType = evaluatedVariant.payload?.type; + + if (flagType === 'string' && flagType !== variantType) { + this.throwTypeMismatchError(variant, variantType, flagType); + } + if (flagType === 'number') { + const numberValue = parseFloat(value); + if (flagType !== variantType || isNaN(numberValue)) { + this.throwTypeMismatchError(variant, variantType, flagType); + } + value = numberValue; + } + if (flagType === 'object') { + if (variantType !== 'json' && variantType !== 'csv') { + this.throwTypeMismatchError(variant, variantType, flagType); + } + } + } + return { + variant: variant, + value: value as T, + }; + } +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + return String(error); +} diff --git a/libs/providers/unleash/tsconfig.json b/libs/providers/unleash/tsconfig.json new file mode 100644 index 000000000..6f3d6b125 --- /dev/null +++ b/libs/providers/unleash/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "ES6", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/providers/unleash/tsconfig.lib.json b/libs/providers/unleash/tsconfig.lib.json new file mode 100644 index 000000000..4befa7f09 --- /dev/null +++ b/libs/providers/unleash/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/providers/unleash/tsconfig.spec.json b/libs/providers/unleash/tsconfig.spec.json new file mode 100644 index 000000000..f578bd775 --- /dev/null +++ b/libs/providers/unleash/tsconfig.spec.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/release-please-config.json b/release-please-config.json index 53696b0a8..e30a767ca 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -155,6 +155,13 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "versioning": "default" + }, + "libs/providers/unleash": { + "release-type": "node", + "prerelease": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "versioning": "default" } }, "changelog-sections": [ diff --git a/tsconfig.base.json b/tsconfig.base.json index 01cdcc50e..93cbb7799 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -37,6 +37,7 @@ "@openfeature/ofrep-core": ["libs/shared/ofrep-core/src/index.ts"], "@openfeature/ofrep-provider": ["libs/providers/ofrep/src/index.ts"], "@openfeature/ofrep-web-provider": ["libs/providers/ofrep-web/src/index.ts"], + "@openfeature/unleash-provider": ["libs/providers/unleash/src/index.ts"], "@openfeature/unleash-web-provider": ["libs/providers/unleash-web/src/index.ts"] } }, From ce784b19e6a672ded8432d17f70329f12e48f06a Mon Sep 17 00:00:00 2001 From: Albert Ilagan Date: Tue, 18 Mar 2025 00:08:27 +0800 Subject: [PATCH 2/4] chore: add readme Signed-off-by: Albert Ilagan --- .github/component_owners.yml | 2 + libs/providers/unleash/README.md | 72 ++++++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 4a129f58c..2ea2fae0b 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -30,6 +30,8 @@ components: - markphelps libs/providers/flipt-web: - markphelps + libs/providers/unleash: + - albertilagan libs/providers/unleash-web: - jarebudev diff --git a/libs/providers/unleash/README.md b/libs/providers/unleash/README.md index b1077d045..b36f09d82 100644 --- a/libs/providers/unleash/README.md +++ b/libs/providers/unleash/README.md @@ -1,15 +1,81 @@ # unleash Provider +## About this provider + +This provider is a community-developed implementation for Unleash which uses the official [Node Server-side SDK](https://docs.getunleash.io/reference/sdks/node). + +### Concepts + +- Boolean evaluation gets feature enabled status. +- String, Number, and Object evaluation gets feature variant value. +- Object evaluation should be used for JSON/CSV payloads in variants. + ## Installation +```shell +$ npm install @openfeature/unleash-provider @openfeature/server-sdk ``` -$ npm install @openfeature/unleash-provider + +## Usage + +To initialize the OpenFeature client with Unleash, you can use the following code snippets: + +### Initialization + +```ts +import { UnleashProvider } from '@openfeature/unleash-provider'; + +const provider = new UnleashProvider({ + url: 'https://YOUR-API-URL', + appName: 'your app', + customHeaders: { Authorization: 'your api key' }, +}); + +await OpenFeature.setProviderAndWait(provider); ``` -## Building +### Available Constructor Configuration Options + +Unleash has a variety of configuration options that can be provided to the `UnleashProvider` constructor. + +Please refer to the options described in the official [Node Server-side SDK](https://docs.getunleash.io/reference/sdks/node). + +### After initialization + +After the provider gets initialized, you can start evaluations of feature flags like so: + +```ts +// Get the client +const client = await OpenFeature.getClient(); + +// You can now use the client to evaluate your flags +const details = client.getBooleanValue('my-feature', false); +``` + +The static evaluation context can be changed if needed + +```ts +const evaluationCtx: EvaluationContext = { + usedId: 'theuser', + currentTime: 'time', + sessionId: 'theSessionId', + remoteAddress: 'theRemoteAddress', + environment: 'theEnvironment', + appName: 'theAppName', + aCustomProperty: 'itsValue', + anotherCustomProperty: 'somethingForIt', +}; + +// changes the static evaluation context for OpenFeature +await OpenFeature.setContext(evaluationCtx); +``` + +## Contribute + +### Building Run `nx package providers-unleash` to build the library. -## Running unit tests +### Running unit tests Run `nx test providers-unleash` to execute the unit tests via [Jest](https://jestjs.io). From 152bc9b0260c5c525680e598d848d2b35bf1ea4d Mon Sep 17 00:00:00 2001 From: Albert Ilagan Date: Tue, 18 Mar 2025 03:14:56 +0800 Subject: [PATCH 3/4] fix: adddress comments Signed-off-by: Albert Ilagan --- libs/providers/unleash/README.md | 14 ++++---------- libs/providers/unleash/src/lib/unleash-provider.ts | 12 ------------ 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/libs/providers/unleash/README.md b/libs/providers/unleash/README.md index b36f09d82..59b083abf 100644 --- a/libs/providers/unleash/README.md +++ b/libs/providers/unleash/README.md @@ -46,28 +46,22 @@ After the provider gets initialized, you can start evaluations of feature flags ```ts // Get the client -const client = await OpenFeature.getClient(); +const client = OpenFeature.getClient(); // You can now use the client to evaluate your flags -const details = client.getBooleanValue('my-feature', false); +const details = await client.getBooleanValue('my-feature', false); ``` -The static evaluation context can be changed if needed +In server usage, the evaluation context changes frequently, as often as every evaluation. Though some context data may remain static for the entire application lifecycle (such as the hostname or application version) much of it may be dynamic ```ts const evaluationCtx: EvaluationContext = { - usedId: 'theuser', - currentTime: 'time', - sessionId: 'theSessionId', - remoteAddress: 'theRemoteAddress', environment: 'theEnvironment', appName: 'theAppName', - aCustomProperty: 'itsValue', - anotherCustomProperty: 'somethingForIt', }; // changes the static evaluation context for OpenFeature -await OpenFeature.setContext(evaluationCtx); +OpenFeature.setContext(evaluationCtx); ``` ## Contribute diff --git a/libs/providers/unleash/src/lib/unleash-provider.ts b/libs/providers/unleash/src/lib/unleash-provider.ts index 4bf9683a7..a994fdf38 100644 --- a/libs/providers/unleash/src/lib/unleash-provider.ts +++ b/libs/providers/unleash/src/lib/unleash-provider.ts @@ -59,18 +59,6 @@ export class UnleashProvider implements Provider { } private registerEventListeners() { - this._client?.on('synchronized', () => { - this._logger?.debug('Unleash ready event received'); - this.events.emit(ProviderEvents.Ready, { - message: 'Ready', - }); - }); - this._client?.on('ready', () => { - this._logger?.debug('Unleash ready event received'); - this.events.emit(ProviderEvents.Ready, { - message: 'Ready', - }); - }); this._client?.on('update', () => { this._logger?.debug('Unleash update event received'); this.events.emit(ProviderEvents.ConfigurationChanged, { From 60981e5728be6bd8af7c6aa2914779b038b398a2 Mon Sep 17 00:00:00 2001 From: Albert Ilagan Date: Tue, 18 Mar 2025 03:24:55 +0800 Subject: [PATCH 4/4] fix: add missing package Signed-off-by: Albert Ilagan --- libs/providers/unleash/package-lock.json | 1 + libs/providers/unleash/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/libs/providers/unleash/package-lock.json b/libs/providers/unleash/package-lock.json index fafc958bc..fb25d20ff 100644 --- a/libs/providers/unleash/package-lock.json +++ b/libs/providers/unleash/package-lock.json @@ -13,6 +13,7 @@ }, "peerDependencies": { "@openfeature/server-sdk": "^1.17.0", + "make-fetch-happen": "^13.0.1", "unleash-client": "^6.6.0" } }, diff --git a/libs/providers/unleash/package.json b/libs/providers/unleash/package.json index 6a9733e7e..062dbafbc 100644 --- a/libs/providers/unleash/package.json +++ b/libs/providers/unleash/package.json @@ -13,6 +13,7 @@ "license": "Apache-2.0", "peerDependencies": { "@openfeature/server-sdk": "^1.17.0", + "make-fetch-happen": "^13.0.1", "unleash-client": "^6.6.0" } }