├── .github ├── funding.yml └── workflows │ └── npm-publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── cjs │ ├── index.d.ts │ └── index.js └── esm │ ├── index.d.ts │ └── index.js ├── package-lock.json ├── package.json ├── src └── index.ts ├── test └── test.js └── tsconfig.json /.github/funding.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: faisalman 4 | patreon: # Replace with a single Patreon username 5 | open_collective: ua-parser-js 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://www.paypal.me/faisalman/'] 13 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Publish Package to npmjs 5 | on: 6 | push: 7 | tags: 8 | - '*' 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | id-token: write 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: '18.x' 20 | registry-url: 'https://registry.npmjs.org' 21 | - run: npm install -g npm 22 | - run: npm ci 23 | - run: npm publish --provenance --access public 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | 93 | # Gatsby files 94 | .cache/ 95 | # Comment in the public line in if your project uses Gatsby and not Next.js 96 | # https://nextjs.org/blog/next-9-1#public-directory-support 97 | # public 98 | 99 | # vuepress build output 100 | .vuepress/dist 101 | 102 | # vuepress v2.x temp and cache directory 103 | .temp 104 | .cache 105 | 106 | # Docusaurus cache and generated files 107 | .docusaurus 108 | 109 | # Serverless directories 110 | .serverless/ 111 | 112 | # FuseBox cache 113 | .fusebox/ 114 | 115 | # DynamoDB Local files 116 | .dynamodb/ 117 | 118 | # TernJS port file 119 | .tern-port 120 | 121 | # Stores VSCode versions used for testing VSCode extensions 122 | .vscode-test 123 | 124 | # yarn v2 125 | .yarn/cache 126 | .yarn/unplugged 127 | .yarn/build-state.yml 128 | .yarn/install-state.gz 129 | .pnp.* 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Faisal Salman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UAClientHints.js 2 | Parse & serialize user-agent client hints (UA-CH) HTTP headers 3 | 4 | ```sh 5 | npm i ua-client-hints-js 6 | ``` 7 | 8 | ## Methods 9 | 10 | ```js 11 | setValuesFromHeaders(headers:object): UAClientHints 12 | ``` 13 | 14 | ```js 15 | setValuesFromUAParser(iresult:object): UAClientHints 16 | ``` 17 | 18 | ```js 19 | setValues(data:object): UAClientHints 20 | ``` 21 | 22 | ```js 23 | getValues([fields:string[]]): object 24 | ``` 25 | 26 | ```js 27 | getValuesAsHeaders([fields:string[]]): object 28 | ``` 29 | 30 | ## Code Example 31 | 32 | ### From HTTP Headers 33 | 34 | ```js 35 | import { UAClientHints } from 'ua-client-hints-js'; 36 | 37 | /* 38 | Suppose we're in a server having this client hints data: 39 | 40 | const req = { 41 | headers : { 42 | 'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"', 43 | 'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"', 44 | 'sec-ch-ua-arch' : '"arm"', 45 | 'sec-ch-ua-bitness' : '"64"', 46 | 'sec-ch-ua-mobile' : '?1', 47 | 'sec-ch-ua-model' : '"Pixel 99"', 48 | 'sec-ch-ua-platform' : '"Linux"', 49 | 'sec-ch-ua-platform-version' : '"13"' 50 | }; 51 | */ 52 | 53 | const ch = new UAClientHints(); 54 | ch.setValuesFromHeaders(req.headers); 55 | 56 | const chData1 = ch.getValues(['architecture', 'bitness', 'mobile']); 57 | console.log(chData1); 58 | /* 59 | { 60 | "architecture": "arm", 61 | "bitness": "64", 62 | "mobile": true 63 | } 64 | */ 65 | 66 | const chData2 = ch.getValues(); 67 | console.log(chData2); 68 | /* 69 | { 70 | "architecture": "arm", 71 | "bitness": "64", 72 | "brands": [ 73 | { 74 | "brand": "Chromium", 75 | "version": "93" 76 | }, 77 | { 78 | "brand": "Google Chrome", 79 | "version": "93" 80 | }, 81 | { 82 | "brand": " Not;A Brand", 83 | "version": "99" 84 | } 85 | ], 86 | "fullVersionList": [ 87 | { 88 | "brand": "Chromium", 89 | "version": "93.0.1.2" 90 | }, 91 | { 92 | "brand": "Google Chrome", 93 | "version": "93.0.1.2" 94 | }, 95 | { 96 | "brand": " Not;A Brand", 97 | "version": "99.0.1.2" 98 | } 99 | ], 100 | "mobile": true, 101 | "model": "Pixel 99", 102 | "platform": "Linux", 103 | "platformVersion": "13", 104 | "wow64": null, 105 | "formFactor": null 106 | } 107 | */ 108 | 109 | ch.setValues({ 110 | 'wow64' : true, 111 | 'formFactor' : 'Automotive' 112 | }); 113 | 114 | const headersData1 = ch.getValuesAsHeaders(); 115 | console.log(headersData1); 116 | /* 117 | { 118 | 'Sec-CH-UA' : '"Chromium"; v="93", "Google Chrome"; v="93", " Not;A Brand"; v="99"', 119 | 'Sec-CH-UA-Full-Version-List' : '"Chromium"; v="93.0.1.2", "Google Chrome"; v="93.0.1.2", " Not;A Brand"; v="99.0.1.2"', 120 | 'Sec-CH-UA-Arch' : '"arm"', 121 | 'Sec-CH-UA-Bitness' : '"64"', 122 | 'Sec-CH-UA-Mobile' : '?1', 123 | 'Sec-CH-UA-Model' : '"Pixel 99"', 124 | 'Sec-CH-UA-Platform' : '"Linux"', 125 | 'Sec-CH-UA-Platform-Version' : '"13"', 126 | 'Sec-CH-UA-WOW64' : '?1', 127 | 'Sec-CH-UA-Form-Factor' : '"Automotive"' 128 | }; 129 | */ 130 | 131 | const headersData2 = ch.getValuesAsHeaders(['brand', 'mobile', 'model']); 132 | console.log(headersData2); 133 | /* 134 | { 135 | 'Sec-CH-UA' : '"Chromium"; v="93", "Google Chrome"; v="93", " Not;A Brand"; v="99"', 136 | 'Sec-CH-UA-Mobile' : '?1', 137 | 'Sec-CH-UA-Model' : '"Pixel 99"' 138 | }; 139 | */ 140 | ``` 141 | 142 | ### From [UAParser.js](https://github.com/faisalman/ua-parser-js) 143 | 144 | ```js 145 | import { UAClientHints } from 'ua-client-hints-js'; 146 | import { UAParser } from 'ua-parser-js'; 147 | 148 | const ua = 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537'; 149 | const uap = new UAParser(ua).getResult(); 150 | 151 | const ch = new UAClientHints(); 152 | ch.setValuesFromUAParser(uap); 153 | 154 | const chData = ch.getValues(); 155 | console.log(chData); 156 | /* 157 | { 158 | architecture: null, 159 | bitness: null, 160 | brands: [ 161 | { 162 | brand: 'IEMobile', 163 | version: '11.0' 164 | } 165 | ], 166 | formFactor: ['Mobile'], 167 | fullVersionList: [ 168 | { 169 | brand: 'IEMobile', 170 | version: '11.0' 171 | } 172 | ], 173 | mobile: true, 174 | model: 'Lumia 635', 175 | platform: 'Windows Phone', 176 | platformVersion: '8.1', 177 | wow64: null 178 | }; 179 | */ 180 | 181 | const chHeaders = ch.getValuesAsHeaders(); 182 | console.log(chHeaders); 183 | /* 184 | { 185 | 'Sec-CH-UA-Arch': '', 186 | 'Sec-CH-UA-Bitness': '', 187 | 'Sec-CH-UA': '"IEMobile"; v="11.0"', 188 | 'Sec-CH-UA-Form-Factor': '"Mobile"', 189 | 'Sec-CH-UA-Full-Version-List': '"IEMobile"; v="11.0"', 190 | 'Sec-CH-UA-Mobile': '?1', 191 | 'Sec-CH-UA-Model': '"Lumia 635"', 192 | 'Sec-CH-UA-Platform': '"Windows Phone"', 193 | 'Sec-CH-UA-Platform-Version': '"8.1"', 194 | 'Sec-CH-UA-WOW64': '' 195 | } 196 | */ 197 | ``` 198 | 199 | # License 200 | 201 | MIT License 202 | 203 | Copyright (c) 2023 Faisal Salman <> 204 | 205 | Permission is hereby granted, free of charge, to any person obtaining a copy 206 | of this software and associated documentation files (the "Software"), to deal 207 | in the Software without restriction, including without limitation the rights 208 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 209 | copies of the Software, and to permit persons to whom the Software is 210 | furnished to do so, subject to the following conditions: 211 | 212 | The above copyright notice and this permission notice shall be included in all 213 | copies or substantial portions of the Software. 214 | 215 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 216 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 217 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 218 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 219 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 220 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 221 | SOFTWARE. -------------------------------------------------------------------------------- /dist/cjs/index.d.ts: -------------------------------------------------------------------------------- 1 | /*! UAClientHints.js 0.1.2 2 | Parse & serialize user-agent client hints (UA-CH) HTTP headers 3 | https://github.com/faisalman/ua-client-hints-js 4 | Author: Faisal Salman 5 | MIT License */ 6 | /// 7 | /// 8 | export declare enum FIELD_TYPE { 9 | Boolean = "sf-boolean", 10 | List = "sf-list", 11 | String = "sf-string" 12 | } 13 | export declare const UACH_MAP: { 14 | readonly architecture: { 15 | readonly field: "Sec-CH-UA-Arch"; 16 | readonly type: FIELD_TYPE.String; 17 | }; 18 | readonly bitness: { 19 | readonly field: "Sec-CH-UA-Bitness"; 20 | readonly type: FIELD_TYPE.String; 21 | }; 22 | readonly brands: { 23 | readonly field: "Sec-CH-UA"; 24 | readonly type: FIELD_TYPE.List; 25 | }; 26 | readonly formFactor: { 27 | readonly field: "Sec-CH-UA-Form-Factor"; 28 | readonly type: FIELD_TYPE.String; 29 | }; 30 | readonly fullVersionList: { 31 | readonly field: "Sec-CH-UA-Full-Version-List"; 32 | readonly type: FIELD_TYPE.List; 33 | }; 34 | readonly mobile: { 35 | readonly field: "Sec-CH-UA-Mobile"; 36 | readonly type: FIELD_TYPE.Boolean; 37 | }; 38 | readonly model: { 39 | readonly field: "Sec-CH-UA-Model"; 40 | readonly type: FIELD_TYPE.String; 41 | }; 42 | readonly platform: { 43 | readonly field: "Sec-CH-UA-Platform"; 44 | readonly type: FIELD_TYPE.String; 45 | }; 46 | readonly platformVersion: { 47 | readonly field: "Sec-CH-UA-Platform-Version"; 48 | readonly type: FIELD_TYPE.String; 49 | }; 50 | readonly wow64: { 51 | readonly field: "Sec-CH-UA-WOW64"; 52 | readonly type: FIELD_TYPE.Boolean; 53 | }; 54 | }; 55 | export type UACHDataType = boolean | string | string[] | NavigatorUABrandVersion[] | null | undefined; 56 | export type UACHDataField = keyof typeof UACH_MAP; 57 | export type UACHHeaderType = typeof FIELD_TYPE[keyof typeof FIELD_TYPE]; 58 | export type UACHHeaderField = Lowercase; 59 | export declare class UAClientHints { 60 | private architecture?; 61 | private bitness?; 62 | private brands?; 63 | private formFactor?; 64 | private fullVersionList?; 65 | private mobile?; 66 | private model?; 67 | private platform?; 68 | private platformVersion?; 69 | private wow64?; 70 | getValues(fields?: UACHDataField[]): UADataValues; 71 | getValuesAsHeaders(fields?: UACHDataField[]): Partial>; 72 | setValues(values?: UADataValues): UAClientHints; 73 | setValuesFromUAParser(uap: UAParser.IResult): UAClientHints; 74 | setValuesFromHeaders(headers: Record): UAClientHints; 75 | private parseHeader; 76 | private serializeHeader; 77 | private isValidType; 78 | } 79 | -------------------------------------------------------------------------------- /dist/cjs/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /*! UAClientHints.js 0.1.2 3 | Parse & serialize user-agent client hints (UA-CH) HTTP headers 4 | https://github.com/faisalman/ua-client-hints-js 5 | Author: Faisal Salman 6 | MIT License */ 7 | Object.defineProperty(exports, "__esModule", { value: true }); 8 | exports.UAClientHints = exports.UACH_MAP = exports.FIELD_TYPE = void 0; 9 | var FIELD_TYPE; 10 | (function (FIELD_TYPE) { 11 | FIELD_TYPE["Boolean"] = "sf-boolean"; 12 | FIELD_TYPE["List"] = "sf-list"; 13 | FIELD_TYPE["String"] = "sf-string"; 14 | })(FIELD_TYPE || (exports.FIELD_TYPE = FIELD_TYPE = {})); 15 | ; 16 | exports.UACH_MAP = { 17 | architecture: { 18 | field: 'Sec-CH-UA-Arch', 19 | type: FIELD_TYPE.String 20 | }, 21 | bitness: { 22 | field: 'Sec-CH-UA-Bitness', 23 | type: FIELD_TYPE.String 24 | }, 25 | brands: { 26 | field: 'Sec-CH-UA', 27 | type: FIELD_TYPE.List 28 | }, 29 | formFactor: { 30 | field: 'Sec-CH-UA-Form-Factor', 31 | type: FIELD_TYPE.String 32 | }, 33 | fullVersionList: { 34 | field: 'Sec-CH-UA-Full-Version-List', 35 | type: FIELD_TYPE.List 36 | }, 37 | mobile: { 38 | field: 'Sec-CH-UA-Mobile', 39 | type: FIELD_TYPE.Boolean 40 | }, 41 | model: { 42 | field: 'Sec-CH-UA-Model', 43 | type: FIELD_TYPE.String 44 | }, 45 | platform: { 46 | field: 'Sec-CH-UA-Platform', 47 | type: FIELD_TYPE.String 48 | }, 49 | platformVersion: { 50 | field: 'Sec-CH-UA-Platform-Version', 51 | type: FIELD_TYPE.String 52 | }, 53 | wow64: { 54 | field: 'Sec-CH-UA-WOW64', 55 | type: FIELD_TYPE.Boolean 56 | } 57 | }; 58 | class UAClientHints { 59 | constructor() { 60 | this.architecture = undefined; 61 | this.bitness = undefined; 62 | this.brands = undefined; 63 | this.formFactor = undefined; 64 | this.fullVersionList = undefined; 65 | this.mobile = undefined; 66 | this.model = undefined; 67 | this.platform = undefined; 68 | this.platformVersion = undefined; 69 | this.wow64 = undefined; 70 | } 71 | getValues(fields) { 72 | let values = {}; 73 | let props = fields || Object.keys(exports.UACH_MAP); 74 | for (const prop of props) { 75 | if (this.hasOwnProperty(prop)) { 76 | values[prop] = this[prop]; 77 | } 78 | } 79 | return values; 80 | } 81 | getValuesAsHeaders(fields) { 82 | let values = {}; 83 | let props = fields || Object.keys(exports.UACH_MAP); 84 | for (const prop of props) { 85 | if (this.hasOwnProperty(prop)) { 86 | const { field, type } = exports.UACH_MAP[prop]; 87 | values[field] = this.serializeHeader(this[prop], type); 88 | } 89 | } 90 | return values; 91 | } 92 | setValues(values) { 93 | for (const key in values) { 94 | if (this.hasOwnProperty(key)) { 95 | const val = values[key]; 96 | if (this.isValidType(val, exports.UACH_MAP[key].type)) { 97 | this[key] = val; 98 | } 99 | } 100 | ; 101 | } 102 | return this; 103 | } 104 | setValuesFromUAParser(uap) { 105 | const arch = /(x86|arm).*(64)/.exec(uap.cpu.architecture || ''); 106 | if (arch) { 107 | this.architecture = arch[1]; 108 | if (arch[2] == '64') { 109 | this.bitness = '64'; 110 | } 111 | } 112 | switch (uap.device.type) { 113 | case 'mobile': 114 | this.formFactor = ['Mobile']; 115 | this.mobile = true; 116 | break; 117 | case 'tablet': 118 | this.formFactor = ['Tablet']; 119 | break; 120 | } 121 | if (uap.device.model) { 122 | this.model = uap.device.model; 123 | } 124 | if (uap.os.name) { 125 | this.platform = uap.os.name; 126 | if (uap.os.version) { 127 | this.platformVersion = uap.os.version; 128 | } 129 | } 130 | if (uap.browser.name) { 131 | const brands = [{ brand: uap.browser.name, version: uap.browser.version || '' }]; 132 | this.brands = brands; 133 | this.fullVersionList = brands; 134 | } 135 | return this; 136 | } 137 | setValuesFromHeaders(headers) { 138 | if (Object.keys(headers).some(prop => prop.startsWith('sec-ch-ua'))) { 139 | for (const key in exports.UACH_MAP) { 140 | const { field, type } = exports.UACH_MAP[key]; 141 | const headerField = field.toLowerCase(); 142 | if (headers.hasOwnProperty(headerField)) { 143 | this[key] = this.parseHeader(headers[headerField], type); 144 | } 145 | } 146 | } 147 | return this; 148 | } 149 | parseHeader(str, type) { 150 | if (!str) { 151 | return null; 152 | } 153 | switch (type) { 154 | case FIELD_TYPE.Boolean: 155 | return /\?1/.test(str); 156 | case FIELD_TYPE.List: 157 | if (!str.includes(';')) { 158 | return str.split(',').map(str => str.trim().replace(/\\?\"/g, '')); 159 | } 160 | return str.split(',') 161 | .map(brands => { 162 | const match = /\\?\"(.+)?\\?\".+\\?\"(.+)?\\?\"/.exec(brands); 163 | return { 164 | brand: match ? match[1] : '', 165 | version: match ? match[2] : '' 166 | }; 167 | }); 168 | case FIELD_TYPE.String: 169 | return str.replace(/\s*\\?\"\s*/g, ''); 170 | default: 171 | return null; 172 | } 173 | } 174 | serializeHeader(data, type) { 175 | if (!data) { 176 | return ''; 177 | } 178 | switch (type) { 179 | case FIELD_TYPE.Boolean: 180 | return data ? '?1' : '?0'; 181 | case FIELD_TYPE.List: 182 | if (!data.some(val => typeof val === 'string')) { 183 | return data.map(browser => `"${browser.brand}"; v="${browser.version}"`).join(', '); 184 | } 185 | return data.join(', '); 186 | case FIELD_TYPE.String: 187 | return `"${data}"`; 188 | default: 189 | return ''; 190 | } 191 | } 192 | isValidType(data, type) { 193 | switch (type) { 194 | case FIELD_TYPE.Boolean: 195 | return typeof data === 'boolean'; 196 | case FIELD_TYPE.List: 197 | return Array.isArray(data); 198 | case FIELD_TYPE.String: 199 | return typeof data === 'string'; 200 | default: 201 | return false; 202 | } 203 | } 204 | } 205 | exports.UAClientHints = UAClientHints; 206 | -------------------------------------------------------------------------------- /dist/esm/index.d.ts: -------------------------------------------------------------------------------- 1 | /*! UAClientHints.js 0.1.2 2 | Parse & serialize user-agent client hints (UA-CH) HTTP headers 3 | https://github.com/faisalman/ua-client-hints-js 4 | Author: Faisal Salman 5 | MIT License */ 6 | /// 7 | /// 8 | export declare enum FIELD_TYPE { 9 | Boolean = "sf-boolean", 10 | List = "sf-list", 11 | String = "sf-string" 12 | } 13 | export declare const UACH_MAP: { 14 | readonly architecture: { 15 | readonly field: "Sec-CH-UA-Arch"; 16 | readonly type: FIELD_TYPE.String; 17 | }; 18 | readonly bitness: { 19 | readonly field: "Sec-CH-UA-Bitness"; 20 | readonly type: FIELD_TYPE.String; 21 | }; 22 | readonly brands: { 23 | readonly field: "Sec-CH-UA"; 24 | readonly type: FIELD_TYPE.List; 25 | }; 26 | readonly formFactor: { 27 | readonly field: "Sec-CH-UA-Form-Factor"; 28 | readonly type: FIELD_TYPE.String; 29 | }; 30 | readonly fullVersionList: { 31 | readonly field: "Sec-CH-UA-Full-Version-List"; 32 | readonly type: FIELD_TYPE.List; 33 | }; 34 | readonly mobile: { 35 | readonly field: "Sec-CH-UA-Mobile"; 36 | readonly type: FIELD_TYPE.Boolean; 37 | }; 38 | readonly model: { 39 | readonly field: "Sec-CH-UA-Model"; 40 | readonly type: FIELD_TYPE.String; 41 | }; 42 | readonly platform: { 43 | readonly field: "Sec-CH-UA-Platform"; 44 | readonly type: FIELD_TYPE.String; 45 | }; 46 | readonly platformVersion: { 47 | readonly field: "Sec-CH-UA-Platform-Version"; 48 | readonly type: FIELD_TYPE.String; 49 | }; 50 | readonly wow64: { 51 | readonly field: "Sec-CH-UA-WOW64"; 52 | readonly type: FIELD_TYPE.Boolean; 53 | }; 54 | }; 55 | export type UACHDataType = boolean | string | string[] | NavigatorUABrandVersion[] | null | undefined; 56 | export type UACHDataField = keyof typeof UACH_MAP; 57 | export type UACHHeaderType = typeof FIELD_TYPE[keyof typeof FIELD_TYPE]; 58 | export type UACHHeaderField = Lowercase; 59 | export declare class UAClientHints { 60 | private architecture?; 61 | private bitness?; 62 | private brands?; 63 | private formFactor?; 64 | private fullVersionList?; 65 | private mobile?; 66 | private model?; 67 | private platform?; 68 | private platformVersion?; 69 | private wow64?; 70 | getValues(fields?: UACHDataField[]): UADataValues; 71 | getValuesAsHeaders(fields?: UACHDataField[]): Partial>; 72 | setValues(values?: UADataValues): UAClientHints; 73 | setValuesFromUAParser(uap: UAParser.IResult): UAClientHints; 74 | setValuesFromHeaders(headers: Record): UAClientHints; 75 | private parseHeader; 76 | private serializeHeader; 77 | private isValidType; 78 | } 79 | -------------------------------------------------------------------------------- /dist/esm/index.js: -------------------------------------------------------------------------------- 1 | /*! UAClientHints.js 0.1.2 2 | Parse & serialize user-agent client hints (UA-CH) HTTP headers 3 | https://github.com/faisalman/ua-client-hints-js 4 | Author: Faisal Salman 5 | MIT License */ 6 | export var FIELD_TYPE; 7 | (function (FIELD_TYPE) { 8 | FIELD_TYPE["Boolean"] = "sf-boolean"; 9 | FIELD_TYPE["List"] = "sf-list"; 10 | FIELD_TYPE["String"] = "sf-string"; 11 | })(FIELD_TYPE || (FIELD_TYPE = {})); 12 | ; 13 | export const UACH_MAP = { 14 | architecture: { 15 | field: 'Sec-CH-UA-Arch', 16 | type: FIELD_TYPE.String 17 | }, 18 | bitness: { 19 | field: 'Sec-CH-UA-Bitness', 20 | type: FIELD_TYPE.String 21 | }, 22 | brands: { 23 | field: 'Sec-CH-UA', 24 | type: FIELD_TYPE.List 25 | }, 26 | formFactor: { 27 | field: 'Sec-CH-UA-Form-Factor', 28 | type: FIELD_TYPE.String 29 | }, 30 | fullVersionList: { 31 | field: 'Sec-CH-UA-Full-Version-List', 32 | type: FIELD_TYPE.List 33 | }, 34 | mobile: { 35 | field: 'Sec-CH-UA-Mobile', 36 | type: FIELD_TYPE.Boolean 37 | }, 38 | model: { 39 | field: 'Sec-CH-UA-Model', 40 | type: FIELD_TYPE.String 41 | }, 42 | platform: { 43 | field: 'Sec-CH-UA-Platform', 44 | type: FIELD_TYPE.String 45 | }, 46 | platformVersion: { 47 | field: 'Sec-CH-UA-Platform-Version', 48 | type: FIELD_TYPE.String 49 | }, 50 | wow64: { 51 | field: 'Sec-CH-UA-WOW64', 52 | type: FIELD_TYPE.Boolean 53 | } 54 | }; 55 | export class UAClientHints { 56 | constructor() { 57 | this.architecture = undefined; 58 | this.bitness = undefined; 59 | this.brands = undefined; 60 | this.formFactor = undefined; 61 | this.fullVersionList = undefined; 62 | this.mobile = undefined; 63 | this.model = undefined; 64 | this.platform = undefined; 65 | this.platformVersion = undefined; 66 | this.wow64 = undefined; 67 | } 68 | getValues(fields) { 69 | let values = {}; 70 | let props = fields || Object.keys(UACH_MAP); 71 | for (const prop of props) { 72 | if (this.hasOwnProperty(prop)) { 73 | values[prop] = this[prop]; 74 | } 75 | } 76 | return values; 77 | } 78 | getValuesAsHeaders(fields) { 79 | let values = {}; 80 | let props = fields || Object.keys(UACH_MAP); 81 | for (const prop of props) { 82 | if (this.hasOwnProperty(prop)) { 83 | const { field, type } = UACH_MAP[prop]; 84 | values[field] = this.serializeHeader(this[prop], type); 85 | } 86 | } 87 | return values; 88 | } 89 | setValues(values) { 90 | for (const key in values) { 91 | if (this.hasOwnProperty(key)) { 92 | const val = values[key]; 93 | if (this.isValidType(val, UACH_MAP[key].type)) { 94 | this[key] = val; 95 | } 96 | } 97 | ; 98 | } 99 | return this; 100 | } 101 | setValuesFromUAParser(uap) { 102 | const arch = /(x86|arm).*(64)/.exec(uap.cpu.architecture || ''); 103 | if (arch) { 104 | this.architecture = arch[1]; 105 | if (arch[2] == '64') { 106 | this.bitness = '64'; 107 | } 108 | } 109 | switch (uap.device.type) { 110 | case 'mobile': 111 | this.formFactor = ['Mobile']; 112 | this.mobile = true; 113 | break; 114 | case 'tablet': 115 | this.formFactor = ['Tablet']; 116 | break; 117 | } 118 | if (uap.device.model) { 119 | this.model = uap.device.model; 120 | } 121 | if (uap.os.name) { 122 | this.platform = uap.os.name; 123 | if (uap.os.version) { 124 | this.platformVersion = uap.os.version; 125 | } 126 | } 127 | if (uap.browser.name) { 128 | const brands = [{ brand: uap.browser.name, version: uap.browser.version || '' }]; 129 | this.brands = brands; 130 | this.fullVersionList = brands; 131 | } 132 | return this; 133 | } 134 | setValuesFromHeaders(headers) { 135 | if (Object.keys(headers).some(prop => prop.startsWith('sec-ch-ua'))) { 136 | for (const key in UACH_MAP) { 137 | const { field, type } = UACH_MAP[key]; 138 | const headerField = field.toLowerCase(); 139 | if (headers.hasOwnProperty(headerField)) { 140 | this[key] = this.parseHeader(headers[headerField], type); 141 | } 142 | } 143 | } 144 | return this; 145 | } 146 | parseHeader(str, type) { 147 | if (!str) { 148 | return null; 149 | } 150 | switch (type) { 151 | case FIELD_TYPE.Boolean: 152 | return /\?1/.test(str); 153 | case FIELD_TYPE.List: 154 | if (!str.includes(';')) { 155 | return str.split(',').map(str => str.trim().replace(/\\?\"/g, '')); 156 | } 157 | return str.split(',') 158 | .map(brands => { 159 | const match = /\\?\"(.+)?\\?\".+\\?\"(.+)?\\?\"/.exec(brands); 160 | return { 161 | brand: match ? match[1] : '', 162 | version: match ? match[2] : '' 163 | }; 164 | }); 165 | case FIELD_TYPE.String: 166 | return str.replace(/\s*\\?\"\s*/g, ''); 167 | default: 168 | return null; 169 | } 170 | } 171 | serializeHeader(data, type) { 172 | if (!data) { 173 | return ''; 174 | } 175 | switch (type) { 176 | case FIELD_TYPE.Boolean: 177 | return data ? '?1' : '?0'; 178 | case FIELD_TYPE.List: 179 | if (!data.some(val => typeof val === 'string')) { 180 | return data.map(browser => `"${browser.brand}"; v="${browser.version}"`).join(', '); 181 | } 182 | return data.join(', '); 183 | case FIELD_TYPE.String: 184 | return `"${data}"`; 185 | default: 186 | return ''; 187 | } 188 | } 189 | isValidType(data, type) { 190 | switch (type) { 191 | case FIELD_TYPE.Boolean: 192 | return typeof data === 'boolean'; 193 | case FIELD_TYPE.List: 194 | return Array.isArray(data); 195 | case FIELD_TYPE.String: 196 | return typeof data === 'string'; 197 | default: 198 | return false; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ua-client-hints-js", 3 | "version": "0.1.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ua-client-hints-js", 9 | "version": "0.1.1", 10 | "funding": [ 11 | { 12 | "type": "github", 13 | "url": "https://github.com/sponsors/faisalman" 14 | }, 15 | { 16 | "type": "opencollective", 17 | "url": "https://opencollective.com/ua-parser-js" 18 | }, 19 | { 20 | "type": "paypal", 21 | "url": "https://paypal.me/faisalman" 22 | } 23 | ], 24 | "license": "MIT", 25 | "devDependencies": { 26 | "@types/ua-parser-js": "^0.7.37", 27 | "mocha": "^10.2.0", 28 | "typescript": "^5.2.2", 29 | "ua-parser-js": "^1.0.35", 30 | "user-agent-data-types": "^0.4.2" 31 | } 32 | }, 33 | "node_modules/@types/ua-parser-js": { 34 | "version": "0.7.37", 35 | "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.37.tgz", 36 | "integrity": "sha512-4sOxS3ZWXC0uHJLYcWAaLMxTvjRX3hT96eF4YWUh1ovTaenvibaZOE5uXtIp4mksKMLRwo7YDiCBCw6vBiUPVg==", 37 | "dev": true 38 | }, 39 | "node_modules/ansi-colors": { 40 | "version": "4.1.1", 41 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 42 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 43 | "dev": true, 44 | "engines": { 45 | "node": ">=6" 46 | } 47 | }, 48 | "node_modules/ansi-regex": { 49 | "version": "5.0.1", 50 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 51 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 52 | "dev": true, 53 | "engines": { 54 | "node": ">=8" 55 | } 56 | }, 57 | "node_modules/ansi-styles": { 58 | "version": "4.3.0", 59 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 60 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 61 | "dev": true, 62 | "dependencies": { 63 | "color-convert": "^2.0.1" 64 | }, 65 | "engines": { 66 | "node": ">=8" 67 | }, 68 | "funding": { 69 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 70 | } 71 | }, 72 | "node_modules/anymatch": { 73 | "version": "3.1.3", 74 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", 75 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", 76 | "dev": true, 77 | "dependencies": { 78 | "normalize-path": "^3.0.0", 79 | "picomatch": "^2.0.4" 80 | }, 81 | "engines": { 82 | "node": ">= 8" 83 | } 84 | }, 85 | "node_modules/argparse": { 86 | "version": "2.0.1", 87 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 88 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 89 | "dev": true 90 | }, 91 | "node_modules/balanced-match": { 92 | "version": "1.0.2", 93 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 94 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 95 | "dev": true 96 | }, 97 | "node_modules/binary-extensions": { 98 | "version": "2.2.0", 99 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 100 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 101 | "dev": true, 102 | "engines": { 103 | "node": ">=8" 104 | } 105 | }, 106 | "node_modules/brace-expansion": { 107 | "version": "2.0.1", 108 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 109 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 110 | "dev": true, 111 | "dependencies": { 112 | "balanced-match": "^1.0.0" 113 | } 114 | }, 115 | "node_modules/braces": { 116 | "version": "3.0.2", 117 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 118 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 119 | "dev": true, 120 | "dependencies": { 121 | "fill-range": "^7.0.1" 122 | }, 123 | "engines": { 124 | "node": ">=8" 125 | } 126 | }, 127 | "node_modules/browser-stdout": { 128 | "version": "1.3.1", 129 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 130 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 131 | "dev": true 132 | }, 133 | "node_modules/camelcase": { 134 | "version": "6.3.0", 135 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 136 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 137 | "dev": true, 138 | "engines": { 139 | "node": ">=10" 140 | }, 141 | "funding": { 142 | "url": "https://github.com/sponsors/sindresorhus" 143 | } 144 | }, 145 | "node_modules/chalk": { 146 | "version": "4.1.2", 147 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 148 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 149 | "dev": true, 150 | "dependencies": { 151 | "ansi-styles": "^4.1.0", 152 | "supports-color": "^7.1.0" 153 | }, 154 | "engines": { 155 | "node": ">=10" 156 | }, 157 | "funding": { 158 | "url": "https://github.com/chalk/chalk?sponsor=1" 159 | } 160 | }, 161 | "node_modules/chalk/node_modules/supports-color": { 162 | "version": "7.2.0", 163 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 164 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 165 | "dev": true, 166 | "dependencies": { 167 | "has-flag": "^4.0.0" 168 | }, 169 | "engines": { 170 | "node": ">=8" 171 | } 172 | }, 173 | "node_modules/chokidar": { 174 | "version": "3.5.3", 175 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", 176 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", 177 | "dev": true, 178 | "funding": [ 179 | { 180 | "type": "individual", 181 | "url": "https://paulmillr.com/funding/" 182 | } 183 | ], 184 | "dependencies": { 185 | "anymatch": "~3.1.2", 186 | "braces": "~3.0.2", 187 | "glob-parent": "~5.1.2", 188 | "is-binary-path": "~2.1.0", 189 | "is-glob": "~4.0.1", 190 | "normalize-path": "~3.0.0", 191 | "readdirp": "~3.6.0" 192 | }, 193 | "engines": { 194 | "node": ">= 8.10.0" 195 | }, 196 | "optionalDependencies": { 197 | "fsevents": "~2.3.2" 198 | } 199 | }, 200 | "node_modules/cliui": { 201 | "version": "7.0.4", 202 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 203 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 204 | "dev": true, 205 | "dependencies": { 206 | "string-width": "^4.2.0", 207 | "strip-ansi": "^6.0.0", 208 | "wrap-ansi": "^7.0.0" 209 | } 210 | }, 211 | "node_modules/color-convert": { 212 | "version": "2.0.1", 213 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 214 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 215 | "dev": true, 216 | "dependencies": { 217 | "color-name": "~1.1.4" 218 | }, 219 | "engines": { 220 | "node": ">=7.0.0" 221 | } 222 | }, 223 | "node_modules/color-name": { 224 | "version": "1.1.4", 225 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 226 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 227 | "dev": true 228 | }, 229 | "node_modules/concat-map": { 230 | "version": "0.0.1", 231 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 232 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 233 | "dev": true 234 | }, 235 | "node_modules/debug": { 236 | "version": "4.3.4", 237 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 238 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 239 | "dev": true, 240 | "dependencies": { 241 | "ms": "2.1.2" 242 | }, 243 | "engines": { 244 | "node": ">=6.0" 245 | }, 246 | "peerDependenciesMeta": { 247 | "supports-color": { 248 | "optional": true 249 | } 250 | } 251 | }, 252 | "node_modules/debug/node_modules/ms": { 253 | "version": "2.1.2", 254 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 255 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 256 | "dev": true 257 | }, 258 | "node_modules/decamelize": { 259 | "version": "4.0.0", 260 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 261 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 262 | "dev": true, 263 | "engines": { 264 | "node": ">=10" 265 | }, 266 | "funding": { 267 | "url": "https://github.com/sponsors/sindresorhus" 268 | } 269 | }, 270 | "node_modules/diff": { 271 | "version": "5.0.0", 272 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 273 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 274 | "dev": true, 275 | "engines": { 276 | "node": ">=0.3.1" 277 | } 278 | }, 279 | "node_modules/emoji-regex": { 280 | "version": "8.0.0", 281 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 282 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 283 | "dev": true 284 | }, 285 | "node_modules/escalade": { 286 | "version": "3.1.1", 287 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 288 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 289 | "dev": true, 290 | "engines": { 291 | "node": ">=6" 292 | } 293 | }, 294 | "node_modules/escape-string-regexp": { 295 | "version": "4.0.0", 296 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 297 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 298 | "dev": true, 299 | "engines": { 300 | "node": ">=10" 301 | }, 302 | "funding": { 303 | "url": "https://github.com/sponsors/sindresorhus" 304 | } 305 | }, 306 | "node_modules/fill-range": { 307 | "version": "7.0.1", 308 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 309 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 310 | "dev": true, 311 | "dependencies": { 312 | "to-regex-range": "^5.0.1" 313 | }, 314 | "engines": { 315 | "node": ">=8" 316 | } 317 | }, 318 | "node_modules/find-up": { 319 | "version": "5.0.0", 320 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 321 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 322 | "dev": true, 323 | "dependencies": { 324 | "locate-path": "^6.0.0", 325 | "path-exists": "^4.0.0" 326 | }, 327 | "engines": { 328 | "node": ">=10" 329 | }, 330 | "funding": { 331 | "url": "https://github.com/sponsors/sindresorhus" 332 | } 333 | }, 334 | "node_modules/flat": { 335 | "version": "5.0.2", 336 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 337 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 338 | "dev": true, 339 | "bin": { 340 | "flat": "cli.js" 341 | } 342 | }, 343 | "node_modules/fs.realpath": { 344 | "version": "1.0.0", 345 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 346 | "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", 347 | "dev": true 348 | }, 349 | "node_modules/fsevents": { 350 | "version": "2.3.3", 351 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 352 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 353 | "dev": true, 354 | "hasInstallScript": true, 355 | "optional": true, 356 | "os": [ 357 | "darwin" 358 | ], 359 | "engines": { 360 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 361 | } 362 | }, 363 | "node_modules/get-caller-file": { 364 | "version": "2.0.5", 365 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 366 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 367 | "dev": true, 368 | "engines": { 369 | "node": "6.* || 8.* || >= 10.*" 370 | } 371 | }, 372 | "node_modules/glob": { 373 | "version": "7.2.0", 374 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", 375 | "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", 376 | "dev": true, 377 | "dependencies": { 378 | "fs.realpath": "^1.0.0", 379 | "inflight": "^1.0.4", 380 | "inherits": "2", 381 | "minimatch": "^3.0.4", 382 | "once": "^1.3.0", 383 | "path-is-absolute": "^1.0.0" 384 | }, 385 | "engines": { 386 | "node": "*" 387 | }, 388 | "funding": { 389 | "url": "https://github.com/sponsors/isaacs" 390 | } 391 | }, 392 | "node_modules/glob-parent": { 393 | "version": "5.1.2", 394 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 395 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 396 | "dev": true, 397 | "dependencies": { 398 | "is-glob": "^4.0.1" 399 | }, 400 | "engines": { 401 | "node": ">= 6" 402 | } 403 | }, 404 | "node_modules/glob/node_modules/brace-expansion": { 405 | "version": "1.1.11", 406 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 407 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 408 | "dev": true, 409 | "dependencies": { 410 | "balanced-match": "^1.0.0", 411 | "concat-map": "0.0.1" 412 | } 413 | }, 414 | "node_modules/glob/node_modules/minimatch": { 415 | "version": "3.1.2", 416 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 417 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 418 | "dev": true, 419 | "dependencies": { 420 | "brace-expansion": "^1.1.7" 421 | }, 422 | "engines": { 423 | "node": "*" 424 | } 425 | }, 426 | "node_modules/has-flag": { 427 | "version": "4.0.0", 428 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 429 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 430 | "dev": true, 431 | "engines": { 432 | "node": ">=8" 433 | } 434 | }, 435 | "node_modules/he": { 436 | "version": "1.2.0", 437 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 438 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 439 | "dev": true, 440 | "bin": { 441 | "he": "bin/he" 442 | } 443 | }, 444 | "node_modules/inflight": { 445 | "version": "1.0.6", 446 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 447 | "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", 448 | "dev": true, 449 | "dependencies": { 450 | "once": "^1.3.0", 451 | "wrappy": "1" 452 | } 453 | }, 454 | "node_modules/inherits": { 455 | "version": "2.0.4", 456 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 457 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 458 | "dev": true 459 | }, 460 | "node_modules/is-binary-path": { 461 | "version": "2.1.0", 462 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 463 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 464 | "dev": true, 465 | "dependencies": { 466 | "binary-extensions": "^2.0.0" 467 | }, 468 | "engines": { 469 | "node": ">=8" 470 | } 471 | }, 472 | "node_modules/is-extglob": { 473 | "version": "2.1.1", 474 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 475 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 476 | "dev": true, 477 | "engines": { 478 | "node": ">=0.10.0" 479 | } 480 | }, 481 | "node_modules/is-fullwidth-code-point": { 482 | "version": "3.0.0", 483 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 484 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 485 | "dev": true, 486 | "engines": { 487 | "node": ">=8" 488 | } 489 | }, 490 | "node_modules/is-glob": { 491 | "version": "4.0.3", 492 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 493 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 494 | "dev": true, 495 | "dependencies": { 496 | "is-extglob": "^2.1.1" 497 | }, 498 | "engines": { 499 | "node": ">=0.10.0" 500 | } 501 | }, 502 | "node_modules/is-number": { 503 | "version": "7.0.0", 504 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 505 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 506 | "dev": true, 507 | "engines": { 508 | "node": ">=0.12.0" 509 | } 510 | }, 511 | "node_modules/is-plain-obj": { 512 | "version": "2.1.0", 513 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 514 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 515 | "dev": true, 516 | "engines": { 517 | "node": ">=8" 518 | } 519 | }, 520 | "node_modules/is-unicode-supported": { 521 | "version": "0.1.0", 522 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 523 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 524 | "dev": true, 525 | "engines": { 526 | "node": ">=10" 527 | }, 528 | "funding": { 529 | "url": "https://github.com/sponsors/sindresorhus" 530 | } 531 | }, 532 | "node_modules/js-yaml": { 533 | "version": "4.1.0", 534 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 535 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 536 | "dev": true, 537 | "dependencies": { 538 | "argparse": "^2.0.1" 539 | }, 540 | "bin": { 541 | "js-yaml": "bin/js-yaml.js" 542 | } 543 | }, 544 | "node_modules/locate-path": { 545 | "version": "6.0.0", 546 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 547 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 548 | "dev": true, 549 | "dependencies": { 550 | "p-locate": "^5.0.0" 551 | }, 552 | "engines": { 553 | "node": ">=10" 554 | }, 555 | "funding": { 556 | "url": "https://github.com/sponsors/sindresorhus" 557 | } 558 | }, 559 | "node_modules/log-symbols": { 560 | "version": "4.1.0", 561 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 562 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 563 | "dev": true, 564 | "dependencies": { 565 | "chalk": "^4.1.0", 566 | "is-unicode-supported": "^0.1.0" 567 | }, 568 | "engines": { 569 | "node": ">=10" 570 | }, 571 | "funding": { 572 | "url": "https://github.com/sponsors/sindresorhus" 573 | } 574 | }, 575 | "node_modules/minimatch": { 576 | "version": "5.0.1", 577 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", 578 | "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", 579 | "dev": true, 580 | "dependencies": { 581 | "brace-expansion": "^2.0.1" 582 | }, 583 | "engines": { 584 | "node": ">=10" 585 | } 586 | }, 587 | "node_modules/mocha": { 588 | "version": "10.2.0", 589 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", 590 | "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", 591 | "dev": true, 592 | "dependencies": { 593 | "ansi-colors": "4.1.1", 594 | "browser-stdout": "1.3.1", 595 | "chokidar": "3.5.3", 596 | "debug": "4.3.4", 597 | "diff": "5.0.0", 598 | "escape-string-regexp": "4.0.0", 599 | "find-up": "5.0.0", 600 | "glob": "7.2.0", 601 | "he": "1.2.0", 602 | "js-yaml": "4.1.0", 603 | "log-symbols": "4.1.0", 604 | "minimatch": "5.0.1", 605 | "ms": "2.1.3", 606 | "nanoid": "3.3.3", 607 | "serialize-javascript": "6.0.0", 608 | "strip-json-comments": "3.1.1", 609 | "supports-color": "8.1.1", 610 | "workerpool": "6.2.1", 611 | "yargs": "16.2.0", 612 | "yargs-parser": "20.2.4", 613 | "yargs-unparser": "2.0.0" 614 | }, 615 | "bin": { 616 | "_mocha": "bin/_mocha", 617 | "mocha": "bin/mocha.js" 618 | }, 619 | "engines": { 620 | "node": ">= 14.0.0" 621 | }, 622 | "funding": { 623 | "type": "opencollective", 624 | "url": "https://opencollective.com/mochajs" 625 | } 626 | }, 627 | "node_modules/ms": { 628 | "version": "2.1.3", 629 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 630 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 631 | "dev": true 632 | }, 633 | "node_modules/nanoid": { 634 | "version": "3.3.3", 635 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", 636 | "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", 637 | "dev": true, 638 | "bin": { 639 | "nanoid": "bin/nanoid.cjs" 640 | }, 641 | "engines": { 642 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 643 | } 644 | }, 645 | "node_modules/normalize-path": { 646 | "version": "3.0.0", 647 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 648 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 649 | "dev": true, 650 | "engines": { 651 | "node": ">=0.10.0" 652 | } 653 | }, 654 | "node_modules/once": { 655 | "version": "1.4.0", 656 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 657 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 658 | "dev": true, 659 | "dependencies": { 660 | "wrappy": "1" 661 | } 662 | }, 663 | "node_modules/p-limit": { 664 | "version": "3.1.0", 665 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 666 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 667 | "dev": true, 668 | "dependencies": { 669 | "yocto-queue": "^0.1.0" 670 | }, 671 | "engines": { 672 | "node": ">=10" 673 | }, 674 | "funding": { 675 | "url": "https://github.com/sponsors/sindresorhus" 676 | } 677 | }, 678 | "node_modules/p-locate": { 679 | "version": "5.0.0", 680 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 681 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 682 | "dev": true, 683 | "dependencies": { 684 | "p-limit": "^3.0.2" 685 | }, 686 | "engines": { 687 | "node": ">=10" 688 | }, 689 | "funding": { 690 | "url": "https://github.com/sponsors/sindresorhus" 691 | } 692 | }, 693 | "node_modules/path-exists": { 694 | "version": "4.0.0", 695 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 696 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 697 | "dev": true, 698 | "engines": { 699 | "node": ">=8" 700 | } 701 | }, 702 | "node_modules/path-is-absolute": { 703 | "version": "1.0.1", 704 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 705 | "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", 706 | "dev": true, 707 | "engines": { 708 | "node": ">=0.10.0" 709 | } 710 | }, 711 | "node_modules/picomatch": { 712 | "version": "2.3.1", 713 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", 714 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", 715 | "dev": true, 716 | "engines": { 717 | "node": ">=8.6" 718 | }, 719 | "funding": { 720 | "url": "https://github.com/sponsors/jonschlinkert" 721 | } 722 | }, 723 | "node_modules/randombytes": { 724 | "version": "2.1.0", 725 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 726 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 727 | "dev": true, 728 | "dependencies": { 729 | "safe-buffer": "^5.1.0" 730 | } 731 | }, 732 | "node_modules/readdirp": { 733 | "version": "3.6.0", 734 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 735 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 736 | "dev": true, 737 | "dependencies": { 738 | "picomatch": "^2.2.1" 739 | }, 740 | "engines": { 741 | "node": ">=8.10.0" 742 | } 743 | }, 744 | "node_modules/require-directory": { 745 | "version": "2.1.1", 746 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 747 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 748 | "dev": true, 749 | "engines": { 750 | "node": ">=0.10.0" 751 | } 752 | }, 753 | "node_modules/safe-buffer": { 754 | "version": "5.2.1", 755 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 756 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 757 | "dev": true, 758 | "funding": [ 759 | { 760 | "type": "github", 761 | "url": "https://github.com/sponsors/feross" 762 | }, 763 | { 764 | "type": "patreon", 765 | "url": "https://www.patreon.com/feross" 766 | }, 767 | { 768 | "type": "consulting", 769 | "url": "https://feross.org/support" 770 | } 771 | ] 772 | }, 773 | "node_modules/serialize-javascript": { 774 | "version": "6.0.0", 775 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 776 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 777 | "dev": true, 778 | "dependencies": { 779 | "randombytes": "^2.1.0" 780 | } 781 | }, 782 | "node_modules/string-width": { 783 | "version": "4.2.3", 784 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 785 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 786 | "dev": true, 787 | "dependencies": { 788 | "emoji-regex": "^8.0.0", 789 | "is-fullwidth-code-point": "^3.0.0", 790 | "strip-ansi": "^6.0.1" 791 | }, 792 | "engines": { 793 | "node": ">=8" 794 | } 795 | }, 796 | "node_modules/strip-ansi": { 797 | "version": "6.0.1", 798 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 799 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 800 | "dev": true, 801 | "dependencies": { 802 | "ansi-regex": "^5.0.1" 803 | }, 804 | "engines": { 805 | "node": ">=8" 806 | } 807 | }, 808 | "node_modules/strip-json-comments": { 809 | "version": "3.1.1", 810 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 811 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 812 | "dev": true, 813 | "engines": { 814 | "node": ">=8" 815 | }, 816 | "funding": { 817 | "url": "https://github.com/sponsors/sindresorhus" 818 | } 819 | }, 820 | "node_modules/supports-color": { 821 | "version": "8.1.1", 822 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 823 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 824 | "dev": true, 825 | "dependencies": { 826 | "has-flag": "^4.0.0" 827 | }, 828 | "engines": { 829 | "node": ">=10" 830 | }, 831 | "funding": { 832 | "url": "https://github.com/chalk/supports-color?sponsor=1" 833 | } 834 | }, 835 | "node_modules/to-regex-range": { 836 | "version": "5.0.1", 837 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 838 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 839 | "dev": true, 840 | "dependencies": { 841 | "is-number": "^7.0.0" 842 | }, 843 | "engines": { 844 | "node": ">=8.0" 845 | } 846 | }, 847 | "node_modules/typescript": { 848 | "version": "5.2.2", 849 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 850 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 851 | "dev": true, 852 | "bin": { 853 | "tsc": "bin/tsc", 854 | "tsserver": "bin/tsserver" 855 | }, 856 | "engines": { 857 | "node": ">=14.17" 858 | } 859 | }, 860 | "node_modules/ua-parser-js": { 861 | "version": "1.0.35", 862 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz", 863 | "integrity": "sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==", 864 | "dev": true, 865 | "funding": [ 866 | { 867 | "type": "opencollective", 868 | "url": "https://opencollective.com/ua-parser-js" 869 | }, 870 | { 871 | "type": "paypal", 872 | "url": "https://paypal.me/faisalman" 873 | } 874 | ], 875 | "engines": { 876 | "node": "*" 877 | } 878 | }, 879 | "node_modules/user-agent-data-types": { 880 | "version": "0.4.2", 881 | "resolved": "https://registry.npmjs.org/user-agent-data-types/-/user-agent-data-types-0.4.2.tgz", 882 | "integrity": "sha512-jXep3kO/dGNmDOkbDa8ccp4QArgxR4I76m3QVcJ1aOF0B9toc+YtSXtX5gLdDTZXyWlpQYQrABr6L1L2GZOghw==", 883 | "dev": true 884 | }, 885 | "node_modules/workerpool": { 886 | "version": "6.2.1", 887 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", 888 | "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", 889 | "dev": true 890 | }, 891 | "node_modules/wrap-ansi": { 892 | "version": "7.0.0", 893 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 894 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 895 | "dev": true, 896 | "dependencies": { 897 | "ansi-styles": "^4.0.0", 898 | "string-width": "^4.1.0", 899 | "strip-ansi": "^6.0.0" 900 | }, 901 | "engines": { 902 | "node": ">=10" 903 | }, 904 | "funding": { 905 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 906 | } 907 | }, 908 | "node_modules/wrappy": { 909 | "version": "1.0.2", 910 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 911 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 912 | "dev": true 913 | }, 914 | "node_modules/y18n": { 915 | "version": "5.0.8", 916 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 917 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 918 | "dev": true, 919 | "engines": { 920 | "node": ">=10" 921 | } 922 | }, 923 | "node_modules/yargs": { 924 | "version": "16.2.0", 925 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 926 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 927 | "dev": true, 928 | "dependencies": { 929 | "cliui": "^7.0.2", 930 | "escalade": "^3.1.1", 931 | "get-caller-file": "^2.0.5", 932 | "require-directory": "^2.1.1", 933 | "string-width": "^4.2.0", 934 | "y18n": "^5.0.5", 935 | "yargs-parser": "^20.2.2" 936 | }, 937 | "engines": { 938 | "node": ">=10" 939 | } 940 | }, 941 | "node_modules/yargs-parser": { 942 | "version": "20.2.4", 943 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 944 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 945 | "dev": true, 946 | "engines": { 947 | "node": ">=10" 948 | } 949 | }, 950 | "node_modules/yargs-unparser": { 951 | "version": "2.0.0", 952 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 953 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 954 | "dev": true, 955 | "dependencies": { 956 | "camelcase": "^6.0.0", 957 | "decamelize": "^4.0.0", 958 | "flat": "^5.0.2", 959 | "is-plain-obj": "^2.1.0" 960 | }, 961 | "engines": { 962 | "node": ">=10" 963 | } 964 | }, 965 | "node_modules/yocto-queue": { 966 | "version": "0.1.0", 967 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 968 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 969 | "dev": true, 970 | "engines": { 971 | "node": ">=10" 972 | }, 973 | "funding": { 974 | "url": "https://github.com/sponsors/sindresorhus" 975 | } 976 | } 977 | } 978 | } 979 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "UAClientHints.js", 3 | "name": "ua-client-hints-js", 4 | "version": "0.1.2", 5 | "author": "Faisal Salman ", 6 | "description": "Parse & serialize user-agent client hints (UA-CH) HTTP headers", 7 | "type": "commonjs", 8 | "main": "./dist/cjs/index.js", 9 | "module": "./dist/esm/index.js", 10 | "exports": { 11 | ".": { 12 | "require": "./dist/cjs/index.js", 13 | "import": "./dist/esm/index.js" 14 | } 15 | }, 16 | "files": [ 17 | "dist" 18 | ], 19 | "directories": { 20 | "dist": "dist", 21 | "src": "src", 22 | "test": "test" 23 | }, 24 | "scripts": { 25 | "build": "npm run build:cjs && npm run build:esm", 26 | "build:cjs": "tsc --module commonjs --outDir ./dist/cjs --target es2015", 27 | "build:esm": "tsc --module esnext --moduleResolution bundler --outDir ./dist/esm --target es6", 28 | "test": "mocha ./test" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/faisalman/ua-client-hints-js.git" 33 | }, 34 | "keywords": [ 35 | "ua-parser-js", 36 | "client-hints", 37 | "ua-ch", 38 | "ch-ua", 39 | "http-header", 40 | "user-agent", 41 | "user-agent-detection", 42 | "device-detection", 43 | "platform-detection", 44 | "mobile-detection", 45 | "browser-detection", 46 | "architecture-detection", 47 | "bitness-detection" 48 | ], 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/faisalman/ua-client-hints-js/issues" 52 | }, 53 | "homepage": "https://github.com/faisalman/ua-client-hints-js#readme", 54 | "funding": [ 55 | { 56 | "type": "github", 57 | "url": "https://github.com/sponsors/faisalman" 58 | }, 59 | { 60 | "type": "opencollective", 61 | "url": "https://opencollective.com/ua-parser-js" 62 | }, 63 | { 64 | "type": "paypal", 65 | "url": "https://paypal.me/faisalman" 66 | } 67 | ], 68 | "devDependencies": { 69 | "@types/ua-parser-js": "^0.7.37", 70 | "mocha": "^10.2.0", 71 | "typescript": "^5.2.2", 72 | "ua-parser-js": "^1.0.35", 73 | "user-agent-data-types": "^0.4.2" 74 | } 75 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////// 2 | /*! UAClientHints.js 0.1.2 3 | Parse & serialize user-agent client hints (UA-CH) HTTP headers 4 | https://github.com/faisalman/ua-client-hints-js 5 | Author: Faisal Salman 6 | MIT License */ 7 | /////////////////////////////////////////////////////////////////// 8 | 9 | /// 10 | 11 | export enum FIELD_TYPE { 12 | Boolean = 'sf-boolean', 13 | List = 'sf-list', 14 | String = 'sf-string' 15 | }; 16 | 17 | export const UACH_MAP = { 18 | architecture : { 19 | field : 'Sec-CH-UA-Arch', 20 | type : FIELD_TYPE.String 21 | }, 22 | bitness : { 23 | field : 'Sec-CH-UA-Bitness', 24 | type : FIELD_TYPE.String 25 | }, 26 | brands : { 27 | field : 'Sec-CH-UA', 28 | type : FIELD_TYPE.List 29 | }, 30 | formFactor : { 31 | field : 'Sec-CH-UA-Form-Factor', 32 | type : FIELD_TYPE.String 33 | }, 34 | fullVersionList : { 35 | field : 'Sec-CH-UA-Full-Version-List', 36 | type : FIELD_TYPE.List 37 | }, 38 | mobile : { 39 | field : 'Sec-CH-UA-Mobile', 40 | type : FIELD_TYPE.Boolean 41 | }, 42 | model : { 43 | field : 'Sec-CH-UA-Model', 44 | type : FIELD_TYPE.String 45 | }, 46 | platform : { 47 | field : 'Sec-CH-UA-Platform', 48 | type : FIELD_TYPE.String 49 | }, 50 | platformVersion : { 51 | field : 'Sec-CH-UA-Platform-Version', 52 | type : FIELD_TYPE.String 53 | }, 54 | wow64 : { 55 | field : 'Sec-CH-UA-WOW64', 56 | type : FIELD_TYPE.Boolean 57 | } 58 | } as const; 59 | 60 | export type UACHDataType = boolean | string | string[] | NavigatorUABrandVersion[] | null | undefined; 61 | export type UACHDataField = keyof typeof UACH_MAP; 62 | export type UACHHeaderType = typeof FIELD_TYPE[keyof typeof FIELD_TYPE]; 63 | export type UACHHeaderField = Lowercase; 64 | 65 | export class UAClientHints { 66 | 67 | private architecture?: string = undefined; 68 | private bitness?: string = undefined; 69 | private brands?: NavigatorUABrandVersion[] = undefined; 70 | private formFactor?: string[] = undefined; 71 | private fullVersionList?: NavigatorUABrandVersion[] = undefined; 72 | private mobile?: boolean = undefined; 73 | private model?: string = undefined; 74 | private platform?: string = undefined; 75 | private platformVersion?: string = undefined; 76 | private wow64?: boolean = undefined; 77 | 78 | getValues(fields?: UACHDataField[]): UADataValues { 79 | let values: any = {}; 80 | let props = fields || Object.keys(UACH_MAP); 81 | for (const prop of props) { 82 | if (this.hasOwnProperty(prop)) { 83 | values[prop] = this[prop]; 84 | } 85 | } 86 | return values; 87 | } 88 | 89 | getValuesAsHeaders(fields?: UACHDataField[]): Partial> { 90 | let values: any = {}; 91 | let props = fields || Object.keys(UACH_MAP); 92 | for (const prop of props) { 93 | if (this.hasOwnProperty(prop)) { 94 | const { field, type } = UACH_MAP[prop]; 95 | values[field] = this.serializeHeader(this[prop], type); 96 | } 97 | } 98 | return values; 99 | } 100 | 101 | setValues(values?: UADataValues): UAClientHints { 102 | for (const key in values) { 103 | if (this.hasOwnProperty(key)) { 104 | const val = values[key]; 105 | if (this.isValidType(val, UACH_MAP[key].type)) { 106 | this[key] = val as any; 107 | } 108 | }; 109 | } 110 | return this; 111 | } 112 | 113 | setValuesFromUAParser(uap: UAParser.IResult): UAClientHints { 114 | const arch = /(x86|arm).*(64)/.exec(uap.cpu.architecture || ''); 115 | if (arch) { 116 | this.architecture = arch[1]; 117 | if (arch[2] == '64') { 118 | this.bitness = '64'; 119 | } 120 | } 121 | switch (uap.device.type) { 122 | case 'mobile': 123 | this.formFactor = ['Mobile']; 124 | this.mobile = true; 125 | break; 126 | case 'tablet': 127 | this.formFactor = ['Tablet']; 128 | break; 129 | } 130 | if (uap.device.model) { 131 | this.model = uap.device.model; 132 | } 133 | if (uap.os.name) { 134 | this.platform = uap.os.name; 135 | if (uap.os.version) { 136 | this.platformVersion = uap.os.version; 137 | } 138 | } 139 | if (uap.browser.name) { 140 | const brands = [{ brand : uap.browser.name, version : uap.browser.version || '' }]; 141 | this.brands = brands; 142 | this.fullVersionList = brands; 143 | } 144 | return this; 145 | } 146 | 147 | setValuesFromHeaders(headers: Record): UAClientHints { 148 | if(Object.keys(headers).some(prop => prop.startsWith('sec-ch-ua'))) { 149 | for (const key in UACH_MAP) { 150 | const { field, type } = UACH_MAP[key]; 151 | const headerField = field.toLowerCase(); 152 | if (headers.hasOwnProperty(headerField)) { 153 | this[key] = this.parseHeader(headers[headerField], type) as any; 154 | } 155 | } 156 | } 157 | return this; 158 | } 159 | 160 | private parseHeader(str: string | undefined, type: UACHHeaderType): UACHDataType { 161 | if (!str) { 162 | return null; 163 | } 164 | switch (type) { 165 | case FIELD_TYPE.Boolean: 166 | return /\?1/.test(str); 167 | case FIELD_TYPE.List: 168 | if (!str.includes(';')) { 169 | return str.split(',').map(str => str.trim().replace(/\\?\"/g, '')); 170 | } 171 | return str.split(',') 172 | .map(brands => { 173 | const match = /\\?\"(.+)?\\?\".+\\?\"(.+)?\\?\"/.exec(brands) 174 | return { 175 | brand : match ? match[1] : '', 176 | version : match ? match[2] : '' 177 | }; 178 | }); 179 | case FIELD_TYPE.String: 180 | return str.replace(/\s*\\?\"\s*/g, ''); 181 | default: 182 | return null; 183 | } 184 | } 185 | 186 | private serializeHeader(data: UACHDataType | undefined, type: UACHHeaderType): string { 187 | if (!data) { 188 | return ''; 189 | } 190 | switch (type) { 191 | case FIELD_TYPE.Boolean: 192 | return data ? '?1' : '?0'; 193 | case FIELD_TYPE.List: 194 | if (!(data).some(val => typeof val === 'string')) { 195 | return (data).map(browser => `"${browser.brand}"; v="${browser.version}"`).join(', '); 196 | } 197 | return (data).join(', '); 198 | case FIELD_TYPE.String: 199 | return `"${data}"`; 200 | default: 201 | return ''; 202 | } 203 | } 204 | 205 | private isValidType(data: UACHDataType, type: UACHHeaderType): boolean { 206 | switch (type) { 207 | case FIELD_TYPE.Boolean: 208 | return typeof data === 'boolean'; 209 | case FIELD_TYPE.List: 210 | return Array.isArray(data); 211 | case FIELD_TYPE.String: 212 | return typeof data === 'string'; 213 | default: 214 | return false; 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const { UAClientHints } = require('../dist/cjs'); 2 | const assert = require('assert'); 3 | const UAParser = require('ua-parser-js'); 4 | 5 | describe('UAClientHints', () => { 6 | describe('Assign client hints values from UA-CH headers into a JS object', () => { 7 | 8 | const req = { 9 | headers : { 10 | 'sec-ch-ua' : '"Chromium";v="93", "Google Chrome";v="93", " Not;A Brand";v="99"', 11 | 'sec-ch-ua-full-version-list' : '"Chromium";v="93.0.1.2", "Google Chrome";v="93.0.1.2", " Not;A Brand";v="99.0.1.2"', 12 | 'sec-ch-ua-arch' : '"arm"', 13 | 'sec-ch-ua-bitness' : '"64"', 14 | 'sec-ch-ua-mobile' : '?1', 15 | 'sec-ch-ua-model' : '"Pixel 99"', 16 | 'sec-ch-ua-platform' : '"Linux"', 17 | 'sec-ch-ua-platform-version' : '"13"' 18 | } 19 | }; 20 | 21 | const ch = new UAClientHints(); 22 | ch.setValuesFromHeaders(req.headers); 23 | 24 | it('Parse values from header', () => { 25 | const chData1 = ch.getValues(['architecture', 'bitness', 'mobile']); 26 | 27 | assert.deepEqual(chData1, { 28 | "architecture": "arm", 29 | "bitness": "64", 30 | "mobile": true 31 | }); 32 | 33 | const chData2 = ch.getValues(); 34 | 35 | assert.deepEqual(chData2, { 36 | "architecture": "arm", 37 | "bitness": "64", 38 | "brands": [ 39 | { 40 | "brand": "Chromium", 41 | "version": "93" 42 | }, 43 | { 44 | "brand": "Google Chrome", 45 | "version": "93" 46 | }, 47 | { 48 | "brand": " Not;A Brand", 49 | "version": "99" 50 | } 51 | ], 52 | "fullVersionList": [ 53 | { 54 | "brand": "Chromium", 55 | "version": "93.0.1.2" 56 | }, 57 | { 58 | "brand": "Google Chrome", 59 | "version": "93.0.1.2" 60 | }, 61 | { 62 | "brand": " Not;A Brand", 63 | "version": "99.0.1.2" 64 | } 65 | ], 66 | "mobile": true, 67 | "model": "Pixel 99", 68 | "platform": "Linux", 69 | "platformVersion": "13", 70 | "wow64": null, 71 | "formFactor": null 72 | }); 73 | }); 74 | 75 | it('Serialize values to header', () => { 76 | 77 | ch.setValues({ 78 | 'wow64' : true, 79 | 'formFactor' : 'Automotive' 80 | }); 81 | 82 | const headersData1 = ch.getValuesAsHeaders(); 83 | assert.deepEqual(headersData1, { 84 | 'Sec-CH-UA' : '"Chromium"; v="93", "Google Chrome"; v="93", " Not;A Brand"; v="99"', 85 | 'Sec-CH-UA-Full-Version-List' : '"Chromium"; v="93.0.1.2", "Google Chrome"; v="93.0.1.2", " Not;A Brand"; v="99.0.1.2"', 86 | 'Sec-CH-UA-Arch' : '"arm"', 87 | 'Sec-CH-UA-Bitness' : '"64"', 88 | 'Sec-CH-UA-Mobile' : '?1', 89 | 'Sec-CH-UA-Model' : '"Pixel 99"', 90 | 'Sec-CH-UA-Platform' : '"Linux"', 91 | 'Sec-CH-UA-Platform-Version' : '"13"', 92 | 'Sec-CH-UA-WOW64' : '?1', 93 | 'Sec-CH-UA-Form-Factor' : '"Automotive"' 94 | }); 95 | 96 | const headersData2 = ch.getValuesAsHeaders(['brands', 'mobile', 'model']); 97 | assert.deepEqual(headersData2, { 98 | 'Sec-CH-UA' : '"Chromium"; v="93", "Google Chrome"; v="93", " Not;A Brand"; v="99"', 99 | 'Sec-CH-UA-Mobile' : '?1', 100 | 'Sec-CH-UA-Model' : '"Pixel 99"' 101 | }); 102 | }); 103 | }); 104 | 105 | describe('Rearrange data from UAParser getResult() into a client hints structure', () => { 106 | 107 | it('Map values from UAParser', () => { 108 | 109 | const ua = 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537'; 110 | const uap = UAParser(ua); 111 | const ch2 = new UAClientHints(); 112 | ch2.setValuesFromUAParser(uap); 113 | const ch2Data = ch2.getValues(); 114 | assert.deepEqual(ch2Data, { 115 | architecture: null, 116 | bitness: null, 117 | brands: [ { brand: 'IEMobile', version: '11.0' } ], 118 | formFactor: [ 'Mobile' ], 119 | fullVersionList: [ { brand: 'IEMobile', version: '11.0' } ], 120 | mobile: true, 121 | model: 'Lumia 635', 122 | platform: 'Windows Phone', 123 | platformVersion: '8.1', 124 | wow64: null 125 | }); 126 | const headersData2 = ch2.getValuesAsHeaders(); 127 | assert.deepEqual(headersData2, { 128 | 'Sec-CH-UA-Arch': '', 129 | 'Sec-CH-UA-Bitness': '', 130 | 'Sec-CH-UA': '"IEMobile"; v="11.0"', 131 | 'Sec-CH-UA-Form-Factor': '"Mobile"', 132 | 'Sec-CH-UA-Full-Version-List': '"IEMobile"; v="11.0"', 133 | 'Sec-CH-UA-Mobile': '?1', 134 | 'Sec-CH-UA-Model': '"Lumia 635"', 135 | 'Sec-CH-UA-Platform': '"Windows Phone"', 136 | 'Sec-CH-UA-Platform-Version': '"8.1"', 137 | 'Sec-CH-UA-WOW64': '' 138 | }); 139 | }); 140 | }); 141 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 59 | "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": [ 110 | "./src" 111 | ] 112 | } 113 | --------------------------------------------------------------------------------