├── .dockerignore ├── .env.example ├── .eslintrc.cjs ├── .github ├── FUNDING.yml └── workflows │ └── fly.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── Dockerfile ├── LICENSE ├── README.md ├── docs └── app-versions.md ├── drizzle.config.ts ├── fly.toml ├── lib └── .gitignore ├── nodemon.json ├── package.json ├── pnpm-lock.yaml ├── resources └── devices.csv ├── src ├── api │ ├── constants.ts │ ├── cron.ts │ ├── database │ │ ├── database.ts │ │ ├── migrate.ts │ │ └── schema.ts │ ├── errors.ts │ ├── logger.ts │ ├── routes │ │ ├── admin.ts │ │ └── api.ts │ ├── server.ts │ ├── types.ts │ └── utils.ts ├── env.ts ├── http_client │ ├── TLSClient.ts │ ├── errors.ts │ ├── fetch_compat.ts │ └── types.ts ├── px │ ├── appc2.ts │ ├── constants.ts │ ├── cookies.ts │ ├── device.ts │ ├── errors.ts │ └── uuid.ts └── utils.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Build output 5 | /dist 6 | tsconfig.tsbuildinfo 7 | 8 | # Logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Environment variables 15 | .env 16 | .env.* 17 | 18 | # IDE 19 | .idea 20 | *.iml 21 | .vscode 22 | 23 | # Misc 24 | **/.swc 25 | 26 | research/**/* 27 | 28 | fly.toml 29 | /lib 30 | src/cli/ -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | API_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 3 | AXIOM_DATASET=test 4 | AXIOM_TOKEN=xaat-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 5 | COMPRESS_RESPONSE=false 6 | DATABASE_URL=mysql://USERNAME:PASSWORD@HOST/DATABASE?ssl={"rejectUnauthorized":true} 7 | PORT=3000 8 | JWT_AUTH_MINUTES=10080 -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | root: true, 5 | parser: "@typescript-eslint/parser", 6 | plugins: ["@typescript-eslint", "import"], 7 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"], 8 | parserOptions: { 9 | sourceType: "module", 10 | ecmaVersion: "ES2021", 11 | }, 12 | rules: { 13 | "@typescript-eslint/no-explicit-any": "error", 14 | "@typescript-eslint/no-unused-vars": "off", 15 | "no-debugger": "error", 16 | "import/consistent-type-specifier-style": ["error", "prefer-inline"], 17 | }, 18 | ignorePatterns: ["**/dist/*"], 19 | }; 20 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [AzureFlow] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 2 | ko_fi: azureflow 3 | -------------------------------------------------------------------------------- /.github/workflows/fly.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | deploy: 8 | name: Deploy app 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: superfly/flyctl-actions/setup-flyctl@master 13 | - run: flyctl deploy --remote-only 14 | env: 15 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | /dist 6 | tsconfig.tsbuildinfo 7 | 8 | # Logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | # Environment variables 15 | .env 16 | .env.* 17 | !.env.example 18 | 19 | # IDE 20 | .idea 21 | *.iml 22 | .vscode 23 | 24 | # Misc 25 | .swc 26 | /src/cli -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | /research 3 | README.md 4 | 5 | # Ignore dependency locks 6 | pnpm-lock.yaml 7 | package-lock.json 8 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | module.exports = { 3 | printWidth: 128, 4 | trailingComma: "all", 5 | useTabs: true, 6 | tabWidth: 4, 7 | semi: true, 8 | singleQuote: false, 9 | quoteProps: "as-needed", 10 | endOfLine: "lf", 11 | bracketSpacing: true, 12 | 13 | overrides: [ 14 | { 15 | files: "*.md", 16 | options: { 17 | tabWidth: 2, 18 | }, 19 | }, 20 | { 21 | files: "README.md", 22 | options: {}, 23 | }, 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Run with: 4 | # docker build -t tinyluma/px . 5 | # docker run -p 3000:3000 --env-file .env tinyluma/px 6 | 7 | 8 | # Adjust NODE_VERSION as desired 9 | ARG NODE_VERSION=lts 10 | # ALPINE: FROM node:${NODE_VERSION}-alpine as base 11 | FROM node:${NODE_VERSION}-slim as base 12 | 13 | LABEL fly_launch_runtime="Node.js" 14 | 15 | # Node.js app lives here 16 | WORKDIR /build 17 | 18 | ARG PNPM_VERSION=8.6.7 19 | RUN npm install -g pnpm@$PNPM_VERSION 20 | RUN pnpm config set update-notifier false 21 | 22 | 23 | # Throw-away build stage to reduce size of final image 24 | FROM base as build 25 | 26 | # Install packages needed to build node modules 27 | # ALPINE: RUN apk add --no-cache make gcc g++ python3 28 | RUN apt-get update -qq && \ 29 | apt-get install -y python-is-python3 pkg-config build-essential 30 | 31 | # Install node modules 32 | COPY --link package.json pnpm-lock.yaml ./ 33 | RUN pnpm install --frozen-lockfile 34 | 35 | # Copy application code 36 | # Prevent breaking cache by only copying what's needed 37 | COPY --link ./src ./src 38 | COPY --link ./tsconfig.json . 39 | 40 | # Build application 41 | RUN pnpm run build 42 | 43 | # Remove development dependencies 44 | RUN pnpm prune --prod 45 | 46 | # ALPINE: FROM golang:alpine as tls-client-build 47 | FROM golang:1.18 as tls-client-build 48 | 49 | WORKDIR /build 50 | 51 | # Git version tag 52 | ARG TLS_VERSION=1.5.0 53 | ARG TLS_ARCH=amd64 54 | 55 | # Install build requirements 56 | # ALPINE: RUN apk add --no-cache curl zip gcc g++ 57 | RUN apt-get update -qq && \ 58 | apt-get install -y curl zip build-essential 59 | 60 | # Download and unpack latest code. 61 | # Use curl instead of "git clone" since there's tons of binaries in the commit history which take forever to clone. 62 | RUN curl -sSLo output.zip https://github.com/bogdanfinn/tls-client/archive/refs/tags/v${TLS_VERSION}.zip 63 | RUN unzip output.zip -d ./ && cd tls-client* 64 | 65 | # Build 66 | # https://stackoverflow.com/questions/53048942/is-it-possible-to-get-the-architecture-of-the-docker-engine-in-a-dockerfile 67 | # ALPINE: RUN cd /build/tls-client*/cffi_dist && GOOS=linux CGO_ENABLED=1 GOARCH=${TLS_ARCH} go build -buildmode=c-shared -o /build/dist/tls-client-linux-alpine-${TLS_ARCH}-${TLS_VERSION}.so 68 | RUN cd /build/tls-client*/cffi_dist && GOOS=linux CGO_ENABLED=1 GOARCH=${TLS_ARCH} go build -buildmode=c-shared -o /build/dist/tls-client-linux-ubuntu-${TLS_ARCH}-${TLS_VERSION}.so 69 | 70 | # Final stage for app image 71 | FROM base 72 | 73 | WORKDIR /app 74 | 75 | # Set production environment 76 | ENV NODE_ENV=production 77 | 78 | # Copy built application 79 | COPY --from=build /build/dist ./dist 80 | COPY --from=build /build/node_modules ./node_modules 81 | COPY --link ./package.json ./ 82 | COPY --link ./drizzle.config.ts ./ 83 | #COPY --link ./drizzle ./drizzle 84 | COPY --link ./resources ./resources 85 | #COPY --from=build /app /app 86 | #COPY --from=build /build/package.json ./ 87 | #COPY --from=build /build/drizzle.config.ts ./ 88 | #COPY --from=build /build/node_modules ./node_modules 89 | #COPY --from=build /build/dist ./build 90 | #COPY --from=build /build/drizzle ./drizzle 91 | #COPY --from=build /build/resources ./resources 92 | #COPY --from=build /build/lib ./lib 93 | COPY --from=tls-client-build /build/dist ./lib 94 | 95 | # Setup cron job 96 | RUN echo "0 0 * * * cd /app && pnpm run cron >> /var/log/cron.log" >> /etc/cron.d/main 97 | RUN ln -sf /dev/stdout /var/log/cron.log 98 | 99 | # Certifcate pinning breaks without this 100 | RUN apt-get update -qq && \ 101 | apt-get install -y ca-certificates 102 | 103 | # Start the server by default, this can be overwritten at runtime 104 | ENV PORT=3000 105 | EXPOSE ${PORT} 106 | ENTRYPOINT ["pnpm", "run", "start"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PX Mobile 2 | 3 | An API that automatically generates PerimeterX mobile cookies 🤖. 4 | 5 | This project is provided "as-is" and without warranty of any kind. It's likely to be broken by updates in the future and is meant to be used as a resource to learn more about reverse engineering. 6 | 7 | # ⚙️ Setup 8 | 9 | ## 💻 Local 10 | 11 | Make sure [`pnpm`](https://pnpm.io/installation) is installed. 12 | 13 | ```bash 14 | cp .env.example .env 15 | pnpm install 16 | pnpm run build 17 | pnpm run start 18 | ``` 19 | 20 | ## 🚀 Deployment to [Fly.io](https://fly.io/) 21 | 22 | ```bash 23 | flyctl create --name generate-api --no-deploy 24 | flyctl secrets set API_SECRET=example 25 | flyctl secrets set DATABASE_URL=example 26 | flyctl secrets set AXIOM_TOKEN=example 27 | flyctl deploy 28 | fly scale count 2 29 | ``` 30 | 31 | # 🔨 Usage 32 | 33 | Start the dev server using `pnpm run dev` and make a curl request to `http://localhost:3000/api/auth` to get started. 34 | Alternatively, use `cli.ts` for local testing. 35 | 36 | # 📚 Documentation 37 | 38 | See [here](docs/app-versions.md) for app version details. 39 | 40 | # 🧞 Commands 41 | 42 | | Command | Action | 43 | |-------------------------------|-----------------------------------------------| 44 | | `pnpm install` | Installs dependencies | 45 | | `pnpm run dev` | Starts a local dev server at `localhost:3000` | 46 | | `pnpm run build` | Build for production to `./dist` | 47 | | `pnpm run start` | Runs the built production files | 48 | | `pnpm run drizzle:generate` | Generates Drizzle schema files | 49 | | `pnpm run drizzle:migrate` | Runs Drizzle migrations | 50 | | `pnpm run drizzle:push` | Push Drizzle schema changes | 51 | | `pnpm run lint` | Run ESLint checking | 52 | | `pnpm run prettier:check` | Check for Prettier violations | 53 | | `pnpm run prettier:format` | Correct Prettier violations | 54 | -------------------------------------------------------------------------------- /docs/app-versions.md: -------------------------------------------------------------------------------- 1 | # 🔴 App Versions 2 | 3 | | App Name | Bundle ID | App Version | PX ID | SDK Version  **▲** | 4 | |----------------------------|------------------------------------|--------------------|----------------------------------|------------------------------| 5 | | `Snipes` (USA) | `com.shopgate.android.app22760` | `5.46.0` | `PX6XNN2xkk` | `1.13.1` | 6 | | `solebox` | `com.solebox.raffleapp` | `2.0.0` | `PXuR63h57Z` | `1.13.2` (React) | 7 | | `Urban Outfitters` | `com.urbanoutfitters.android` | `2.60` | `PX0N3XMOl0` | `1.13.2` | 8 | | `Laybuy` | `com.laybuy.laybuy` | `4.32.1` | `PXN56PXeEB` | `1.13.2` | 9 | | `FIVE GUYS` | `com.fiveguys.olo.android` | `5.1.1` | `PXAOit9CN0` | `1.13.2` | 10 | | `Sam's Club` | `com.rfi.sams.android` | `23.06.10` | `PXkZ8ZZQmW` (dev: `PX8eeWmT9a`) | `1.13.4` | 11 | | `GOAT` | `com.airgoat.goat` | `1.64.10` | `PXmFvVqEj3` (dev: `PXp6KJReLE`) | `1.15.0` | 12 | | `Hibbett | City Gear` | `com.hibbett.android` | `6.4.1` | `PX9Qx3Rve4` | `1.15.2` | 13 | | `StockX` | `com.stockx.stockx` | `4.14.43` | `PX16uD0kOF` | `1.15.2` | 14 | | `Chegg Study` | `com.chegg` | `13.31.1` | `PXaOtQIWNf` | `1.15.0` | 15 | | `iHerb` | `com.iherb` | `9.6.0615` | `PXVtidNbtC` | `1.16.5` | 16 | | `TextNow` | `com.enflick.android.TextNow` | `23.29.0.2` | `PXK56WkC4O` (dev: `PXN4VzfSCm`) | `2.2.0` | 17 | | `My B&BW` | `com.bathandbody.bbw` | `5.4.1.29` | `PXlsXlyYa5` (dev: `PXVmK4o7m2`) | `2.2.1` | 18 | | `TVG` | `com.tvg` | `1.32.20230406-SL` | `PXYIkzMJ9m` | `2.1.1` | 19 | | `SNIPES` (EU) | `com.snipes` | `2.2.3` | `PXszbF5p84` | `2.2.2` (React) | 20 | | `George` | `com.georgeatasda` | `1.0.134` | `PXdoe5chT3` | `2.2.2` | 21 | | `Walmart` | `com.walmart.android` | `23.19` | `PXUArm9B04` | `2.2.2` | 22 | | `SSENSE` | `ssense.android.prod` | `3.1.0` | `PXQ7o93831` | `2.2.2` | 23 | | `Vivid Seats` | `com.vividseats.android` | `2023.57.0` | `PXIuDu56vJ` (`PXbj5OofE8`) | `2.2.2` | 24 | | `Zillow` | `com.zillow.android.zillowmap` | `14.6.1.72135` | `PXHYx10rg3` (+extras) | `2.2.2` | 25 | | `Grubhub` | `com.grubhub.android` | `2023.22.1` | `PXO97ybH4J` | `2.2.3` | 26 | | `FanDuel Sportsbook` | `com.fanduel.sportsbook` | `1.73.0` | `PXJMCVuBG8` | `2.2.3` | 27 | | `Total Wine` | `com.totalwine.app.store` | `7.5.2` | `PXFF0j69T5` (dev: `PX8d6Sr2bT`) | `3.0.2` | 28 | | `Wayfair` | `com.wayfair.wayfair` | `5.206` | `PX3Vk96I6i` (dev: `PX1Iv8I1cE`) | `3.0.2` | 29 | | `Priceline` | `com.priceline.android.negotiator` | `7.6.264` | `PX9aTjSd0n` | `3.0.3` | 30 | | `Shiekh` | `com.shiekh.android` | `10.16` | `PXM2JHbdkv` (dev: `PXoHlvzT0p`) | `3.0.5` | 31 | 32 | How to find app values: 33 | 1. App name: `resources.arsc/res/values/strings.xml` and search for: 34 | - `name="app_name"` 35 | - `application_name` 36 | - `android:label="` (`AndroidManifest.xml`) 37 | 2. Bundle ID: `BuildConfig.APPLICATION_ID` 38 | 3. App Version: `BuildConfig.VERSION_NAME` 39 | 4. PX ID: Search for: 40 | - `"PX[a-zA-Z0-9]{8}"` (enable regex) 41 | - `PX[a-zA-Z0-9]{8}` (enable regex + resources) 42 | - `PerimeterX.INSTANCE.start` and xref the containing method 43 | - `"appId"` 44 | - Run app and view `/data/data/com.example.app/shared_prefs/com.perimeterx.mobile_sdk.PXxxxxxxxx.xml` 45 | 5. SDK Version: Search for: 46 | - `PerimeterX Android SDK` 47 | - `PX340` (SDK Version) 48 | - `String sdkVersion()` 49 | 50 | New versions of the Mobile SDK can be downloaded [here](https://perimeterx.jfrog.io/ui/repos/tree/General/). [PX Android Docs](https://docs.perimeterx.com/docs/sdk-android). 51 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { type Config } from "drizzle-kit"; 2 | import * as process from "process"; 3 | import "dotenv/config"; 4 | 5 | export default { 6 | schema: "./src/api/database/schema.ts", 7 | out: "./drizzle", 8 | driver: "mysql2", 9 | dbCredentials: { 10 | connectionString: process.env["DATABASE_URL"], 11 | }, 12 | breakpoints: false, 13 | } satisfies Config; 14 | -------------------------------------------------------------------------------- /fly.toml: -------------------------------------------------------------------------------- 1 | # See https://fly.io/docs/reference/configuration/ for information about how to use this file. 2 | 3 | app = "generate-api" 4 | primary_region = "iad" 5 | kill_signal = "SIGINT" 6 | kill_timeout = 5 7 | processes = [] 8 | 9 | #[processes] 10 | # app = "" 11 | # cron = "cron -f" 12 | 13 | # https://fly.io/docs/reference/configuration/#the-deploy-section 14 | #[deploy] 15 | # release_command = "pnpm run drizzle:push" 16 | # strategy = "bluegreen" 17 | 18 | [env] 19 | NODE_ENV = "production" 20 | PORT = "3000" 21 | AXIOM_DATASET = "prod" 22 | COMPRESS_RESPONSE = false 23 | 24 | [http_service] 25 | internal_port = 3000 26 | force_https = true 27 | auto_stop_machines = true 28 | auto_start_machines = true 29 | min_machines_running = 0 30 | 31 | processes = ["app"] 32 | [http_service.concurrency] 33 | type = "requests" 34 | soft_limit = 200 35 | hard_limit = 250 -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["./src/**/*.ts"], 3 | "exec": "node --loader ts-paths-esm-loader --no-warnings ./src/api/server.ts", 4 | "ext": "js ts", 5 | "delay": 250, 6 | "quiet": true 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generate_api", 3 | "version": "0.8.5", 4 | "private": true, 5 | "description": "A multi-version PerimeterX mobile cookie generator", 6 | "keywords": [ 7 | "API", 8 | "anti-bot", 9 | "cookie generator" 10 | ], 11 | "license": "UNLICENSED", 12 | "author": "AzureFlow", 13 | "type": "module", 14 | "main": "src/cli/cli.ts", 15 | "scripts": { 16 | "build": "tsc --build --clean && tsc --build && tsc-alias", 17 | "cron": "node dist/api/cron.js", 18 | "dev": "nodemon", 19 | "drizzle:generate": "drizzle-kit generate:mysql", 20 | "drizzle:migrate": "node --loader ts-paths-esm-loader --no-warnings src/api/database/migrate.ts", 21 | "drizzle:push": "drizzle-kit push:mysql", 22 | "preinstall": "npx only-allow pnpm", 23 | "lint": "eslint src", 24 | "prettier:check": "prettier --check .", 25 | "prettier:format": "prettier --write .", 26 | "start": "node dist/api/server.js" 27 | }, 28 | "dependencies": { 29 | "@axiomhq/pino": "^0.1.3", 30 | "@hono/node-server": "^1.8.2", 31 | "@hono/zod-validator": "^0.1.11", 32 | "@t3-oss/env-core": "^0.6.1", 33 | "chalk": "^5.3.0", 34 | "debug": "^4.3.4", 35 | "dotenv": "^16.4.5", 36 | "drizzle-orm": "^0.28.6", 37 | "ffi-napi": "^4.0.3", 38 | "hono": "^3.12.12", 39 | "long": "^5.2.3", 40 | "mysql2": "^3.9.2", 41 | "pino": "^8.19.0", 42 | "pino-pretty": "^10.3.1", 43 | "semver": "^7.6.0", 44 | "typeid-js": "^0.2.1", 45 | "zod": "^3.22.4", 46 | "zod-validation-error": "^1.5.0" 47 | }, 48 | "devDependencies": { 49 | "@swc/core": "^1.4.8", 50 | "@types/debug": "^4.1.12", 51 | "@types/diff": "^5.0.9", 52 | "@types/ffi-napi": "^4.0.10", 53 | "@types/node": "^20.11.28", 54 | "@types/semver": "^7.5.8", 55 | "@typescript-eslint/eslint-plugin": "^6.21.0", 56 | "@typescript-eslint/parser": "^6.21.0", 57 | "diff": "^5.2.0", 58 | "drizzle-kit": "^0.19.13", 59 | "eslint": "^8.57.0", 60 | "eslint-config-prettier": "^8.10.0", 61 | "eslint-plugin-import": "^2.29.1", 62 | "eslint-plugin-prettier": "^5.1.3", 63 | "nodemon": "^3.1.0", 64 | "p-queue": "^7.4.1", 65 | "prettier": "^3.2.5", 66 | "ts-node": "^10.9.2", 67 | "ts-paths-esm-loader": "^1.4.3", 68 | "tsc-alias": "^1.8.8", 69 | "typescript": "^5.4.2" 70 | }, 71 | "packageManager": "^pnpm@8.6.7", 72 | "engines": { 73 | "node": ">=18.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/api/constants.ts: -------------------------------------------------------------------------------- 1 | export const TYPEID_USER = "usr"; 2 | export const TYPEID_API = "api"; 3 | export const TYPEID_REFERENCE = "ref"; 4 | 5 | export const LOGIN_RATELIMIT_MINUTES = 1; 6 | -------------------------------------------------------------------------------- /src/api/cron.ts: -------------------------------------------------------------------------------- 1 | console.log("Running cron..."); 2 | -------------------------------------------------------------------------------- /src/api/database/database.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/mysql2"; 2 | import { createPool } from "mysql2/promise"; 3 | import { DefaultLogger, eq, LogWriter, sql } from "drizzle-orm"; 4 | import { userMapping, users } from "@src/api/database/schema.js"; 5 | import { typeid } from "typeid-js"; 6 | import env from "@src/env.js"; 7 | import { saltKey } from "@src/api/utils.js"; 8 | import { ApiUser } from "@src/api/types.js"; 9 | import logger from "@src/api/logger.js"; 10 | import { TYPEID_API, TYPEID_USER } from "@src/api/constants.js"; 11 | import { QueryError } from "mysql2"; 12 | import { ExistingUserException } from "@src/api/errors.js"; 13 | 14 | class DrizzleWriter implements LogWriter { 15 | write(message: string) { 16 | logger.trace(message); 17 | } 18 | } 19 | 20 | const drizzleLogger = new DefaultLogger({ writer: new DrizzleWriter() }); 21 | 22 | export const poolConnection = createPool({ 23 | uri: env.DATABASE_URL, 24 | ssl: { 25 | rejectUnauthorized: false, 26 | }, 27 | multipleStatements: true, 28 | }); 29 | const db = drizzle(poolConnection, { 30 | logger: drizzleLogger, 31 | }); 32 | 33 | export async function addUsage(userId: string) { 34 | return db 35 | .update(users) 36 | .set({ remainingQuota: sql`${users.remainingQuota} - 1` }) 37 | .where(eq(users.userId, userId)); 38 | } 39 | 40 | export async function addUser(discordId: string, initQuota = 0, expiresAt: Date | null = null) { 41 | const tid = typeid(TYPEID_USER); 42 | const apiKey = makeKey(); 43 | const hashedApiKey = await saltKey(apiKey); 44 | 45 | try { 46 | await db.transaction(async (tx) => { 47 | await tx.insert(users).values({ 48 | userId: tid.toString(), 49 | remainingQuota: initQuota, 50 | apiKey: hashedApiKey, 51 | expiresAt: expiresAt, 52 | }); 53 | 54 | await tx.insert(userMapping).values({ 55 | userId: tid.toString(), 56 | discordId: discordId, 57 | }); 58 | }); 59 | 60 | const ret: ApiUser = { 61 | userId: tid.toString(), 62 | apiKey: apiKey, 63 | remainingQuota: initQuota, 64 | expiresAt: expiresAt === null ? null : (expiresAt?.getTime() / 1000) | 0, 65 | }; 66 | 67 | return ret; 68 | } catch (err) { 69 | // Duplicate user 70 | if ("sqlMessage" in (err as Error) && (err as QueryError).code === "ER_DUP_ENTRY") { 71 | throw new ExistingUserException(); 72 | } 73 | 74 | throw err; 75 | } 76 | } 77 | 78 | export async function getUserById(userId: string) { 79 | return (await db.select().from(users).where(eq(users.userId, userId)))[0]; 80 | } 81 | 82 | export async function getUserByDiscordId(discordId: string) { 83 | return ( 84 | await db 85 | .select() 86 | .from(users) 87 | .leftJoin(userMapping, eq(users.userId, userMapping.userId)) 88 | .where(eq(userMapping.discordId, discordId)) 89 | )[0]?.users; 90 | } 91 | 92 | export async function getUserByKey(apiKey: string) { 93 | const hashedApiKey = await saltKey(apiKey); 94 | return (await db.select().from(users).where(eq(users.apiKey, hashedApiKey)))[0]; 95 | } 96 | 97 | export async function updateLastLogin(userId: string) { 98 | return db 99 | .update(users) 100 | .set({ 101 | lastLogin: new Date(), 102 | }) 103 | .where(eq(users.userId, userId)); 104 | } 105 | 106 | export async function resetKey(userId: string) { 107 | const newKey = makeKey(); 108 | const hashedApiKey = await saltKey(newKey); 109 | 110 | await db 111 | .update(users) 112 | .set({ 113 | apiKey: hashedApiKey, 114 | }) 115 | .where(eq(users.userId, userId)); 116 | 117 | return newKey; 118 | } 119 | 120 | function makeKey() { 121 | // return randomString(32); 122 | return typeid(TYPEID_API).toString(); 123 | } 124 | 125 | export default db; 126 | -------------------------------------------------------------------------------- /src/api/database/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/mysql2/migrator"; 2 | import db from "@src/api/database/database.js"; 3 | 4 | migrate(db, { migrationsFolder: "./drizzle" }) 5 | .then(() => { 6 | console.log("Migrations complete!"); 7 | process.exit(0); 8 | }) 9 | .catch((err) => { 10 | console.error("Migrations failed!", err); 11 | process.exit(1); 12 | }); 13 | -------------------------------------------------------------------------------- /src/api/database/schema.ts: -------------------------------------------------------------------------------- 1 | import { int, mysqlTableCreator, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core"; 2 | import { sql } from "drizzle-orm"; 3 | 4 | const mysqlTable = mysqlTableCreator((name) => `pxm_${name}`); 5 | 6 | export const users = mysqlTable( 7 | "users", 8 | { 9 | userId: varchar("user_id", { 10 | length: 64, // typeid 11 | }) 12 | .unique() 13 | .notNull(), 14 | apiKey: varchar("api_key", { 15 | length: 64, 16 | // length: 97, // argon2 17 | }).notNull(), 18 | remainingQuota: int("remaining_quota").notNull(), 19 | createdAt: timestamp("created_at") 20 | .notNull() 21 | .default(sql`CURRENT_TIMESTAMP`), 22 | updatedAt: timestamp("updated_at") 23 | .onUpdateNow() 24 | .notNull() 25 | .default(sql`CURRENT_TIMESTAMP`), 26 | expiresAt: timestamp("expires_at"), 27 | lastLogin: timestamp("last_login"), 28 | }, 29 | (table) => { 30 | return { 31 | userIdIdx: uniqueIndex("user_id_idx").on(table.userId), 32 | apiKeyIdx: uniqueIndex("api_key_idx").on(table.apiKey), 33 | }; 34 | }, 35 | ); 36 | 37 | export const userMapping = mysqlTable( 38 | "user_mapping", 39 | { 40 | userId: varchar("user_id", { 41 | length: 64, // typeid 42 | }) 43 | .unique() 44 | .notNull(), 45 | discordId: varchar("discord_id", { 46 | length: 32, 47 | }) 48 | .unique() 49 | .notNull(), 50 | }, 51 | (table) => { 52 | return { 53 | userIdIdx: uniqueIndex("user_id_idx").on(table.userId), 54 | discordIdIdx: uniqueIndex("discord_id_idx").on(table.discordId), 55 | }; 56 | }, 57 | ); 58 | -------------------------------------------------------------------------------- /src/api/errors.ts: -------------------------------------------------------------------------------- 1 | export class ExistingUserException extends Error { 2 | name = "ExistingUserException"; 3 | message = "User already exists"; 4 | } 5 | -------------------------------------------------------------------------------- /src/api/logger.ts: -------------------------------------------------------------------------------- 1 | import { pino } from "pino"; 2 | import env from "@src/env.js"; 3 | 4 | const p = pino( 5 | { 6 | level: "trace", 7 | }, 8 | pino.transport({ 9 | targets: [ 10 | { 11 | level: "info", 12 | target: "@axiomhq/pino", 13 | options: { 14 | dataset: env.AXIOM_DATASET, 15 | token: env.AXIOM_TOKEN, 16 | }, 17 | }, 18 | { 19 | level: "trace", 20 | target: "pino-pretty", 21 | options: { 22 | colorize: true, 23 | }, 24 | }, 25 | ], 26 | }), 27 | // multistream([ 28 | // pino.transport({ 29 | // target: "@axiomhq/pino", 30 | // options: { 31 | // dataset: env.AXIOM_DATASET, 32 | // token: env.AXIOM_TOKEN, 33 | // }, 34 | // }), 35 | // { 36 | // level: "trace", 37 | // stream: process.stdout, 38 | // prettyPrint: true, 39 | // }, 40 | // // pino.transport({ 41 | // // level: "trace", 42 | // // target: "pino-pretty", 43 | // // options: { 44 | // // colorize: true, 45 | // // }, 46 | // // }), 47 | // ]), 48 | ); 49 | // const logger = p.child({ 50 | // env: env.NODE_ENV, 51 | // }); 52 | 53 | export default p; 54 | -------------------------------------------------------------------------------- /src/api/routes/admin.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import logger from "@src/api/logger.js"; 3 | import { AddUserResponse, addUserSchema, BaseResponse, resetUserSchema, UsageResponse } from "@src/api/types.js"; 4 | import { addUser, getUserByDiscordId, getUserById, resetKey } from "@src/api/database/database.js"; 5 | import env from "@src/env.js"; 6 | import { zValidator } from "@hono/zod-validator"; 7 | import { fromZodError } from "zod-validation-error"; 8 | import { sha256 } from "@src/utils.js"; 9 | import { dateToEpoch } from "@src/api/utils.js"; 10 | import { ExistingUserException } from "@src/api/errors.js"; 11 | 12 | const hashedSecret = await sha256(env.API_SECRET); 13 | 14 | const adminRouter = new Hono(); 15 | // adminRouter.use("*", async (ctx, next) => { 16 | // const params = ctx.req.valid("json"); 17 | // if (!isValidSecret(params.secret)) { 18 | // return ctx.json({ 19 | // success: false, 20 | // message: "Invalid auth", 21 | // }, 401); 22 | // } 23 | // 24 | // await next(); 25 | // }); 26 | 27 | adminRouter.put( 28 | "addUser", 29 | zValidator("json", addUserSchema, (result, ctx) => { 30 | if (!result.success) { 31 | // https://www.npmjs.com/package/zod-validation-error#arguments 32 | const errorMessage = fromZodError(result.error).toString(); 33 | 34 | return ctx.json( 35 | { 36 | success: false, 37 | message: errorMessage, 38 | }, 39 | 400, 40 | ); 41 | } 42 | }), 43 | async (ctx) => { 44 | const params = ctx.req.valid("json"); 45 | if (!isValidSecret(params.secret)) { 46 | return ctx.json( 47 | { 48 | success: false, 49 | message: "Invalid auth", 50 | }, 51 | 401, 52 | ); 53 | } 54 | 55 | let expiresAt = null; 56 | if (params.expiresAt) { 57 | expiresAt = new Date(params.expiresAt); 58 | } 59 | 60 | try { 61 | const user = await addUser(params.discordId, params.requests, expiresAt); 62 | logger.info( 63 | { 64 | userId: user.userId, 65 | }, 66 | "Added user!", 67 | ); 68 | 69 | return ctx.json({ 70 | success: true, 71 | user: user, 72 | }); 73 | } catch (err) { 74 | if (err instanceof ExistingUserException) { 75 | return ctx.json( 76 | { 77 | success: false, 78 | message: err.message, 79 | }, 80 | 400, 81 | ); 82 | } 83 | 84 | throw err; 85 | } 86 | }, 87 | ); 88 | 89 | adminRouter.put( 90 | "resetKey", 91 | zValidator("json", resetUserSchema, (result, ctx) => { 92 | if (!result.success) { 93 | // https://www.npmjs.com/package/zod-validation-error#arguments 94 | const errorMessage = fromZodError(result.error).toString(); 95 | 96 | return ctx.json( 97 | { 98 | success: false, 99 | message: errorMessage, 100 | }, 101 | 400, 102 | ); 103 | } 104 | }), 105 | async (ctx) => { 106 | const params = ctx.req.valid("json"); 107 | if (!isValidSecret(params.secret)) { 108 | return ctx.json( 109 | { 110 | success: false, 111 | message: "Invalid auth", 112 | }, 113 | 401, 114 | ); 115 | } 116 | 117 | let user; 118 | if (params.isDiscord) { 119 | user = await getUserByDiscordId(params.userId); 120 | } else { 121 | user = await getUserById(params.userId); 122 | } 123 | 124 | if (!user) { 125 | return ctx.json( 126 | { 127 | success: false, 128 | message: "Unknown user", 129 | }, 130 | 400, 131 | ); 132 | } 133 | 134 | const newKey = await resetKey(user.userId); 135 | 136 | logger.info( 137 | { 138 | userId: user.userId, 139 | }, 140 | `Reset ${user.userId}'s API key`, 141 | ); 142 | 143 | const expiresAt = dateToEpoch(user.expiresAt); 144 | return ctx.json({ 145 | success: true, 146 | user: { 147 | userId: user.userId, 148 | apiKey: newKey, 149 | remainingQuota: user.remainingQuota, 150 | expiresAt: expiresAt, 151 | }, 152 | }); 153 | }, 154 | ); 155 | 156 | adminRouter.post( 157 | "usage", 158 | zValidator("json", resetUserSchema, (result, ctx) => { 159 | if (!result.success) { 160 | // https://www.npmjs.com/package/zod-validation-error#arguments 161 | const errorMessage = fromZodError(result.error).toString(); 162 | 163 | return ctx.json( 164 | { 165 | success: false, 166 | message: errorMessage, 167 | }, 168 | 400, 169 | ); 170 | } 171 | }), 172 | async (ctx) => { 173 | const params = ctx.req.valid("json"); 174 | if (!isValidSecret(params.secret)) { 175 | return ctx.json( 176 | { 177 | success: false, 178 | message: "Invalid auth", 179 | }, 180 | 401, 181 | ); 182 | } 183 | 184 | let user; 185 | if (params.isDiscord) { 186 | user = await getUserByDiscordId(params.userId); 187 | } else { 188 | user = await getUserById(params.userId); 189 | } 190 | 191 | if (!user) { 192 | return ctx.json( 193 | { 194 | success: false, 195 | message: "Unknown user", 196 | }, 197 | 400, 198 | ); 199 | } 200 | 201 | const expiresAt = dateToEpoch(user.expiresAt); 202 | return ctx.json({ 203 | success: true, 204 | user: { 205 | userId: user.userId, 206 | remainingQuota: user.remainingQuota, 207 | expiresAt: expiresAt, 208 | }, 209 | }); 210 | }, 211 | ); 212 | 213 | function isValidSecret(secret: string | undefined) { 214 | return secret !== undefined && secret.toUpperCase() === hashedSecret; 215 | } 216 | 217 | export default adminRouter; 218 | -------------------------------------------------------------------------------- /src/api/routes/api.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { 3 | ApiPxSolution, 4 | AuthPayload, 5 | AuthResponse, 6 | authSchema, 7 | BaseResponse, 8 | GenerateResponse, 9 | generateSchema, 10 | UsageResponse, 11 | } from "@src/api/types.js"; 12 | import { zValidator } from "@hono/zod-validator"; 13 | import { fromZodError } from "zod-validation-error"; 14 | import { APP_DATABASE } from "@src/px/constants.js"; 15 | import bakeCookie from "@src/px/cookies.js"; 16 | import { ProxyException, SSLPinException } from "@src/http_client/errors.js"; 17 | import { addUsage, getUserById, getUserByKey, updateLastLogin } from "@src/api/database/database.js"; 18 | import { createAuthJwt, createJwt, dateToEpoch, extractAuthHeader, isValidType, verifyJwt } from "@src/api/utils.js"; 19 | import { jwt } from "hono/jwt"; 20 | import env from "@src/env.js"; 21 | import { LOGIN_RATELIMIT_MINUTES, TYPEID_API, TYPEID_REFERENCE, TYPEID_USER } from "@src/api/constants.js"; 22 | import logger from "@src/api/logger.js"; 23 | import { typeid } from "typeid-js"; 24 | import { GenerationException } from "@src/px/errors.js"; 25 | 26 | const apiRouter = new Hono(); 27 | const authMiddleware = jwt({ 28 | secret: env.API_SECRET, 29 | alg: "HS256", 30 | }); 31 | 32 | apiRouter.post( 33 | "auth", 34 | zValidator("form", authSchema, (result, ctx) => { 35 | if (!result.success) { 36 | // https://www.npmjs.com/package/zod-validation-error#arguments 37 | const errorMessage = fromZodError(result.error).toString(); 38 | 39 | return ctx.json( 40 | { 41 | success: false, 42 | message: errorMessage, 43 | }, 44 | 400, 45 | ); 46 | } 47 | }), 48 | async (ctx) => { 49 | const params = ctx.req.valid("form"); 50 | 51 | if (!isValidType(params.apiKey, TYPEID_API)) { 52 | return ctx.json( 53 | { 54 | success: false, 55 | message: "Malformed API key", 56 | }, 57 | 400, 58 | ); 59 | } 60 | 61 | const user = await getUserByKey(params.apiKey); 62 | 63 | if (!user) { 64 | return ctx.json( 65 | { 66 | success: false, 67 | message: "Invalid API key", 68 | }, 69 | 400, 70 | ); 71 | } 72 | 73 | if (LOGIN_RATELIMIT_MINUTES > 0 && user.lastLogin !== null) { 74 | const now = new Date(); 75 | const saved = user.lastLogin; 76 | saved.setTime(saved.getTime() + LOGIN_RATELIMIT_MINUTES * 60 * 1000); 77 | 78 | if (now.getTime() <= saved.getTime()) { 79 | return ctx.json( 80 | { 81 | success: false, 82 | message: `Ratelimit, try again in ${((user.lastLogin.getTime() - now.getTime()) / 1000) | 0}s`, 83 | }, 84 | 429, 85 | ); 86 | } 87 | } 88 | 89 | const jwt = await createAuthJwt(user.userId, env.JWT_AUTH_MINUTES); 90 | await updateLastLogin(user.userId); 91 | 92 | return ctx.json({ 93 | success: true, 94 | user: { 95 | userId: user.userId, 96 | accessToken: jwt.token, 97 | expiresAt: jwt.expiresAt, 98 | }, 99 | }); 100 | }, 101 | ); 102 | 103 | apiRouter.get("usage", authMiddleware, async (ctx) => { 104 | const decoded = extractAuthHeader(ctx.req.headers); 105 | const userId = (decoded.payload as AuthPayload).sub; 106 | 107 | if (!isValidType(userId, TYPEID_USER)) { 108 | return ctx.json( 109 | { 110 | success: false, 111 | message: "Malformed user ID", 112 | }, 113 | 400, 114 | ); 115 | } 116 | 117 | const user = await getUserById(userId); 118 | if (!user) { 119 | return ctx.json( 120 | { 121 | success: false, 122 | message: "Invalid user ID", 123 | }, 124 | 400, 125 | ); 126 | } 127 | 128 | const expiresAt = dateToEpoch(user.expiresAt); 129 | return ctx.json({ 130 | success: true, 131 | user: { 132 | userId: user.userId, 133 | remainingQuota: user.remainingQuota, 134 | expiresAt: expiresAt, 135 | }, 136 | }); 137 | }); 138 | 139 | apiRouter.post( 140 | "generate", 141 | authMiddleware, 142 | zValidator("json", generateSchema, (result, ctx) => { 143 | if (!result.success) { 144 | // https://www.npmjs.com/package/zod-validation-error#arguments 145 | const errorMessage = fromZodError(result.error).toString(); 146 | 147 | return ctx.json( 148 | { 149 | success: false, 150 | message: errorMessage, 151 | }, 152 | 400, 153 | ); 154 | } 155 | }), 156 | async (ctx) => { 157 | const params = ctx.req.valid("json"); 158 | 159 | try { 160 | const decoded = extractAuthHeader(ctx.req.headers); 161 | const userId = (decoded.payload as AuthPayload).sub; 162 | 163 | if (!isValidType(userId, TYPEID_USER)) { 164 | return ctx.json( 165 | { 166 | success: false, 167 | message: "Malformed user ID", 168 | }, 169 | 400, 170 | ); 171 | } 172 | 173 | const user = await getUserById(userId); 174 | if (!user) { 175 | return ctx.json( 176 | { 177 | success: false, 178 | message: "Invalid user ID", 179 | }, 180 | 400, 181 | ); 182 | } 183 | 184 | if (user.remainingQuota <= 0) { 185 | return ctx.json( 186 | { 187 | success: false, 188 | message: "You have no remaining requests. Please check you plan.", 189 | }, 190 | 400, 191 | ); 192 | } 193 | 194 | if (params.task.type === "PxMobileProxy") { 195 | const app = APP_DATABASE[params.task.site]; 196 | const result = (await bakeCookie(app, params.task.proxy)) as ApiPxSolution; 197 | result.captchaToken = await createJwt({ 198 | sid: result.sid, 199 | vid: result.vid, 200 | }); 201 | 202 | const body: GenerateResponse = { 203 | success: true, 204 | solution: result, 205 | }; 206 | 207 | // noinspection ES6MissingAwait 208 | addUsage(userId); 209 | 210 | return ctx.json(body); 211 | } else if (params.task.type === "PxCaptcha") { 212 | const payload = (await verifyJwt(params.task.captchaToken)) as { 213 | sid?: string; 214 | vid?: string; 215 | }; 216 | 217 | if (payload === null) { 218 | return ctx.json( 219 | { 220 | success: false, 221 | message: "Unauthorized", 222 | }, 223 | 401, 224 | ); 225 | } 226 | 227 | if (payload.sid !== params.task.sid || payload.vid !== params.task.vid) { 228 | return ctx.json( 229 | { 230 | success: false, 231 | message: "Invalid parameters", 232 | }, 233 | 400, 234 | ); 235 | } 236 | 237 | console.log("payload:", payload); 238 | 239 | return ctx.json( 240 | { 241 | success: false, 242 | message: "Unimplemented", 243 | }, 244 | 400, 245 | ); 246 | } 247 | } catch (err) { 248 | if (err instanceof SSLPinException) { 249 | const errId = typeid(TYPEID_REFERENCE); 250 | logger.warn({ 251 | message: "Detected possible MITM attack! Proxy: " + err.message, 252 | env: env.NODE_ENV, 253 | ref: errId, 254 | }); 255 | } 256 | 257 | if (err instanceof GenerationException) { 258 | return ctx.json( 259 | { 260 | success: false, 261 | message: `An error occurred: ${err.message}`, 262 | }, 263 | 500, 264 | ); 265 | } 266 | 267 | if (err instanceof ProxyException) { 268 | return ctx.json( 269 | { 270 | success: false, 271 | message: `An error occurred while connecting to your proxy: "${err.message}". Please check your proxy and try again.`, 272 | }, 273 | 500, 274 | ); 275 | } 276 | 277 | throw err; 278 | } 279 | }, 280 | ); 281 | 282 | export default apiRouter; 283 | -------------------------------------------------------------------------------- /src/api/server.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { serve } from "@hono/node-server"; 3 | import { logger as honoLogger } from "hono/logger"; 4 | import { compress } from "hono/compress"; 5 | import { HTTPException } from "hono/http-exception"; 6 | import { BaseResponse } from "@src/api/types.js"; 7 | import { typeid } from "typeid-js"; 8 | import env from "@src/env.js"; 9 | import logger from "@src/api/logger.js"; 10 | import adminRouter from "@src/api/routes/admin.js"; 11 | import apiRouter from "@src/api/routes/api.js"; 12 | import { TYPEID_REFERENCE } from "@src/api/constants.js"; 13 | 14 | const app = new Hono({ 15 | strict: false, 16 | }); 17 | 18 | app.route("/admin", adminRouter); 19 | app.route("/api", apiRouter); 20 | 21 | app.notFound((ctx) => { 22 | return ctx.json( 23 | { 24 | success: false, 25 | message: "Not Found", 26 | }, 27 | 404, 28 | ); 29 | }); 30 | 31 | app.onError(async (err, ctx) => { 32 | if (err instanceof HTTPException) { 33 | const message = await err.getResponse().text(); 34 | return ctx.json( 35 | { 36 | success: false, 37 | message: message, 38 | }, 39 | err.status, 40 | ); 41 | } 42 | 43 | const headers: string[] = []; 44 | ctx.req.headers.forEach((value, key) => { 45 | if (key.includes("authorization")) { 46 | value = value.substring(0, 25) + "[...]"; 47 | } 48 | 49 | headers.push(`${key}: ${value}`); 50 | }); 51 | 52 | // Hono doesn't provide a way to get the incoming IP 53 | let ip = "unknown"; 54 | if (ctx.req.headers.get("cf-connecting-ip") !== null) { 55 | ip = ctx.req.headers.get("cf-connecting-ip") ?? "unknown"; 56 | } else if (ctx.req.headers.get("x-forwarded-for") !== null) { 57 | ip = ctx.req.headers.get("x-forwarded-for") ?? "unknown"; 58 | } 59 | 60 | const errId = typeid(TYPEID_REFERENCE); 61 | logger.error({ 62 | stacktrace: err.stack, 63 | request: { 64 | ip: ip, 65 | method: ctx.req.method, 66 | url: ctx.req.url, 67 | headers, 68 | body: await ctx.req.text(), 69 | }, 70 | env: env.NODE_ENV, 71 | ref: errId, 72 | }); 73 | 74 | return ctx.json( 75 | { 76 | success: false, 77 | message: `An internal server error has occurred. This problem has been logged and support has been notified. Ref#: ${errId}`, 78 | }, 79 | 500, 80 | ); 81 | }); 82 | 83 | if (env.COMPRESS_RESPONSE) { 84 | app.use("*", compress()); 85 | } 86 | 87 | // Print incoming requests to the console 88 | app.use( 89 | "*", 90 | honoLogger((msg) => { 91 | logger.trace(msg); 92 | }), 93 | ); 94 | 95 | // Middleware order matters, always do this first 96 | app.use("*", async (ctx, next) => { 97 | const start = Date.now(); 98 | await next(); 99 | const ms = Date.now() - start; 100 | ctx.header("X-Response-Time", `${ms}ms`); 101 | ctx.header("X-Notice", `By using this API you agree to be bound by its TOS`); 102 | }); 103 | 104 | app.get("/", (ctx) => { 105 | return ctx.json( 106 | { 107 | success: true, 108 | message: "https://tenor.com/view/hello-there-general-kenobi-star-wars-grevious-gif-17774326", 109 | }, 110 | 418, 111 | ); 112 | }); 113 | 114 | serve( 115 | { 116 | fetch: app.fetch, 117 | port: env.PORT, 118 | hostname: "0.0.0.0", 119 | }, 120 | (info) => { 121 | logger.trace(`Listening on http://localhost:${info.port}`); 122 | }, 123 | ); 124 | 125 | export default app; 126 | -------------------------------------------------------------------------------- /src/api/types.ts: -------------------------------------------------------------------------------- 1 | import { PxSolution } from "@src/px/cookies.js"; 2 | import { z } from "zod"; 3 | 4 | export interface BaseResponse { 5 | success: boolean; 6 | // errorMessage?: ErrorMessage; 7 | message?: string; 8 | } 9 | 10 | export interface AuthResponse extends BaseResponse { 11 | user: AuthUser; 12 | } 13 | 14 | export interface AddUserResponse extends BaseResponse { 15 | user: ApiUser; 16 | } 17 | 18 | export interface GenerateResponse extends BaseResponse { 19 | solution?: ApiPxSolution; 20 | } 21 | 22 | export interface ApiPxSolution extends PxSolution { 23 | captchaToken: string; 24 | } 25 | 26 | export interface UsageResponse extends BaseResponse { 27 | user: ApiUser; 28 | } 29 | 30 | export interface AuthPayload { 31 | iat: number; 32 | exp: number; 33 | sub: string; 34 | } 35 | 36 | export interface ApiUser { 37 | userId: string; 38 | apiKey?: string; 39 | remainingQuota?: number; 40 | expiresAt: number | null; 41 | } 42 | 43 | export interface AuthUser { 44 | userId: string; 45 | accessToken: string; 46 | expiresAt: number | null; 47 | } 48 | 49 | const ApiTask = z.object({ 50 | type: z.string(), 51 | }); 52 | 53 | const PXMobileTask = ApiTask.extend({ 54 | type: z.literal("PxMobileProxy"), 55 | proxy: z.string().url(), 56 | site: z.enum(["walmart", "hibbett", "snipes_usa", "snipes_eu", "shiekh", "stockx"]), 57 | }); 58 | 59 | const PXCaptcha = ApiTask.extend({ 60 | type: z.literal("PxCaptcha"), 61 | captchaToken: z.string(), 62 | sid: z.string().regex(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, "Invalid UUID"), 63 | vid: z.string().regex(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, "Invalid UUID"), 64 | }); 65 | 66 | // const DDCaptcha = ApiTask.extend({ 67 | // type: z.literal("DataDomeCaptcha"), 68 | // }); 69 | 70 | // const AkamaiMobileTask = ApiTask.extend({ 71 | // type: z.literal("AkamaiMobileProxy"), 72 | // testing: z.string(), 73 | // }); 74 | 75 | export const generateSchema = z.object({ 76 | task: z.union([PXMobileTask, PXCaptcha]), 77 | ref: z.string().uuid().optional(), 78 | }); 79 | 80 | export const authSchema = z.object({ 81 | apiKey: z.string(), 82 | }); 83 | 84 | const adminBaseSchema = z.object({ 85 | secret: z.string(), 86 | }); 87 | 88 | export const addUserSchema = adminBaseSchema.extend({ 89 | discordId: z.string().min(17, "Malformed Discord ID").max(20, "Malformed Discord ID"), 90 | requests: z.number().nonnegative(), 91 | expiresAt: z.string().datetime().optional().nullable(), 92 | }); 93 | 94 | export const resetUserSchema = adminBaseSchema.extend({ 95 | userId: z.string(), 96 | isDiscord: z.boolean().optional(), 97 | }); 98 | -------------------------------------------------------------------------------- /src/api/utils.ts: -------------------------------------------------------------------------------- 1 | import { Jwt } from "hono/utils/jwt"; 2 | import { AlgorithmTypes } from "hono/utils/jwt/types"; 3 | import env from "@src/env.js"; 4 | import { sha256 } from "@src/utils.js"; 5 | import { AuthPayload } from "@src/api/types.js"; 6 | import { TypeID } from "typeid-js"; 7 | 8 | export function extractAuthHeader(headers: Headers) { 9 | const jwt = headers.get("Authorization")?.toString().replace("Bearer ", "") ?? ""; 10 | return Jwt.decode(jwt); 11 | } 12 | 13 | export async function createAuthJwt(userId: string, expireMinutes: number = 43800) { 14 | const now = new Date(); 15 | const expiresDate = new Date(); 16 | expiresDate.setTime(expiresDate.getTime() + expireMinutes * 60 * 1000); 17 | 18 | const payload: AuthPayload = { 19 | iat: (now.getTime() / 1000) | 0, 20 | exp: (expiresDate.getTime() / 1000) | 0, 21 | sub: userId, 22 | }; 23 | 24 | return { 25 | token: await createJwt(payload), 26 | expiresAt: payload.exp, 27 | }; 28 | } 29 | 30 | export async function createJwt(payload: object) { 31 | return Jwt.sign(payload, env.API_SECRET, AlgorithmTypes.HS256); 32 | } 33 | 34 | export async function verifyJwt(jwt: string) { 35 | try { 36 | return await Jwt.verify(jwt, env.API_SECRET, AlgorithmTypes.HS256); 37 | } catch (e) { 38 | return null; 39 | } 40 | } 41 | 42 | export async function saltKey(apiKey: string) { 43 | return sha256(apiKey + env.API_SECRET); 44 | } 45 | 46 | export function dateToEpoch(date: Date | null) { 47 | return date === null ? null : (date.getTime() / 1000) | 0; 48 | } 49 | 50 | export function isValidType(id: string, typeId: string) { 51 | try { 52 | const testType = TypeID.fromString(id); 53 | return testType.getType() === typeId; 54 | } catch (e) { 55 | return false; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-core"; 2 | import { z } from "zod"; 3 | import { config } from "dotenv"; 4 | 5 | config({ 6 | path: `.env${process.env.NODE_ENV ? "." + process.env.NODE_ENV : ""}`, 7 | override: true, 8 | }); 9 | 10 | const env = createEnv({ 11 | server: { 12 | NODE_ENV: z.enum(["development", "production"]).default("development"), 13 | API_SECRET: z.string().min(32), 14 | AXIOM_DATASET: z.string().min(1).default("test"), 15 | AXIOM_TOKEN: z.string().startsWith("xaat-").length(41), 16 | PORT: z.coerce.number().int().nonnegative().lte(65535).default(3000), 17 | COMPRESS_RESPONSE: z.string().transform((str) => str !== "false" && str !== "0"), 18 | DATABASE_URL: z.string().url(), 19 | JWT_AUTH_MINUTES: z.coerce.number().default(60), 20 | }, 21 | runtimeEnv: process.env, 22 | }); 23 | export default env; 24 | -------------------------------------------------------------------------------- /src/http_client/TLSClient.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { Library, LibraryObject } from "ffi-napi"; 4 | import { 5 | TLSClientFetchCookiesForSessionRequestPayload, 6 | TLSClientFetchCookiesForSessionResponse, 7 | TLSClientInstance, 8 | TLSClientReleaseSessionPayload, 9 | TLSClientReleaseSessionResponse, 10 | TLSClientRequestPayload, 11 | TLSClientResponseData, 12 | } from "./types.js"; 13 | import { execSync } from "child_process"; 14 | import createDebugMessages from "debug"; 15 | import { existsSync } from "fs"; 16 | 17 | const debug = createDebugMessages("tls-client:client"); 18 | const __dirname = dirname(fileURLToPath(import.meta.url)); 19 | 20 | export default class TLSClient implements TLSClientInstance { 21 | private wrapper: LibraryObject | null; 22 | 23 | constructor(libVersion: string = "1.5.0") { 24 | this.wrapper = createWrapper(libVersion); 25 | } 26 | 27 | request(payload: TLSClientRequestPayload): TLSClientResponseData { 28 | const resp = this.wrapper.request(JSON.stringify(payload)); 29 | return JSON.parse(resp) as TLSClientResponseData; 30 | } 31 | 32 | async requestAsync(payload: TLSClientRequestPayload): Promise { 33 | return new Promise((resolve) => { 34 | this.wrapper.request.async(JSON.stringify(payload), (error: Error, response: string) => { 35 | const clientResponse: TLSClientResponseData = JSON.parse(response); 36 | 37 | resolve(clientResponse); 38 | }); 39 | }); 40 | } 41 | 42 | freeMemory(id: string): void { 43 | this.wrapper.freeMemory(id); 44 | } 45 | 46 | destroySession(sessionId: string): TLSClientReleaseSessionResponse { 47 | const payload: TLSClientReleaseSessionPayload = { 48 | sessionId, 49 | }; 50 | const resp = this.wrapper.destroySession(JSON.stringify(payload)); 51 | return JSON.parse(resp) as TLSClientReleaseSessionResponse; 52 | } 53 | 54 | async destroySessionAsync(sessionId: string): Promise { 55 | const payload: TLSClientReleaseSessionPayload = { 56 | sessionId, 57 | }; 58 | return new Promise((resolve) => { 59 | this.wrapper.destroySession.async(JSON.stringify(payload), (error: Error, response: string) => { 60 | const clientResponse: TLSClientReleaseSessionResponse = JSON.parse(response); 61 | 62 | resolve(clientResponse); 63 | }); 64 | }); 65 | } 66 | 67 | getCookiesFromSession(payload: TLSClientFetchCookiesForSessionRequestPayload): TLSClientFetchCookiesForSessionResponse { 68 | const resp = this.wrapper.getCookiesFromSession(JSON.stringify(payload)); 69 | return JSON.parse(resp) as TLSClientFetchCookiesForSessionResponse; 70 | } 71 | 72 | async getCookiesFromSessionAsync( 73 | payload: TLSClientFetchCookiesForSessionRequestPayload, 74 | ): Promise { 75 | return new Promise((resolve) => { 76 | this.wrapper.getCookiesFromSession.async(JSON.stringify(payload), (error: Error, response: string) => { 77 | const clientResponse: TLSClientFetchCookiesForSessionResponse = JSON.parse(response); 78 | 79 | resolve(clientResponse); 80 | }); 81 | }); 82 | } 83 | } 84 | 85 | const createWrapper = (libVersion: string): LibraryObject => { 86 | let sharedLibraryFilename; 87 | if (process.platform === "darwin") { 88 | // macOS 89 | sharedLibraryFilename = `tls-client-darwin-${process.arch === "x64" ? "amd64" : "arm64"}-${libVersion}.dylib`; 90 | } else if (process.platform === "win32") { 91 | sharedLibraryFilename = `tls-client-windows-${process.arch.replace("x", "")}-${libVersion}.dll`; 92 | } else if (process.platform === "linux") { 93 | const osRelease = execSync("cat /etc/*release*").toString(); 94 | 95 | // Check if Ubuntu or Alpine 96 | let prefix = ""; 97 | if (process.arch !== "arm64") { 98 | if (osRelease.includes("ID=ubuntu") || osRelease.includes("ID=debian")) { 99 | prefix = "ubuntu-"; 100 | } else if (osRelease.includes("ID=alpine")) { 101 | prefix = "alpine-"; 102 | } else { 103 | throw new Error(`Invalid OS Release: ${osRelease}`); 104 | } 105 | } 106 | 107 | sharedLibraryFilename = `tls-client-linux-${prefix}${process.arch === "x64" ? "amd64" : "arm64"}-${libVersion}.so`; 108 | } else { 109 | throw new Error("Invalid platform!"); 110 | } 111 | 112 | const libFile = join(__dirname, "../../lib", sharedLibraryFilename); 113 | debug(`Loading shared library: "${libFile}"`); 114 | if (!existsSync(libFile)) { 115 | throw new Error("Shared library not found!"); 116 | } 117 | 118 | return Library(libFile, { 119 | request: ["string", ["string"]], 120 | getCookiesFromSession: ["string", ["string"]], 121 | addCookiesToSession: ["string", ["string"]], 122 | freeMemory: ["void", ["string"]], 123 | destroyAll: ["string", []], 124 | destroySession: ["string", ["string"]], 125 | }); 126 | }; 127 | -------------------------------------------------------------------------------- /src/http_client/errors.ts: -------------------------------------------------------------------------------- 1 | export class TLSClientException extends Error { 2 | name = "TLSClientException"; 3 | } 4 | 5 | export class TimeoutException extends TLSClientException { 6 | message = "Connection timeout"; 7 | name = "TimeoutException"; 8 | } 9 | 10 | export class ProxyException extends TLSClientException { 11 | name = "ProxyException"; 12 | } 13 | 14 | export class SSLPinException extends TLSClientException { 15 | name = "SSLPinException"; 16 | } 17 | -------------------------------------------------------------------------------- /src/http_client/fetch_compat.ts: -------------------------------------------------------------------------------- 1 | import TLSClient from "@src/http_client/TLSClient.js"; 2 | import { FetchInit, TLSClientRequestPayload } from "@src/http_client/types.js"; 3 | import { randomUUID } from "crypto"; 4 | import createDebugMessages from "debug"; 5 | import { ProxyException, SSLPinException, TimeoutException, TLSClientException } from "@src/http_client/errors.js"; 6 | import { randomItem } from "@src/utils.js"; 7 | 8 | const debug = createDebugMessages("tls-client:fetch"); 9 | const tlsClient = new TLSClient(); 10 | 11 | export default async function fetch(url: string, init?: FetchInit): Promise { 12 | const sessionId = randomUUID(); 13 | const payload: TLSClientRequestPayload = { 14 | requestUrl: url, 15 | requestMethod: init?.method || "GET", 16 | requestBody: init?.body || "", 17 | headers: init?.headers, 18 | headerOrder: ["accept", "user-agent", "accept-encoding", "accept-language"], 19 | proxyUrl: init?.proxy || "", 20 | certificatePinningHosts: { 21 | "*.perimeterx.net": ["V5L96iSCz0XLFgvKi7YVo6M4SIkOP9zSkDjZ0EoU6b8="], 22 | }, 23 | 24 | // https://github.com/bogdanfinn/tls-client/blob/c35e858e739d5e5d9f17b513c3665c6c064a7b2a/profiles.go#L36 25 | tlsClientIdentifier: randomItem([ 26 | "okhttp4_android_7", 27 | "okhttp4_android_8", 28 | "okhttp4_android_9", 29 | "okhttp4_android_10", 30 | "okhttp4_android_11", 31 | "okhttp4_android_12", 32 | "okhttp4_android_13", 33 | // "okhttp4_android_11", 34 | // "mesh_android", 35 | // "confirmed_android", 36 | ]), 37 | 38 | // TODO: Custom tls 39 | // customTlsClient: { 40 | // // https://github.com/bogdanfinn/tls-client/blob/master/cffi_dist/example_node/index_custom_client.js 41 | // supportedVersions: ["1.3", "1.2"], 42 | // supportedSignatureAlgorithms: [ 43 | // "ECDSAWithP256AndSHA256" 44 | // ], 45 | // keyShareCurves: [ 46 | // "X25519", 47 | // "P256", 48 | // "P384", 49 | // ], 50 | // }, 51 | insecureSkipVerify: false, 52 | followRedirects: true, 53 | sessionId: sessionId, 54 | timeoutSeconds: 20, 55 | withoutCookieJar: true, 56 | }; 57 | debug("request: %O", payload); 58 | 59 | const resp = await tlsClient.requestAsync(payload); 60 | debug("response: %O", resp); 61 | 62 | // Free request 63 | tlsClient.freeMemory(resp.id); 64 | tlsClient.destroySession(sessionId); 65 | 66 | // Return result 67 | if (resp.status === 0) { 68 | if (resp.body.includes("No connection could be made because the target machine actively refused it.")) { 69 | throw new ProxyException("Couldn't connect to proxy"); 70 | } else if (resp.body.includes("failed to build client out of request input")) { 71 | throw new ProxyException("Malformed proxy"); 72 | } else if (resp.body.includes("Proxy responded with non 200 code")) { 73 | throw new ProxyException("Invalid proxy credentials: " + resp.body); 74 | } else if (resp.body.includes("context deadline exceeded (Client.Timeout exceeded while awaiting headers)")) { 75 | throw new TimeoutException(); 76 | } else if (resp.body.includes("bad ssl pin detected")) { 77 | throw new SSLPinException(init?.proxy || ""); 78 | } else { 79 | throw new TLSClientException(resp.body); 80 | } 81 | } 82 | 83 | const headers: [string, string][] = []; 84 | if (resp.headers) { 85 | for (const [header, value] of Object.entries(resp.headers)) { 86 | for (const h of value) { 87 | headers.push([header, h]); 88 | } 89 | } 90 | } 91 | 92 | return new Response(resp.body, { 93 | headers: headers, 94 | status: resp.status, 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /src/http_client/types.ts: -------------------------------------------------------------------------------- 1 | type TLSClientIdentifier = 2 | | "chrome_103" 3 | | "chrome_104" 4 | | "chrome_105" 5 | | "chrome_106" 6 | | "chrome_107" 7 | | "chrome_108" 8 | | "chrome_109" 9 | | "chrome_110" 10 | | "chrome_111" 11 | | "chrome_112" 12 | | "safari_15_6_1" 13 | | "safari_16_0" 14 | | "safari_ipad_15_6" 15 | | "safari_ios_15_5" 16 | | "safari_ios_15_6" 17 | | "safari_ios_16_0" 18 | | "firefox_102" 19 | | "firefox_104" 20 | | "firefox_105" 21 | | "firefox_106" 22 | | "firefox_108" 23 | | "firefox_110" 24 | | "opera_89" 25 | | "opera_90" 26 | | "opera_91" 27 | | "zalando_android_mobile" 28 | | "zalando_ios_mobile" 29 | | "nike_ios_mobile" 30 | | "nike_android_mobile" 31 | | "cloudscraper" 32 | | "mms_ios" 33 | | "mesh_ios" 34 | | "mesh_ios_1" 35 | | "mesh_ios_2" 36 | | "mesh_android" 37 | | "mesh_android_1" 38 | | "mesh_android_2" 39 | | "confirmed_ios" 40 | | "confirmed_android" 41 | | "okhttp4_android_7" 42 | | "okhttp4_android_8" 43 | | "okhttp4_android_9" 44 | | "okhttp4_android_10" 45 | | "okhttp4_android_11" 46 | | "okhttp4_android_12" 47 | | "okhttp4_android_13"; 48 | type TLSClientRequestMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; 49 | 50 | export interface TLSClientInstance { 51 | request: (payload: TLSClientRequestPayload) => TLSClientResponseData; 52 | requestAsync: (payload: TLSClientRequestPayload) => Promise; 53 | getCookiesFromSession: (payload: TLSClientFetchCookiesForSessionRequestPayload) => TLSClientFetchCookiesForSessionResponse; 54 | getCookiesFromSessionAsync: ( 55 | payload: TLSClientFetchCookiesForSessionRequestPayload, 56 | ) => Promise; 57 | destroySession: (sessionId: string) => TLSClientReleaseSessionResponse; 58 | destroySessionAsync: (sessionId: string) => Promise; 59 | } 60 | 61 | export interface TLSClientRequestPayload { 62 | catchPanics?: boolean; 63 | // WithCertificatePinning enables SSL Pinning for the client and will throw an error if the SSL Pin is not matched. 64 | // Please refer to https://github.com/tam7t/hpkp/#examples in order to see how to generate pins. The certificatePins are a map with the host as key. 65 | certificatePinningHosts?: { [key: string]: string[] }; 66 | customTlsClient?: TLSCustomClient; 67 | followRedirects?: boolean; 68 | forceHttp1?: boolean; 69 | headerOrder?: string[]; 70 | headers?: { [key: string]: string }; 71 | insecureSkipVerify?: boolean; 72 | isByteRequest?: boolean; 73 | isByteResponse?: boolean; 74 | isRotatingProxy?: boolean; 75 | disableIPV6?: boolean; 76 | localAddress?: string; 77 | proxyUrl?: string; 78 | requestBody: string; 79 | requestCookies?: { [key: string]: string }[]; 80 | requestMethod: TLSClientRequestMethod; 81 | requestUrl: string; 82 | sessionId?: string; 83 | streamOutputBlockSize?: number; 84 | streamOutputEOFSymbol?: string; 85 | streamOutputPath?: string; 86 | timeoutMilliseconds?: number; 87 | timeoutSeconds?: number; 88 | tlsClientIdentifier?: TLSClientIdentifier; 89 | withDebug?: boolean; 90 | withDefaultCookieJar?: boolean; 91 | withoutCookieJar?: boolean; 92 | withRandomTLSExtensionOrder?: boolean; 93 | } 94 | 95 | // https://github.com/bogdanfinn/tls-client/blob/master/mapper.go 96 | // https://github.com/bogdanfinn/tls-client/blob/master/custom_profiles.go 97 | // https://github.com/bogdanfinn/tls-client/blob/c35e858e739d5e5d9f17b513c3665c6c064a7b2a/cffi_src/types.go#L82 98 | // https://bogdanfinn.gitbook.io/open-source-oasis/shared-library/payload 99 | export interface TLSCustomClient { 100 | certCompressionAlgo?: CertCompressionAlgo; 101 | connectionFlow?: number; 102 | h2Settings: TLSH2Settings; 103 | h2SettingsOrder?: TLSHTTPSettingID[]; 104 | headerPriority?: TLSPriorityParam; 105 | ja3String?: string; 106 | keyShareCurves?: CurveID[]; 107 | priorityFrames?: TLSPriorityFrames[]; 108 | pseudoHeaderOrder?: string[]; 109 | supportedDelegatedCredentialsAlgorithms?: SignatureScheme[]; 110 | supportedSignatureAlgorithms?: SignatureScheme[]; 111 | supportedVersions?: TLSVersions[]; 112 | } 113 | 114 | export interface TLSH2Settings { 115 | HEADER_TABLE_SIZE: number; 116 | ENABLE_PUSH: number; 117 | MAX_CONCURRENT_STREAMS: number; 118 | INITIAL_WINDOW_SIZE: number; 119 | MAX_FRAME_SIZE: number; 120 | MAX_HEADER_LIST_SIZE: number; 121 | } 122 | 123 | export type TLSHTTPSettingID = keyof TLSH2Settings; 124 | 125 | export type TLSVersions = "GREASE" | "1.3" | "1.2" | "1.1" | "1.0"; 126 | 127 | export type SignatureScheme = 128 | | "PKCS1WithSHA256" 129 | | "PKCS1WithSHA384" 130 | | "PKCS1WithSHA512" 131 | | "PSSWithSHA256" 132 | | "PSSWithSHA384" 133 | | "PSSWithSHA512" 134 | | "ECDSAWithP256AndSHA256" 135 | | "ECDSAWithP384AndSHA384" 136 | | "ECDSAWithP521AndSHA512" 137 | | "PKCS1WithSHA1" 138 | | "ECDSAWithSHA1" 139 | | "Ed25519"; 140 | 141 | export type CurveID = "GREASE" | "P256" | "P384" | "P521" | "X25519"; 142 | 143 | export type CertCompressionAlgo = "zlib" | "brotli" | "zstd"; 144 | 145 | export interface TLSPriorityFrames { 146 | priorityParam: TLSPriorityParam; 147 | streamID: number; 148 | } 149 | 150 | export interface TLSPriorityParam { 151 | exclusive: boolean; 152 | streamDep: number; 153 | weight: number; 154 | } 155 | 156 | export interface TLSClientResponseData { 157 | id: string; 158 | sessionId: string; 159 | status: number; 160 | target: string; 161 | body: string; 162 | headers: { [key: string]: string[] } | null; 163 | cookies: { [key: string]: string } | null; 164 | } 165 | 166 | export interface TLSClientReleaseSessionPayload { 167 | sessionId: string; 168 | } 169 | 170 | export type TLSClientReleaseSessionResponse = { 171 | success: boolean; 172 | }; 173 | 174 | export interface TLSClientFetchCookiesForSessionRequestPayload { 175 | sessionId: string; 176 | url: string; 177 | } 178 | 179 | export type TLSClientFetchCookiesForSessionResponse = Cookie[]; 180 | 181 | export interface Cookie { 182 | Name: string; 183 | Value: string; 184 | Path: string; 185 | Domain: string; 186 | Expires: string; 187 | RawExpires: string; 188 | MaxAge: number; 189 | Secure: boolean; 190 | HttpOnly: boolean; 191 | SameSite: number; 192 | Raw: string; 193 | Unparsed: string; 194 | } 195 | 196 | export interface FetchInit { 197 | body?: string; 198 | headers?: { [key: string]: string }; 199 | method?: TLSClientRequestMethod; 200 | proxy?: string; 201 | } 202 | -------------------------------------------------------------------------------- /src/px/appc2.ts: -------------------------------------------------------------------------------- 1 | export default function getChallengeResultFromString(appc2: string, model: string) { 2 | const parts = appc2.split("|").slice(1); 3 | 4 | // const ts = parseInt(parts[1]); // getTs 5 | // const signed = parts[2]; // getSigned 6 | const part3 = parseInt(parts[3]); // part3 7 | const part4 = parseInt(parts[4]); // part4 8 | const part5 = parseInt(parts[5]); // part5 9 | const part6 = parseInt(parts[6]); // part6 10 | const part7 = parseInt(parts[7]); // part7 11 | const part8 = parseInt(parts[8]); // part8 12 | 13 | return getChallengeResult(part5, part6, part7, part3, part4, part8, model); 14 | } 15 | 16 | function getChallengeResult( 17 | part5: number, 18 | part6: number, 19 | part7: number, 20 | part3: number, 21 | part4: number, 22 | part8: number, 23 | model: string, 24 | ) { 25 | // model = Build.MODEL 26 | const result = doRound(doRound(part5, part6, part3, part8), part7, part4, part8) ^ deviceModelAsInt(model); 27 | return result.toString(); 28 | } 29 | 30 | function doRound(i: number, i2: number, i3: number, i4: number) { 31 | const i5 = i4 % 10; 32 | const i6 = i * i; 33 | const i7 = i2 * i2; 34 | 35 | switch (i5 != 0 ? i3 % i5 : i3 % 10) { 36 | case 0: 37 | return i6 + i2; 38 | case 1: 39 | return i + i7; 40 | case 2: 41 | return i6 * i2; 42 | case 3: 43 | return i ^ i2; 44 | case 4: 45 | return i - i7; 46 | case 5: 47 | // eslint-disable-next-line no-case-declarations 48 | const i8 = i + 783; 49 | return i8 * i8 + i7; 50 | case 6: 51 | return (i ^ i2) + i2; 52 | case 7: 53 | return i6 - i7; 54 | case 8: 55 | return i * i2; 56 | case 9: 57 | return i * i2 - i; 58 | default: 59 | return -1; 60 | } 61 | } 62 | 63 | // function deviceModelAsInt(model: string) { 64 | // const bArr = new TextEncoder().encode(model) 65 | // if(bArr.length < 4) { 66 | // console.log("ZERO"); 67 | // return 0; 68 | // } 69 | // 70 | // const dataView = new DataView(bArr.buffer); 71 | // return dataView.getUint32(0); 72 | // } 73 | 74 | function deviceModelAsInt(model: string) { 75 | // const a = Buffer.from(model); 76 | const a = new TextEncoder().encode(model); 77 | return ToInt32(a.slice(0, 4).reverse()); 78 | } 79 | 80 | // function ToInt32(buffer: Buffer) { 81 | function ToInt32(buffer: Uint8Array) { 82 | return (buffer[0] | (buffer[1] << 8) | (buffer[2] << 16) | (buffer[3] << 24)) >>> 0; 83 | } 84 | -------------------------------------------------------------------------------- /src/px/constants.ts: -------------------------------------------------------------------------------- 1 | import { parse as parseSemVer, SemVer } from "semver"; 2 | 3 | export const DoEnum = { 4 | CHALLENGE: "appc", 5 | SID: "sid", 6 | VID: "vid", 7 | BAKE: "bake", 8 | } as const; 9 | 10 | export const PxCookieNames = { 11 | _px: 1, 12 | _px2: 2, 13 | _px3: 3, 14 | } as const; 15 | 16 | export const SDK_VERSIONS = { 17 | "1.15.0": parseSemVer("1.15.0")!, 18 | "1.15.2": parseSemVer("1.15.2")!, 19 | "1.13.1": parseSemVer("1.13.1")!, 20 | "2.2.0": parseSemVer("2.2.0")!, 21 | "2.2.2": parseSemVer("2.2.2")!, 22 | "2.2.1": parseSemVer("2.2.1")!, 23 | "3.0.0": parseSemVer("3.0.0")!, 24 | "3.0.5": parseSemVer("3.0.5")!, 25 | } as const; 26 | 27 | export const APP_DATABASE = { 28 | hibbett: { 29 | sdkNumber: SDK_VERSIONS["1.15.2"], 30 | appName: "Hibbett | City Gear", 31 | appVersion: "6.5.0", 32 | bundleID: "com.hibbett.android", 33 | userAgent: "Hibbett Sports/6.5.0 (redroid11_arm64; android 11)", // TODO: replacements 34 | appId: "PX9Qx3Rve4", 35 | }, 36 | snipes_usa: { 37 | sdkNumber: SDK_VERSIONS["1.13.1"], 38 | appName: "Snipes", 39 | appVersion: "21.0.4.7-10-g4959b67", 40 | bundleID: "com.shopgate.android.app22760", 41 | userAgent: "Snipes/21.0.4 Android/{VERSION_CODES}", 42 | appId: "PX6XNN2xkk", 43 | }, 44 | snipes_eu: { 45 | sdkNumber: SDK_VERSIONS["2.2.2"], 46 | appName: "SNIPES", 47 | appVersion: "2.2.3", 48 | bundleID: "com.snipes", 49 | userAgent: "SnipesApp/2.2.3 (android; unknown)", 50 | appId: "PXszbF5p84", 51 | }, 52 | bed_bath: { 53 | sdkNumber: SDK_VERSIONS["2.2.1"], 54 | appName: "My B&BW", 55 | appVersion: "5.4.1.29", 56 | bundleID: "com.bathandbody.bbw", 57 | userAgent: "TODO", 58 | appId: "PXlsXlyYa5", 59 | }, 60 | walmart: { 61 | sdkNumber: SDK_VERSIONS["2.2.2"], 62 | appName: "Walmart", 63 | appVersion: "23.19", 64 | bundleID: "com.walmart.android", 65 | appId: "PXUArm9B04", 66 | userAgent: "WMT1H/23.19 Android/{BUILD_VERSION}", 67 | batteryString: "li.a@9b3a65c", 68 | }, 69 | chegg: { 70 | sdkNumber: SDK_VERSIONS["1.15.0"], 71 | appName: "Chegg Study", 72 | appVersion: "13.31.1", 73 | bundleID: "com.chegg", 74 | appId: "PXaOtQIWNf", 75 | userAgent: "Chegg Study/13.31.1 (Linux; U; Android 11; redroid11_arm64 Build/RD2A.211001.002)", 76 | extraData: ["Android", "Chegg Study", "13.31.1"], 77 | }, 78 | shiekh: { 79 | sdkNumber: SDK_VERSIONS["3.0.5"], 80 | appName: "Shiekh", 81 | appVersion: "10.16", 82 | bundleID: "com.shiekh.android", 83 | appId: "PXM2JHbdkv", 84 | userAgent: "okhttp/4.10.0", 85 | }, 86 | textfree: { 87 | sdkNumber: SDK_VERSIONS["2.2.0"], 88 | appName: "TextNow", 89 | appVersion: "23.27.1.0", 90 | bundleID: "com.enflick.android.TextNow", 91 | appId: "PXK56WkC4O", 92 | // userAgent: "TextNow 23.27.1.0 (redroid11_arm64; Android OS 11; en_US); TextNow-API 3.133", 93 | userAgent: "TextNow/23.27.1 (iPhone12,1; iOS 15.2; Scale/2.00)", 94 | batteryString: "ji.b@a421ef6", 95 | }, 96 | stockx: { 97 | sdkNumber: SDK_VERSIONS["1.15.2"], 98 | appName: "StockX", 99 | appVersion: "4.14.43", 100 | bundleID: "com.stockx.stockx", 101 | appId: "PX16uD0kOF", 102 | userAgent: "TODO", 103 | }, 104 | } as const satisfies { [key: string]: PxApp }; 105 | 106 | export const NETWORK_TYPES = ["3G", "4G"] as const; 107 | export const NETWORK_CARRIERS = [ 108 | "T-Mobile", 109 | "Vodafone", 110 | "Msg2Send", 111 | "Mobitel", 112 | "Cequens", 113 | "Vodacom", 114 | "MTN", 115 | "Meteor", 116 | "Movistar", 117 | "Swisscom", 118 | "Orange", 119 | "Unite", 120 | "Oxygen8", 121 | "Txtlocal", 122 | "TextOver", 123 | "Virgin-Mobile", 124 | "Aircel", 125 | "AT&T", 126 | "Cellcom", 127 | "BellSouth", 128 | "Cleartalk", 129 | "Cricket", 130 | "DTC", 131 | "nTelos", 132 | "Esendex", 133 | "Kajeet", 134 | "LongLines", 135 | "MetroPCS", 136 | "Nextech", 137 | "SMS4Free", 138 | "Solavei", 139 | "Southernlinc", 140 | "Sprint", 141 | "Teleflip", 142 | "Unicel", 143 | "Viaero", 144 | "UTBox", 145 | ] as const; 146 | 147 | export const PX_COLLECTOR_TEMPLATE = "https://collector-{APP_ID}.perimeterx.net/api/v1/collector/mobile"; 148 | 149 | export interface PxApp { 150 | appName: string; 151 | appVersion: string; 152 | bundleID: string; 153 | appId: string; 154 | sdkNumber: SemVer; 155 | batteryString?: string; 156 | userAgent: string; 157 | 158 | // https://docs.perimeterx.com/docs/android-sdk-react-native-integration-guide#adding-custom-parameters 159 | // Max 10 160 | extraData?: ReadonlyArray; 161 | } 162 | -------------------------------------------------------------------------------- /src/px/cookies.ts: -------------------------------------------------------------------------------- 1 | import { generateNewPXUUID, generatePXUUID } from "@src/px/uuid.js"; 2 | import { randomFloat, randomInt, randomItem, sha1 } from "@src/utils.js"; 3 | import getChallengeResultFromString from "@src/px/appc2.js"; 4 | import { 5 | DoEnum, 6 | NETWORK_CARRIERS, 7 | NETWORK_TYPES, 8 | PX_COLLECTOR_TEMPLATE, 9 | PxApp, 10 | PxCookieNames, 11 | SDK_VERSIONS, 12 | } from "./constants.js"; 13 | import { DeviceFingerprint, getRandomDevice } from "@src/px/device.js"; 14 | import { randomBytes, randomUUID } from "crypto"; 15 | import { writeFile } from "fs/promises"; 16 | import { URLSearchParams } from "url"; 17 | import fetch from "@src/http_client/fetch_compat.js"; 18 | import createDebugMessages from "debug"; 19 | import env from "@src/env.js"; 20 | import { GenerationException } from "@src/px/errors.js"; 21 | 22 | const debug = createDebugMessages("px:cookie"); 23 | const DEBUG_PAYLOAD = env.NODE_ENV === "development"; 24 | 25 | export default async function bakeCookie(app: PxApp, proxy: string) { 26 | const startTime = performance.now(); 27 | 28 | const PX_ENDPOINT = PX_COLLECTOR_TEMPLATE.replace("{APP_ID}", app.appId.toLowerCase()); 29 | const USER_AGENT = "PerimeterX Android SDK/" + app.sdkNumber.version; 30 | const device = await getRandomDevice(); 31 | const payload1 = await getInitPayload(device, app); 32 | debug("Payload1: %O", payload1[0].d); 33 | 34 | if (DEBUG_PAYLOAD) { 35 | await writeFile("./research/payloads/payload1.txt", encodePayload(payload1), "utf8"); 36 | } 37 | 38 | // Only use new uuid method on versions newer than 2.2.0 39 | let uuidReq; 40 | if (app.sdkNumber.compare(SDK_VERSIONS["2.2.0"]) >= 0) { 41 | uuidReq = randomUUID(); 42 | } else { 43 | uuidReq = generatePXUUID(); 44 | } 45 | 46 | const resp = await fetch(PX_ENDPOINT, { 47 | method: "POST", 48 | body: new URLSearchParams( 49 | addExtraData( 50 | { 51 | payload: encodePayload(payload1), 52 | uuid: uuidReq, 53 | appId: app.appId, 54 | tag: "mobile", 55 | ftag: "22", 56 | }, 57 | app, 58 | ), 59 | ).toString(), 60 | headers: { 61 | "user-agent": USER_AGENT, 62 | // "content-type": "application/x-www-form-urlencoded", // older 63 | 64 | "accept-charset": "UTF-8", 65 | accept: "*/*", 66 | "content-type": "application/x-www-form-urlencoded; charset=utf-8", 67 | "accept-encoding": "gzip", 68 | }, 69 | proxy: proxy, 70 | }); 71 | const content = (await resp.json()) as PxResponse; 72 | debug("DO: %O", content.do); 73 | 74 | // TODO: Check if contains DoEnum.BAKE on first request - means HoldCaptcha 75 | 76 | let sid = ""; 77 | let vid = ""; 78 | for (const item of content.do) { 79 | const parts = item.split("|"); 80 | const type = parts[0]; 81 | 82 | if (type === DoEnum.SID) { 83 | sid = parts[1]; 84 | // debug("SID: %O", sid); 85 | } else if (type === DoEnum.VID) { 86 | vid = parts[1]; 87 | // debug("VID: %O", vid); 88 | } else if (type === DoEnum.CHALLENGE) { 89 | const index = parseInt(parts[1]); 90 | const timestamp = parseInt(parts[2]); 91 | 92 | // eslint-disable-next-line no-empty 93 | if (index === 1) { 94 | } else if (index === 2) { 95 | const signed = parts[3]; 96 | 97 | const challengeResult = getChallengeResultFromString(item, device.productModel); 98 | const payload2 = await getPayload2(app, payload1, signed, challengeResult, timestamp); 99 | if (DEBUG_PAYLOAD) { 100 | await writeFile("./research/payloads/payload2.txt", encodePayload(payload2), "utf8"); 101 | } 102 | 103 | // TODO: This is stupid, don't split twice, once above and once in appc 104 | 105 | debug("Parsed: %O", { 106 | appc2: { 107 | challengeResult, 108 | signed, 109 | }, 110 | sid, 111 | vid, 112 | }); 113 | 114 | debug("Payload2: %O", payload2[0].d); 115 | 116 | const resp2 = await fetch(PX_ENDPOINT, { 117 | method: "POST", 118 | body: new URLSearchParams( 119 | addExtraData( 120 | { 121 | payload: encodePayload(payload2), 122 | uuid: uuidReq, 123 | appId: app.appId, 124 | tag: "mobile", 125 | ftag: "22", 126 | sid: sid, 127 | vid: vid, 128 | }, 129 | app, 130 | ), 131 | ).toString(), 132 | headers: { 133 | "user-agent": USER_AGENT, 134 | // "content-type": "application/x-www-form-urlencoded", // older 135 | 136 | "accept-charset": "UTF-8", 137 | accept: "*/*", 138 | "content-type": "application/x-www-form-urlencoded; charset=utf-8", 139 | "accept-encoding": "gzip", 140 | }, 141 | proxy: proxy, 142 | }); 143 | const content2 = (await resp2.json()) as PxResponse; 144 | debug("content2 (%d): %O", resp2.status, content2); 145 | 146 | if (content2.do.length > 0 && content2.do[0].includes("bake")) { 147 | const cookieParts = content2.do[0].split("|"); 148 | 149 | if (!(cookieParts[1] in PxCookieNames)) { 150 | throw new Error("Invalid cookie type"); 151 | } 152 | 153 | // Check for _px2 or _px3 154 | const cookieVersion = PxCookieNames[cookieParts[1] as keyof typeof PxCookieNames]; 155 | const cookie = cookieVersion + ":" + cookieParts[3]; 156 | debug("PX cookie: %O", cookie); 157 | 158 | // const userAgent = app.userAgent 159 | // .replace("{BUILD_VERSION}", "idk") 160 | // .replace("VERSION_CODES", ""); 161 | 162 | const headers: PxSolutionHeaders = { 163 | "user-agent": app.userAgent, 164 | "x-px-authorization": cookie, 165 | }; 166 | 167 | if (app.sdkNumber.compare(SDK_VERSIONS["3.0.0"]) >= 0) { 168 | headers["x-px-os-version"] = device.buildVersionCode; 169 | headers["x-px-uuid"] = uuidReq; 170 | headers["x-px-device-fp"] = payload1[0].d.PX1214 ?? "ERROR"; 171 | headers["x-px-device-model"] = device.productModel; // Build.MODEL 172 | headers["x-px-os"] = "Android"; 173 | // Huh, version 2 token, but version 3 for hello 174 | headers["x-px-hello"] = encodePxHello(uuidReq, "3"); 175 | headers["x-px-mobile-sdk-version"] = app.sdkNumber.version; 176 | // "x-px-authorization": "1", 177 | } 178 | 179 | const result: PxSolution = { 180 | site: app.appId, 181 | solveTime: (performance.now() - startTime) | 0, 182 | sid, 183 | vid, 184 | 185 | // Return v3 headers 186 | headers: headers, 187 | }; 188 | 189 | return result; 190 | } else { 191 | throw new GenerationException("Failed! " + JSON.stringify(content2)); 192 | } 193 | } 194 | } 195 | } 196 | 197 | throw new GenerationException("An unknown error occurred while generating a PX cookie. Please try again later."); 198 | } 199 | 200 | async function getInitPayload(device: DeviceFingerprint, app: PxApp) { 201 | const isV2 = app.sdkNumber.compare(SDK_VERSIONS["2.2.0"]) >= 0; 202 | const timestamp = (Date.now() / 1000) | 0; 203 | const fingerprints = await createFingerprints(device, isV2); 204 | 205 | // PX414 206 | // https://developer.android.com/reference/android/os/BatteryManager 207 | // (0): "" 208 | // (1): unknown 209 | // BATTERY_STATUS_CHARGING (2): charging 210 | // BATTERY_STATUS_DISCHARGING (3): discharging 211 | // BATTERY_STATUS_NOT_CHARGING (4): not charging 212 | // BATTERY_STATUS_FULL (5): full 213 | 214 | const data: PxData = { 215 | // PX350: 1 // manager_ready_time_interval - TODO: on later 216 | PX91: device.width, // screen.width 217 | PX92: device.height, // screen.height 218 | PX316: true, // sim_support 219 | // PX345: 0, // resume_counter - increased every time app is opened/swap to 220 | // PX351: 1, // app_active_time_interval 221 | PX345: randomInt(0, 2), // resume_counter - increased every time app is opened/swap to 222 | PX351: randomInt(0, 86000), // app_active_time_interval 223 | PX317: "wifi", // connection_type 224 | PX318: device.buildVersionSdk, // device_os_version 225 | PX319: device.buildVersionRelease, // TODO: device_kernel_version 226 | PX320: device.productModel, // device_model 227 | PX323: timestamp, // unix_time 228 | PX326: fingerprints.fingerprint1, // "fd1bf990-f347-11ed-9675-00001d291f30" 229 | PX327: fingerprints.fingerprint2, // "FD1BF991" 230 | PX328: fingerprints.fingerprint3, // "09D6DC091783DED4B218A01012C029BDD29E055D" 231 | PX337: true, // sensor_gps 232 | PX336: true, // sensor_gyroscope 233 | PX335: true, // sensor_accelrometer 234 | PX334: false, // sensor_ethernet 235 | PX333: true, // sensor_touchscreen 236 | PX331: true, // sensor_nfc 237 | PX332: true, // sensor_wifi 238 | PX330: "new_session", // app_state 239 | PX421: "false", // is_rooted 240 | PX442: "false", // is_test_keys 241 | PX339: device.productManufacturer, // device_type 242 | PX322: "Android", // device_os_name 243 | PX340: `v${app.sdkNumber.version}`, // sdk_version 244 | PX341: app.appName, // app_name 245 | PX342: app.appVersion, // app_version 246 | PX348: app.bundleID, // app_identifier 247 | PX343: randomItem(NETWORK_TYPES), // network 248 | PX344: randomItem(NETWORK_CARRIERS), // carrier 249 | PX347: ["en_US"], // device_supported_languages 250 | PX413: "good", // battery_health 251 | PX414: "not charging", // battery_status, USB 252 | PX415: randomInt(25, 95), // battery_level 253 | PX416: "None", // battery_plugged, (v3: USB) 254 | PX419: "Li-ion", // battery_technology 255 | PX418: parseFloat(randomFloat(20, 25).toFixed(2)), // Float.valueOf(battery_temperature) 256 | PX420: parseFloat(randomFloat(3, 4).toFixed(3)), // Float.valueOf(battery_voltage) 257 | // PX420: parseFloat(randomItem([4.2, 4.1, 4.0, 3.9, 3.8, 3.7, 3.6, 3.5]).toFixed(3)), // Float.valueOf(battery_voltage) 258 | // PX418: parseInt(randomFloat(20, 25).toFixed(2)), // battery_temperature 259 | // PX420: parseInt(randomItem([4.2, 4.1, 4.0, 3.9, 3.8, 3.7, 3.6, 3.5]).toFixed(3)), // battery_voltage 260 | }; 261 | 262 | if (app.sdkNumber.compare(SDK_VERSIONS["1.15.2"]) >= 0) { 263 | data.PX1159 = false; // is_instant_app - TODO: Not used in older versions 264 | } 265 | 266 | if (isV2) { 267 | data.PX1208 = "[]"; // unknown_idk 268 | data.PX1214 = createAndroidId(); // androidId 269 | data.PX317 = "WiFi"; // connection_type 270 | data.PX321 = device.productDevice; // device_name 271 | data.PX341 = `"${app.appName}"`; // app_name 272 | data.PX347 = "[en_US]"; // device_supported_languages 273 | data.PX416 = ""; // battery_plugged 274 | data.PX419 = app.batteryString ?? "li.a@9b3a65c"; // battery_technology - walmart 275 | } 276 | 277 | if (app.sdkNumber.compare(SDK_VERSIONS["3.0.0"]) >= 0) { 278 | data.PX347 = '["en_US"]'; // device_supported_languages 279 | data.PX419 = ""; // battery_technology 280 | data.PX21215 = randomInt(10, 255); // screen_brightness (0-255): https://developer.android.com/reference/android/provider/Settings.System#SCREEN_BRIGHTNESS 281 | // data.PX21217 = "[]"; // TODO: device_motion_datas 282 | // data.PX21218 = "[]"; // TODO: touch_datas 283 | data.PX21217 = getMotionData(); 284 | data.PX21218 = getTouchData(device); 285 | data.PX21219 = "{}"; // additional_data - always empty ("{}") 286 | data.PX21221 = "true"; // unknown_always_true 287 | } 288 | 289 | return [ 290 | { 291 | t: "PX315", 292 | d: data, 293 | }, 294 | ] as PxPayload[]; 295 | } 296 | 297 | function getMotionData() { 298 | // "x,y,z,timestamp" 299 | // return '["-49,-1,-57,31","-49,-2,-58,31","-50,-2,-58,31","-49,-3,-58,31","-49,-3,-60,31","-49,-4,-60,31","-49,-4,-59,31","-50,-4,-59,31","-50,-4,-61,31","-50,-4,-60,31","-50,-3,-60,31","-50,-3,-59,31","-50,-2,-59,31","-50,-2,-58,31","-50,-2,-59,31","-50,-2,-58,31","-50,-1,-58,31","-50,-1,-59,31","-50,-1,-58,31","-50,0,-58,31","-50,0,-57,31","-50,0,-58,31","-50,0,-57,31","-50,0,-58,31","-50,0,-57,31","-50,0,-58,31","-50,0,-57,31","-50,0,-58,31","-50,-1,-58,31","-50,-2,-58,31","-50,-2,-59,31","-50,-3,-59,31","-50,-3,-60,31","-50,-2,-60,31","-50,-2,-59,31","-49,-2,-59,31","-49,-2,-60,31","-50,-1,-59,31","-50,-1,-58,31","-49,-1,-58,31","-49,-1,-59,31","-49,-1,-58,31","-50,-1,-58,31","-50,-1,-59,31","-50,-1,-58,31","-50,-2,-58,31","-50,-1,-58,31","-50,-1,-59,31","-50,-1,-58,31","-50,-1,-59,31"]'; 300 | return "[]"; 301 | } 302 | 303 | function getTouchData(device: DeviceFingerprint) { 304 | const results: string[] = []; 305 | let timestamp = 200; 306 | for (let i = 0; i < 10; i++) { 307 | const x = randomInt(0, device.width); 308 | const y = randomInt(0, device.height); 309 | timestamp += randomInt(0, 25); 310 | 311 | // "x,y,timestamp" 312 | results.push([x, y, timestamp].join(",")); 313 | } 314 | 315 | return JSON.stringify(results); 316 | // return "[\"560,828,278\",\"560,785,278\",\"699,791,340\",\"430,994,340\",\"443,525,341\",\"403,913,342\",\"417,978,342\",\"359,916,343\",\"233,992,344\",\"341,540,344\"]"; 317 | } 318 | 319 | async function getPayload2( 320 | app: PxApp, 321 | payload1: PxPayload[], 322 | challengeSigned: string, 323 | challengeResult: string, 324 | challengeTs: number, 325 | ) { 326 | const p1 = payload1[0].d; 327 | 328 | const data: PxData = { 329 | PX349: randomInt(150, 200), // collector_request_rtt x 330 | PX320: p1.PX320, // device_model 331 | PX259: challengeTs, // challenge_ts x 332 | PX256: challengeSigned, // challenge_signed x 333 | PX257: challengeResult, // challenge_result x 334 | PX339: p1.PX339, // device_type 335 | PX322: p1.PX322, // device_os_name 336 | PX340: p1.PX340, // sdk_version 337 | PX341: p1.PX341, // app_name 338 | PX342: p1.PX342, // app_version 339 | PX348: p1.PX348, // app_identifier 340 | PX343: p1.PX343, // network 341 | PX344: p1.PX344, // carrier 342 | PX347: p1.PX347, // device_supported_languages 343 | PX413: p1.PX413, // battery_health 344 | PX414: p1.PX414, // battery_status 345 | PX415: p1.PX415, // battery_level 346 | PX416: p1.PX416, // battery_plugged 347 | PX419: p1.PX419, // battery_technology 348 | PX418: p1.PX418, // battery_temperature 349 | PX420: p1.PX420, // battery_voltage 350 | }; 351 | 352 | if (app.sdkNumber.compare(SDK_VERSIONS["1.15.2"]) >= 0) { 353 | data.PX1159 = false; // is_instant_app 354 | } 355 | 356 | if (app.sdkNumber.compare(SDK_VERSIONS["2.2.0"]) >= 0) { 357 | data.PX91 = p1.PX91; 358 | data.PX92 = p1.PX92; 359 | data.PX316 = p1.PX316; 360 | data.PX317 = p1.PX317; 361 | data.PX318 = p1.PX318; 362 | data.PX319 = p1.PX319; 363 | data.PX321 = p1.PX321; 364 | data.PX323 = p1.PX323; 365 | data.PX326 = p1.PX326; 366 | data.PX327 = p1.PX327; 367 | data.PX328 = p1.PX328; 368 | data.PX330 = p1.PX330; 369 | data.PX331 = p1.PX331; 370 | data.PX332 = p1.PX332; 371 | data.PX333 = p1.PX333; 372 | data.PX334 = p1.PX334; 373 | data.PX335 = p1.PX335; 374 | data.PX336 = p1.PX336; 375 | data.PX337 = p1.PX337; 376 | data.PX345 = p1.PX345; 377 | data.PX351 = p1.PX351; 378 | data.PX421 = p1.PX421; 379 | data.PX442 = p1.PX442; 380 | data.PX1208 = p1.PX1208; 381 | data.PX1214 = p1.PX1214; 382 | delete data.PX349; 383 | } 384 | 385 | if (app.sdkNumber.compare(SDK_VERSIONS["3.0.0"]) >= 0) { 386 | data.PX21215 = p1.PX21215; 387 | data.PX21217 = p1.PX21217; 388 | data.PX21218 = p1.PX21218; 389 | data.PX21219 = p1.PX21219; 390 | data.PX21221 = p1.PX21221; 391 | } 392 | 393 | return [ 394 | { 395 | t: "PX329", 396 | d: data, 397 | }, 398 | ] as PxPayload[]; 399 | } 400 | 401 | function addExtraData(body: { [key: string]: string }, app: PxApp) { 402 | if (!app.extraData) { 403 | return body; 404 | } 405 | 406 | for (const [index, value] of app.extraData.entries()) { 407 | // Warning: This mutates the original object 408 | body[`p${index + 1}`] = value; 409 | } 410 | 411 | // Alt method 412 | // const customParams: { [key: string]: string }[] = app.extraData.map((value, index) => ({[`p${index + 1}`]: value})); 413 | // return Object.assign(body, ...customParams); 414 | 415 | return body; 416 | } 417 | 418 | function encodePayload(payload: PxPayload[]) { 419 | return Buffer.from(JSON.stringify(payload)).toString("base64"); 420 | } 421 | 422 | function encodePxHello(uuid: string, version: "2" | "3") { 423 | const a = new TextEncoder().encode(uuid); 424 | const b = new TextEncoder().encode(version); 425 | 426 | const result = new Uint8Array(a.length); 427 | for (let i = 0; i < a.length; i++) { 428 | result[i] = a[i] ^ b[i % b.length]; 429 | } 430 | 431 | return Buffer.from(result).toString("base64"); 432 | } 433 | 434 | function createAndroidId() { 435 | return randomBytes(8).toString("hex"); 436 | } 437 | 438 | async function createFingerprints(device: DeviceFingerprint, newVersion: boolean) { 439 | let fingerprint1: string; 440 | let fingerprint2: string; 441 | let fingerprint3: string; 442 | 443 | if (newVersion) { 444 | // long j = System.currentTimeMillis(); 445 | // if (currentTimeMillis == C0147f.f431a) { 446 | // j++; 447 | // } 448 | // String uuid = new UUID(j, 1L).toString(); 449 | 450 | fingerprint1 = generateNewPXUUID(); 451 | fingerprint2 = fingerprint1.split("-")[0].toUpperCase(); 452 | fingerprint3 = await sha1(device.productModel + fingerprint1 + fingerprint2); 453 | } else { 454 | // PX326: "fd1bf990-f347-11ed-9675-00001d291f30" 455 | fingerprint1 = generatePXUUID(); 456 | 457 | // PX327: "FD1BF991" 458 | fingerprint2 = generatePXUUID().split("-")[0].toUpperCase(); 459 | 460 | // PX328: "09D6DC091783DED4B218A01012C029BDD29E055D" 461 | fingerprint3 = await sha1(device.productModel + fingerprint1 + fingerprint2); 462 | } 463 | 464 | return { 465 | fingerprint1, 466 | fingerprint2, 467 | fingerprint3, 468 | }; 469 | } 470 | 471 | interface PxPayload { 472 | t: string; 473 | d: PxData; 474 | } 475 | 476 | interface PxData { 477 | // [key: string]: any; 478 | PX1?: string; 479 | PX2?: string; 480 | PX3?: string; 481 | PX4?: string; 482 | PX6?: string; 483 | PX7?: string; 484 | PX8?: string; 485 | PX9?: string; 486 | PX10?: string; 487 | PX11?: string; 488 | PX12?: string; 489 | PX13?: string; 490 | PX14?: string; 491 | PX15?: string; 492 | PX16?: string; 493 | PX17?: string; 494 | PX18?: string; 495 | PX19?: string; 496 | PX20?: string; 497 | PX21?: string; 498 | PX22?: string; 499 | PX23?: string; 500 | PX24?: string; 501 | PX25?: string; 502 | PX26?: string; 503 | PX27?: string; 504 | PX28?: string; 505 | PX29?: string; 506 | PX30?: string; 507 | PX31?: string; 508 | PX32?: string; 509 | PX33?: string; 510 | PX34?: string; 511 | PX35?: string; 512 | PX36?: string; 513 | PX38?: string; 514 | PX39?: string; 515 | PX40?: string; 516 | PX41?: string; 517 | PX42?: string; 518 | PX43?: string; 519 | PX44?: string; 520 | PX45?: string; 521 | PX46?: string; 522 | PX47?: string; 523 | PX48?: string; 524 | PX49?: string; 525 | PX50?: string; 526 | PX51?: string; 527 | PX52?: string; 528 | PX53?: string; 529 | PX54?: string; 530 | PX55?: string; 531 | PX56?: string; 532 | PX57?: string; 533 | PX58?: string; 534 | PX59?: string; 535 | PX60?: string; 536 | PX61?: string; 537 | PX62?: string; 538 | PX63?: string; 539 | PX64?: string; 540 | PX65?: string; 541 | PX66?: string; 542 | PX67?: string; 543 | PX68?: string; 544 | PX69?: string; 545 | PX70?: string; 546 | PX71?: string; 547 | PX72?: string; 548 | PX73?: string; 549 | PX74?: string; 550 | PX75?: string; 551 | PX76?: string; 552 | PX77?: string; 553 | PX78?: string; 554 | PX79?: string; 555 | PX80?: string; 556 | PX81?: string; 557 | PX82?: string; 558 | PX83?: string; 559 | PX84?: string; 560 | PX85?: string; 561 | PX86?: string; 562 | PX87?: string; 563 | PX88?: string; 564 | PX89?: string; 565 | PX90?: string; 566 | PX91?: number; 567 | PX92?: number; 568 | PX93?: string; 569 | PX94?: string; 570 | PX95?: string; 571 | PX96?: string; 572 | PX97?: string; 573 | PX98?: string; 574 | PX99?: string; 575 | PX100?: string; 576 | PX101?: string; 577 | PX102?: string; 578 | PX103?: string; 579 | PX104?: string; 580 | PX105?: string; 581 | PX106?: string; 582 | PX107?: string; 583 | PX108?: string; 584 | PX109?: string; 585 | PX110?: string; 586 | PX111?: string; 587 | PX112?: string; 588 | PX113?: string; 589 | PX114?: string; 590 | PX115?: string; 591 | PX116?: string; 592 | PX117?: string; 593 | PX118?: string; 594 | PX119?: string; 595 | PX120?: string; 596 | PX121?: string; 597 | PX122?: string; 598 | PX123?: string; 599 | PX124?: string; 600 | PX125?: string; 601 | PX126?: string; 602 | PX127?: string; 603 | PX128?: string; 604 | PX129?: string; 605 | PX130?: string; 606 | PX131?: string; 607 | PX133?: string; 608 | PX134?: string; 609 | PX135?: string; 610 | PX136?: string; 611 | PX137?: string; 612 | PX138?: string; 613 | PX139?: string; 614 | PX140?: string; 615 | PX141?: string; 616 | PX142?: string; 617 | PX143?: string; 618 | PX144?: string; 619 | PX145?: string; 620 | PX146?: string; 621 | PX147?: string; 622 | PX148?: string; 623 | PX149?: string; 624 | PX150?: string; 625 | PX151?: string; 626 | PX152?: string; 627 | PX153?: string; 628 | PX154?: string; 629 | PX155?: string; 630 | PX156?: string; 631 | PX157?: string; 632 | PX158?: string; 633 | PX159?: string; 634 | PX160?: string; 635 | PX161?: string; 636 | PX162?: string; 637 | PX163?: string; 638 | PX164?: string; 639 | PX165?: string; 640 | PX166?: string; 641 | PX167?: string; 642 | PX168?: string; 643 | PX169?: string; 644 | PX170?: string; 645 | PX171?: string; 646 | PX172?: string; 647 | PX173?: string; 648 | PX174?: string; 649 | PX175?: string; 650 | PX176?: string; 651 | PX177?: string; 652 | PX178?: string; 653 | PX179?: string; 654 | PX180?: string; 655 | PX181?: string; 656 | PX182?: string; 657 | PX183?: string; 658 | PX184?: string; 659 | PX185?: string; 660 | PX186?: string; 661 | PX187?: string; 662 | PX188?: string; 663 | PX189?: string; 664 | PX190?: string; 665 | PX191?: string; 666 | PX192?: string; 667 | PX193?: string; 668 | PX194?: string; 669 | PX195?: string; 670 | PX196?: string; 671 | PX197?: string; 672 | PX198?: string; 673 | PX199?: string; 674 | PX200?: string; 675 | PX201?: string; 676 | PX202?: string; 677 | PX203?: string; 678 | PX204?: string; 679 | PX205?: string; 680 | PX206?: string; 681 | PX207?: string; 682 | PX208?: string; 683 | PX209?: string; 684 | PX210?: string; 685 | PX211?: string; 686 | PX212?: string; 687 | PX213?: string; 688 | PX214?: string; 689 | PX215?: string; 690 | PX216?: string; 691 | PX217?: string; 692 | PX218?: string; 693 | PX219?: string; 694 | PX220?: string; 695 | PX221?: string; 696 | PX222?: string; 697 | PX223?: string; 698 | PX224?: string; 699 | PX225?: string; 700 | PX226?: string; 701 | PX227?: string; 702 | PX228?: string; 703 | PX229?: string; 704 | PX230?: string; 705 | PX231?: string; 706 | PX232?: string; 707 | PX233?: string; 708 | PX234?: string; 709 | PX235?: string; 710 | PX236?: string; 711 | PX237?: string; 712 | PX238?: string; 713 | PX239?: string; 714 | PX240?: string; 715 | PX241?: string; 716 | PX242?: string; 717 | PX243?: string; 718 | PX244?: string; 719 | PX245?: string; 720 | PX246?: string; 721 | PX247?: string; 722 | PX248?: string; 723 | PX249?: string; 724 | PX250?: string; 725 | PX251?: string; 726 | PX252?: string; 727 | PX253?: string; 728 | PX254?: string; 729 | PX255?: string; 730 | PX256?: string; 731 | PX257?: string; 732 | PX258?: string; 733 | PX259?: number; 734 | PX260?: string; 735 | PX261?: string; 736 | PX262?: string; 737 | PX263?: string; 738 | PX264?: string; 739 | PX265?: string; 740 | PX266?: string; 741 | PX267?: string; 742 | PX268?: string; 743 | PX269?: string; 744 | PX270?: string; 745 | PX271?: string; 746 | PX272?: string; 747 | PX273?: string; 748 | PX274?: string; 749 | PX275?: string; 750 | PX276?: string; 751 | PX277?: string; 752 | PX278?: string; 753 | PX279?: string; 754 | PX280?: string; 755 | PX281?: string; 756 | PX282?: string; 757 | PX283?: string; 758 | PX284?: string; 759 | PX285?: string; 760 | PX286?: string; 761 | PX287?: string; 762 | PX288?: string; 763 | PX289?: string; 764 | PX290?: string; 765 | PX291?: string; 766 | PX292?: string; 767 | PX293?: string; 768 | PX294?: string; 769 | PX295?: string; 770 | PX296?: string; 771 | PX297?: string; 772 | PX298?: string; 773 | PX299?: string; 774 | PX300?: string; 775 | PX301?: string; 776 | PX302?: string; 777 | PX303?: string; 778 | PX304?: string; 779 | PX305?: string; 780 | PX306?: string; 781 | PX307?: string; 782 | PX308?: string; 783 | PX309?: string; 784 | PX310?: string; 785 | PX311?: string; 786 | PX312?: string; 787 | PX313?: string; 788 | PX314?: string; 789 | PX315?: string; 790 | PX316?: boolean; 791 | PX317?: string; 792 | PX318?: string; 793 | PX319?: string; 794 | PX320?: string; 795 | PX321?: string; 796 | PX322?: string; 797 | PX323?: number; 798 | PX324?: string; 799 | PX325?: string; 800 | PX326?: string; 801 | PX327?: string; 802 | PX328?: string; 803 | PX329?: string; 804 | PX330?: string; 805 | PX331?: boolean; 806 | PX332?: boolean; 807 | PX333?: boolean; 808 | PX334?: boolean; 809 | PX335?: boolean; 810 | PX336?: boolean; 811 | PX337?: boolean; 812 | PX338?: string; 813 | PX339?: string; 814 | PX340?: string; 815 | PX341?: string; 816 | PX342?: string; 817 | PX343?: string; 818 | PX344?: string; 819 | PX345?: number; 820 | PX346?: string; 821 | PX347?: string[] | string; 822 | PX348?: string; 823 | PX349?: number; 824 | PX350?: string; 825 | PX351?: number; 826 | PX352?: string; 827 | PX353?: string; 828 | PX354?: string; 829 | PX355?: string; 830 | PX356?: string; 831 | PX357?: string; 832 | PX358?: string; 833 | PX359?: string; 834 | PX360?: string; 835 | PX361?: string; 836 | PX362?: string; 837 | PX363?: string; 838 | PX364?: string; 839 | PX365?: string; 840 | PX366?: string; 841 | PX367?: string; 842 | PX368?: string; 843 | PX369?: string; 844 | PX370?: string; 845 | PX371?: string; 846 | PX372?: string; 847 | PX373?: string; 848 | PX374?: string; 849 | PX375?: string; 850 | PX376?: string; 851 | PX377?: string; 852 | PX378?: string; 853 | PX379?: string; 854 | PX380?: string; 855 | PX381?: string; 856 | PX382?: string; 857 | PX383?: string; 858 | PX384?: string; 859 | PX385?: string; 860 | PX386?: string; 861 | PX387?: string; 862 | PX388?: string; 863 | PX389?: string; 864 | PX390?: string; 865 | PX391?: string; 866 | PX392?: string; 867 | PX393?: string; 868 | PX394?: string; 869 | PX395?: string; 870 | PX396?: string; 871 | PX397?: string; 872 | PX398?: string; 873 | PX399?: string; 874 | PX400?: string; 875 | PX401?: string; 876 | PX402?: string; 877 | PX403?: string; 878 | PX404?: string; 879 | PX405?: string; 880 | PX406?: string; 881 | PX407?: string; 882 | PX408?: string; 883 | PX409?: string; 884 | PX410?: string; 885 | PX411?: string; 886 | PX412?: string; 887 | PX413?: string; 888 | PX414?: string; 889 | PX415?: number; 890 | PX416?: string; 891 | PX417?: string; 892 | PX418?: number; 893 | PX419?: string; 894 | PX420?: number; 895 | PX421?: string; 896 | PX422?: string; 897 | PX423?: string; 898 | PX424?: string; 899 | PX425?: string; 900 | PX426?: string; 901 | PX427?: string; 902 | PX428?: string; 903 | PX429?: string; 904 | PX430?: string; 905 | PX431?: string; 906 | PX432?: string; 907 | PX433?: string; 908 | PX434?: string; 909 | PX435?: string; 910 | PX436?: string; 911 | PX437?: string; 912 | PX438?: string; 913 | PX439?: string; 914 | PX440?: string; 915 | PX441?: string; 916 | PX442?: string; 917 | PX443?: string; 918 | PX444?: string; 919 | PX445?: string; 920 | PX446?: string; 921 | PX447?: string; 922 | PX448?: string; 923 | PX449?: string; 924 | PX450?: string; 925 | PX451?: string; 926 | PX452?: string; 927 | PX453?: string; 928 | PX454?: string; 929 | PX455?: string; 930 | PX456?: string; 931 | PX457?: string; 932 | PX458?: string; 933 | PX459?: string; 934 | PX460?: string; 935 | PX461?: string; 936 | PX462?: string; 937 | PX463?: string; 938 | PX464?: string; 939 | PX465?: string; 940 | PX466?: string; 941 | PX467?: string; 942 | PX468?: string; 943 | PX469?: string; 944 | PX470?: string; 945 | PX471?: string; 946 | PX472?: string; 947 | PX473?: string; 948 | PX474?: string; 949 | PX475?: string; 950 | PX476?: string; 951 | PX477?: string; 952 | PX478?: string; 953 | PX479?: string; 954 | PX480?: string; 955 | PX481?: string; 956 | PX482?: string; 957 | PX483?: string; 958 | PX484?: string; 959 | PX485?: string; 960 | PX486?: string; 961 | PX487?: string; 962 | PX489?: string; 963 | PX490?: string; 964 | PX491?: string; 965 | PX492?: string; 966 | PX493?: string; 967 | PX494?: string; 968 | PX495?: string; 969 | PX496?: string; 970 | PX497?: string; 971 | PX498?: string; 972 | PX499?: string; 973 | PX500?: string; 974 | PX501?: string; 975 | PX502?: string; 976 | PX503?: string; 977 | PX504?: string; 978 | PX505?: string; 979 | PX506?: string; 980 | PX507?: string; 981 | PX508?: string; 982 | PX509?: string; 983 | PX510?: string; 984 | PX511?: string; 985 | PX512?: string; 986 | PX513?: string; 987 | PX514?: string; 988 | PX515?: string; 989 | PX516?: string; 990 | PX517?: string; 991 | PX518?: string; 992 | PX519?: string; 993 | PX520?: string; 994 | PX521?: string; 995 | PX522?: string; 996 | PX523?: string; 997 | PX524?: string; 998 | PX525?: string; 999 | PX526?: string; 1000 | PX527?: string; 1001 | PX528?: string; 1002 | PX529?: string; 1003 | PX530?: string; 1004 | PX531?: string; 1005 | PX532?: string; 1006 | PX533?: string; 1007 | PX534?: string; 1008 | PX535?: string; 1009 | PX536?: string; 1010 | PX537?: string; 1011 | PX538?: string; 1012 | PX539?: string; 1013 | PX540?: string; 1014 | PX541?: string; 1015 | PX542?: string; 1016 | PX543?: string; 1017 | PX544?: string; 1018 | PX545?: string; 1019 | PX546?: string; 1020 | PX547?: string; 1021 | PX548?: string; 1022 | PX549?: string; 1023 | PX550?: string; 1024 | PX551?: string; 1025 | PX552?: string; 1026 | PX553?: string; 1027 | PX554?: string; 1028 | PX555?: string; 1029 | PX556?: string; 1030 | PX557?: string; 1031 | PX558?: string; 1032 | PX559?: string; 1033 | PX560?: string; 1034 | PX561?: string; 1035 | PX562?: string; 1036 | PX563?: string; 1037 | PX564?: string; 1038 | PX565?: string; 1039 | PX566?: string; 1040 | PX567?: string; 1041 | PX568?: string; 1042 | PX569?: string; 1043 | PX570?: string; 1044 | PX571?: string; 1045 | PX572?: string; 1046 | PX573?: string; 1047 | PX574?: string; 1048 | PX575?: string; 1049 | PX576?: string; 1050 | PX577?: string; 1051 | PX578?: string; 1052 | PX579?: string; 1053 | PX580?: string; 1054 | PX581?: string; 1055 | PX582?: string; 1056 | PX583?: string; 1057 | PX584?: string; 1058 | PX585?: string; 1059 | PX586?: string; 1060 | PX587?: string; 1061 | PX588?: string; 1062 | PX589?: string; 1063 | PX590?: string; 1064 | PX591?: string; 1065 | PX592?: string; 1066 | PX593?: string; 1067 | PX594?: string; 1068 | PX595?: string; 1069 | PX596?: string; 1070 | PX597?: string; 1071 | PX598?: string; 1072 | PX599?: string; 1073 | PX600?: string; 1074 | PX601?: string; 1075 | PX602?: string; 1076 | PX603?: string; 1077 | PX604?: string; 1078 | PX605?: string; 1079 | PX606?: string; 1080 | PX607?: string; 1081 | PX608?: string; 1082 | PX609?: string; 1083 | PX610?: string; 1084 | PX611?: string; 1085 | PX612?: string; 1086 | PX613?: string; 1087 | PX614?: string; 1088 | PX615?: string; 1089 | PX616?: string; 1090 | PX617?: string; 1091 | PX618?: string; 1092 | PX619?: string; 1093 | PX620?: string; 1094 | PX621?: string; 1095 | PX622?: string; 1096 | PX623?: string; 1097 | PX624?: string; 1098 | PX625?: string; 1099 | PX626?: string; 1100 | PX627?: string; 1101 | PX628?: string; 1102 | PX629?: string; 1103 | PX630?: string; 1104 | PX631?: string; 1105 | PX632?: string; 1106 | PX633?: string; 1107 | PX635?: string; 1108 | PX636?: string; 1109 | PX637?: string; 1110 | PX638?: string; 1111 | PX639?: string; 1112 | PX640?: string; 1113 | PX641?: string; 1114 | PX642?: string; 1115 | PX643?: string; 1116 | PX644?: string; 1117 | PX645?: string; 1118 | PX646?: string; 1119 | PX647?: string; 1120 | PX648?: string; 1121 | PX649?: string; 1122 | PX650?: string; 1123 | PX651?: string; 1124 | PX652?: string; 1125 | PX653?: string; 1126 | PX654?: string; 1127 | PX655?: string; 1128 | PX656?: string; 1129 | PX657?: string; 1130 | PX658?: string; 1131 | PX659?: string; 1132 | PX660?: string; 1133 | PX661?: string; 1134 | PX662?: string; 1135 | PX663?: string; 1136 | PX664?: string; 1137 | PX665?: string; 1138 | PX666?: string; 1139 | PX667?: string; 1140 | PX668?: string; 1141 | PX669?: string; 1142 | PX670?: string; 1143 | PX671?: string; 1144 | PX672?: string; 1145 | PX673?: string; 1146 | PX674?: string; 1147 | PX675?: string; 1148 | PX676?: string; 1149 | PX677?: string; 1150 | PX678?: string; 1151 | PX679?: string; 1152 | PX680?: string; 1153 | PX681?: string; 1154 | PX682?: string; 1155 | PX683?: string; 1156 | PX684?: string; 1157 | PX685?: string; 1158 | PX686?: string; 1159 | PX687?: string; 1160 | PX688?: string; 1161 | PX689?: string; 1162 | PX690?: string; 1163 | PX691?: string; 1164 | PX692?: string; 1165 | PX693?: string; 1166 | PX694?: string; 1167 | PX695?: string; 1168 | PX696?: string; 1169 | PX697?: string; 1170 | PX698?: string; 1171 | PX699?: string; 1172 | PX700?: string; 1173 | PX701?: string; 1174 | PX702?: string; 1175 | PX703?: string; 1176 | PX704?: string; 1177 | PX705?: string; 1178 | PX706?: string; 1179 | PX707?: string; 1180 | PX708?: string; 1181 | PX709?: string; 1182 | PX710?: string; 1183 | PX711?: string; 1184 | PX712?: string; 1185 | PX713?: string; 1186 | PX714?: string; 1187 | PX715?: string; 1188 | PX716?: string; 1189 | PX717?: string; 1190 | PX718?: string; 1191 | PX719?: string; 1192 | PX720?: string; 1193 | PX721?: string; 1194 | PX722?: string; 1195 | PX723?: string; 1196 | PX724?: string; 1197 | PX725?: string; 1198 | PX726?: string; 1199 | PX727?: string; 1200 | PX728?: string; 1201 | PX729?: string; 1202 | PX730?: string; 1203 | PX731?: string; 1204 | PX732?: string; 1205 | PX733?: string; 1206 | PX734?: string; 1207 | PX735?: string; 1208 | PX736?: string; 1209 | PX737?: string; 1210 | PX738?: string; 1211 | PX739?: string; 1212 | PX740?: string; 1213 | PX741?: string; 1214 | PX742?: string; 1215 | PX743?: string; 1216 | PX744?: string; 1217 | PX745?: string; 1218 | PX746?: string; 1219 | PX747?: string; 1220 | PX748?: string; 1221 | PX749?: string; 1222 | PX750?: string; 1223 | PX751?: string; 1224 | PX752?: string; 1225 | PX753?: string; 1226 | PX754?: string; 1227 | PX755?: string; 1228 | PX756?: string; 1229 | PX757?: string; 1230 | PX758?: string; 1231 | PX759?: string; 1232 | PX760?: string; 1233 | PX761?: string; 1234 | PX762?: string; 1235 | PX763?: string; 1236 | PX764?: string; 1237 | PX765?: string; 1238 | PX766?: string; 1239 | PX767?: string; 1240 | PX768?: string; 1241 | PX769?: string; 1242 | PX770?: string; 1243 | PX771?: string; 1244 | PX772?: string; 1245 | PX773?: string; 1246 | PX774?: string; 1247 | PX775?: string; 1248 | PX776?: string; 1249 | PX777?: string; 1250 | PX778?: string; 1251 | PX779?: string; 1252 | PX780?: string; 1253 | PX781?: string; 1254 | PX782?: string; 1255 | PX783?: string; 1256 | PX784?: string; 1257 | PX785?: string; 1258 | PX786?: string; 1259 | PX787?: string; 1260 | PX788?: string; 1261 | PX789?: string; 1262 | PX790?: string; 1263 | PX791?: string; 1264 | PX792?: string; 1265 | PX793?: string; 1266 | PX794?: string; 1267 | PX795?: string; 1268 | PX796?: string; 1269 | PX797?: string; 1270 | PX798?: string; 1271 | PX799?: string; 1272 | PX800?: string; 1273 | PX801?: string; 1274 | PX802?: string; 1275 | PX803?: string; 1276 | PX804?: string; 1277 | PX805?: string; 1278 | PX806?: string; 1279 | PX807?: string; 1280 | PX808?: string; 1281 | PX809?: string; 1282 | PX810?: string; 1283 | PX811?: string; 1284 | PX812?: string; 1285 | PX813?: string; 1286 | PX814?: string; 1287 | PX815?: string; 1288 | PX816?: string; 1289 | PX817?: string; 1290 | PX818?: string; 1291 | PX819?: string; 1292 | PX820?: string; 1293 | PX821?: string; 1294 | PX822?: string; 1295 | PX823?: string; 1296 | PX824?: string; 1297 | PX825?: string; 1298 | PX826?: string; 1299 | PX827?: string; 1300 | PX828?: string; 1301 | PX829?: string; 1302 | PX830?: string; 1303 | PX831?: string; 1304 | PX832?: string; 1305 | PX833?: string; 1306 | PX834?: string; 1307 | PX835?: string; 1308 | PX836?: string; 1309 | PX837?: string; 1310 | PX838?: string; 1311 | PX839?: string; 1312 | PX840?: string; 1313 | PX841?: string; 1314 | PX842?: string; 1315 | PX843?: string; 1316 | PX844?: string; 1317 | PX845?: string; 1318 | PX846?: string; 1319 | PX847?: string; 1320 | PX848?: string; 1321 | PX849?: string; 1322 | PX850?: string; 1323 | PX851?: string; 1324 | PX852?: string; 1325 | PX853?: string; 1326 | PX854?: string; 1327 | PX855?: string; 1328 | PX856?: string; 1329 | PX857?: string; 1330 | PX858?: string; 1331 | PX859?: string; 1332 | PX860?: string; 1333 | PX861?: string; 1334 | PX862?: string; 1335 | PX863?: string; 1336 | PX864?: string; 1337 | PX865?: string; 1338 | PX866?: string; 1339 | PX867?: string; 1340 | PX868?: string; 1341 | PX869?: string; 1342 | PX870?: string; 1343 | PX871?: string; 1344 | PX872?: string; 1345 | PX873?: string; 1346 | PX874?: string; 1347 | PX875?: string; 1348 | PX876?: string; 1349 | PX877?: string; 1350 | PX878?: string; 1351 | PX879?: string; 1352 | PX880?: string; 1353 | PX881?: string; 1354 | PX882?: string; 1355 | PX883?: string; 1356 | PX884?: string; 1357 | PX885?: string; 1358 | PX886?: string; 1359 | PX887?: string; 1360 | PX888?: string; 1361 | PX889?: string; 1362 | PX890?: string; 1363 | PX891?: string; 1364 | PX892?: string; 1365 | PX893?: string; 1366 | PX894?: string; 1367 | PX895?: string; 1368 | PX896?: string; 1369 | PX897?: string; 1370 | PX898?: string; 1371 | PX899?: string; 1372 | PX900?: string; 1373 | PX901?: string; 1374 | PX902?: string; 1375 | PX903?: string; 1376 | PX904?: string; 1377 | PX905?: string; 1378 | PX906?: string; 1379 | PX907?: string; 1380 | PX908?: string; 1381 | PX909?: string; 1382 | PX910?: string; 1383 | PX911?: string; 1384 | PX912?: string; 1385 | PX913?: string; 1386 | PX914?: string; 1387 | PX915?: string; 1388 | PX916?: string; 1389 | PX917?: string; 1390 | PX918?: string; 1391 | PX919?: string; 1392 | PX920?: string; 1393 | PX921?: string; 1394 | PX922?: string; 1395 | PX923?: string; 1396 | PX924?: string; 1397 | PX925?: string; 1398 | PX926?: string; 1399 | PX927?: string; 1400 | PX928?: string; 1401 | PX929?: string; 1402 | PX930?: string; 1403 | PX931?: string; 1404 | PX932?: string; 1405 | PX933?: string; 1406 | PX934?: string; 1407 | PX935?: string; 1408 | PX936?: string; 1409 | PX937?: string; 1410 | PX938?: string; 1411 | PX939?: string; 1412 | PX940?: string; 1413 | PX941?: string; 1414 | PX942?: string; 1415 | PX943?: string; 1416 | PX944?: string; 1417 | PX945?: string; 1418 | PX946?: string; 1419 | PX947?: string; 1420 | PX948?: string; 1421 | PX949?: string; 1422 | PX950?: string; 1423 | PX951?: string; 1424 | PX952?: string; 1425 | PX953?: string; 1426 | PX954?: string; 1427 | PX955?: string; 1428 | PX956?: string; 1429 | PX957?: string; 1430 | PX958?: string; 1431 | PX959?: string; 1432 | PX960?: string; 1433 | PX961?: string; 1434 | PX962?: string; 1435 | PX963?: string; 1436 | PX964?: string; 1437 | PX965?: string; 1438 | PX966?: string; 1439 | PX967?: string; 1440 | PX968?: string; 1441 | PX969?: string; 1442 | PX970?: string; 1443 | PX971?: string; 1444 | PX972?: string; 1445 | PX973?: string; 1446 | PX974?: string; 1447 | PX975?: string; 1448 | PX976?: string; 1449 | PX977?: string; 1450 | PX978?: string; 1451 | PX979?: string; 1452 | PX980?: string; 1453 | PX981?: string; 1454 | PX982?: string; 1455 | PX983?: string; 1456 | PX984?: string; 1457 | PX985?: string; 1458 | PX986?: string; 1459 | PX987?: string; 1460 | PX988?: string; 1461 | PX989?: string; 1462 | PX990?: string; 1463 | PX991?: string; 1464 | PX992?: string; 1465 | PX993?: string; 1466 | PX994?: string; 1467 | PX995?: string; 1468 | PX996?: string; 1469 | PX997?: string; 1470 | PX998?: string; 1471 | PX999?: string; 1472 | PX1000?: string; 1473 | PX1001?: string; 1474 | PX1002?: string; 1475 | PX1003?: string; 1476 | PX1004?: string; 1477 | PX1005?: string; 1478 | PX1006?: string; 1479 | PX1007?: string; 1480 | PX1008?: string; 1481 | PX1009?: string; 1482 | PX1010?: string; 1483 | PX1011?: string; 1484 | PX1012?: string; 1485 | PX1013?: string; 1486 | PX1014?: string; 1487 | PX1015?: string; 1488 | PX1016?: string; 1489 | PX1017?: string; 1490 | PX1018?: string; 1491 | PX1019?: string; 1492 | PX1020?: string; 1493 | PX1021?: string; 1494 | PX1022?: string; 1495 | PX1023?: string; 1496 | PX1024?: string; 1497 | PX1025?: string; 1498 | PX1026?: string; 1499 | PX1027?: string; 1500 | PX1028?: string; 1501 | PX1029?: string; 1502 | PX1030?: string; 1503 | PX1031?: string; 1504 | PX1032?: string; 1505 | PX1033?: string; 1506 | PX1034?: string; 1507 | PX1035?: string; 1508 | PX1036?: string; 1509 | PX1037?: string; 1510 | PX1038?: string; 1511 | PX1039?: string; 1512 | PX1040?: string; 1513 | PX1041?: string; 1514 | PX1042?: string; 1515 | PX1043?: string; 1516 | PX1044?: string; 1517 | PX1045?: string; 1518 | PX1046?: string; 1519 | PX1047?: string; 1520 | PX1048?: string; 1521 | PX1049?: string; 1522 | PX1050?: string; 1523 | PX1051?: string; 1524 | PX1052?: string; 1525 | PX1053?: string; 1526 | PX1054?: string; 1527 | PX1055?: string; 1528 | PX1056?: string; 1529 | PX1057?: string; 1530 | PX1058?: string; 1531 | PX1059?: string; 1532 | PX1060?: string; 1533 | PX1061?: string; 1534 | PX1062?: string; 1535 | PX1063?: string; 1536 | PX1064?: string; 1537 | PX1065?: string; 1538 | PX1066?: string; 1539 | PX1067?: string; 1540 | PX1068?: string; 1541 | PX1069?: string; 1542 | PX1070?: string; 1543 | PX1071?: string; 1544 | PX1072?: string; 1545 | PX1073?: string; 1546 | PX1074?: string; 1547 | PX1075?: string; 1548 | PX1076?: string; 1549 | PX1077?: string; 1550 | PX1078?: string; 1551 | PX1079?: string; 1552 | PX1080?: string; 1553 | PX1081?: string; 1554 | PX1082?: string; 1555 | PX1083?: string; 1556 | PX1084?: string; 1557 | PX1085?: string; 1558 | PX1086?: string; 1559 | PX1087?: string; 1560 | PX1088?: string; 1561 | PX1089?: string; 1562 | PX1090?: string; 1563 | PX1091?: string; 1564 | PX1092?: string; 1565 | PX1093?: string; 1566 | PX1094?: string; 1567 | PX1095?: string; 1568 | PX1096?: string; 1569 | PX1097?: string; 1570 | PX1098?: string; 1571 | PX1099?: string; 1572 | PX1100?: string; 1573 | PX1101?: string; 1574 | PX1102?: string; 1575 | PX1103?: string; 1576 | PX1104?: string; 1577 | PX1105?: string; 1578 | PX1106?: string; 1579 | PX1107?: string; 1580 | PX1108?: string; 1581 | PX1109?: string; 1582 | PX1110?: string; 1583 | PX1111?: string; 1584 | PX1112?: string; 1585 | PX1113?: string; 1586 | PX1114?: string; 1587 | PX1115?: string; 1588 | PX1116?: string; 1589 | PX1117?: string; 1590 | PX1118?: string; 1591 | PX1119?: string; 1592 | PX1120?: string; 1593 | PX1121?: string; 1594 | PX1122?: string; 1595 | PX1123?: string; 1596 | PX1124?: string; 1597 | PX1125?: string; 1598 | PX1126?: string; 1599 | PX1127?: string; 1600 | PX1128?: string; 1601 | PX1129?: string; 1602 | PX1130?: string; 1603 | PX1159?: boolean; 1604 | PX1208?: string; 1605 | PX1214?: string; 1606 | PX21215?: number; 1607 | PX21217?: string; 1608 | PX21218?: string; 1609 | PX21221?: string; 1610 | PX21219?: string; 1611 | } 1612 | 1613 | interface PxResponse { 1614 | do: string[]; 1615 | } 1616 | 1617 | export interface PxSolution { 1618 | solveTime: number; 1619 | site: string; 1620 | sid: string; 1621 | vid: string; 1622 | headers: PxSolutionHeaders; 1623 | } 1624 | 1625 | interface PxSolutionHeaders { 1626 | [key: string]: string | undefined; 1627 | 1628 | "user-agent": string; 1629 | "x-px-os-version"?: string; 1630 | "x-px-uuid"?: string; 1631 | "x-px-device-fp"?: string; 1632 | "x-px-device-model"?: string; 1633 | "x-px-os"?: string; 1634 | "x-px-hello"?: string; 1635 | "x-px-mobile-sdk-version"?: string; 1636 | "x-px-authorization": string; 1637 | } 1638 | -------------------------------------------------------------------------------- /src/px/device.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from "fs/promises"; 2 | import { randomItem } from "@src/utils.js"; 3 | 4 | let devices: DeviceFingerprint[] | null = null; 5 | 6 | export async function getRandomDevice() { 7 | if (devices === null) { 8 | const screenSizes = [ 9 | // [480, 800], 10 | // [640, 1136], 11 | // [720, 1280], 12 | // [750, 1334], 13 | // [1080, 1920], 14 | // [1440, 2560], 15 | 16 | [480, 800], 17 | [480, 854], 18 | [768, 1280], 19 | [800, 1280], 20 | [1032, 1920], 21 | [1080, 1920], 22 | [1200, 1920], 23 | [1440, 2560], 24 | [1600, 2560], 25 | ]; 26 | const lines = await readFile("./resources/devices.csv", "utf8"); 27 | 28 | devices = lines 29 | .split("\r\n") 30 | .slice(1) 31 | .map((line) => { 32 | const [ 33 | buildId, 34 | buildDisplayId, 35 | productName, 36 | productDevice, 37 | productBoard, 38 | productManufacturer, 39 | productBrand, 40 | productModel, 41 | bootloader, 42 | hardware, 43 | buildType, 44 | buildTags, 45 | buildFingerprint, 46 | buildUser, 47 | buildHost, 48 | buildVersionIncremental, 49 | buildVersionRelease, 50 | buildVersionSdk, 51 | buildVersionCodename, 52 | ] = line.split(","); 53 | 54 | const rect = randomItem(screenSizes); 55 | const device: DeviceFingerprint = { 56 | // TODO: Temporary solution 57 | // width: 1080, 58 | // height: 1920, 59 | // width: 720, 60 | // height: 1184, 61 | width: rect[0], 62 | height: rect[1], 63 | 64 | buildId, 65 | buildDisplayId, 66 | productName, 67 | productDevice, 68 | productBoard, 69 | productManufacturer, 70 | productBrand, 71 | productModel, 72 | bootloader, 73 | hardware, 74 | buildType, 75 | buildTags, 76 | buildFingerprint, 77 | buildUser, 78 | buildHost, 79 | buildVersionIncremental, 80 | buildVersionRelease, 81 | buildVersionSdk, 82 | buildVersionCode: "11", 83 | buildVersionCodename, 84 | }; 85 | 86 | return device; 87 | }); 88 | } 89 | 90 | return randomItem(devices); 91 | } 92 | 93 | export interface DeviceFingerprint { 94 | width: number; 95 | height: number; 96 | 97 | buildId: string; 98 | buildDisplayId: string; 99 | productName: string; 100 | productDevice: string; 101 | productBoard: string; 102 | productManufacturer: string; 103 | productBrand: string; 104 | productModel: string; 105 | bootloader: string; 106 | hardware: string; 107 | buildType: string; 108 | buildTags: string; 109 | buildFingerprint: string; 110 | buildUser: string; 111 | buildHost: string; 112 | buildVersionIncremental: string; 113 | buildVersionRelease: string; 114 | buildVersionSdk: string; 115 | buildVersionCode: string; 116 | buildVersionCodename: string; 117 | } 118 | -------------------------------------------------------------------------------- /src/px/errors.ts: -------------------------------------------------------------------------------- 1 | export class GenerationException extends Error { 2 | name = "GenerationException"; 3 | } 4 | -------------------------------------------------------------------------------- /src/px/uuid.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | 3 | import crypto from "crypto"; 4 | import Long from "long"; 5 | 6 | // TODO: Allow reset 7 | let f317a = Long.MIN_VALUE; 8 | 9 | export function generatePXUUID() { 10 | const ts = Date.now() + 1; 11 | return toUuidString(BigInt(generateMostSigBits(ts)), BigInt(generateLeastSigBits())); 12 | } 13 | 14 | export function generateNewPXUUID() { 15 | // TODO: Use Date.now() + (idsCreated++) 16 | const ts = Date.now(); 17 | return toUuidString(BigInt(ts), BigInt(1)); 18 | } 19 | 20 | function generateMostSigBits(ts: number) { 21 | const j = generate( 22 | Long.fromNumber(ts) 23 | .multiply(10000) 24 | // .add(Long.fromString("122192928000000000")); // TODO 25 | .add(Long.fromNumber(122192928000000000)), 26 | ); 27 | 28 | const a = j.and(Long.fromNumber(-281474976710656)).shiftRightUnsigned(48); 29 | const b = j.shiftLeft(32); 30 | const c = Long.fromNumber(281470681743360).and(j).shiftRightUnsigned(16); 31 | return a.or(b).or(4096).or(c).toString(); 32 | } 33 | 34 | function generate(mixedTs: Long) { 35 | if (mixedTs > f317a) { 36 | f317a = mixedTs; 37 | return mixedTs; 38 | } 39 | 40 | // const j2 = j + 1; 41 | // Add +1 for each uuid created this session 42 | f317a = f317a.add(1); 43 | return f317a; 44 | } 45 | 46 | function generateLeastSigBits() { 47 | const b = Array.from(crypto.randomBytes(4)); 48 | // const b = [-121, 42, 60, -115]; // TESTING 49 | 50 | const j2 = Long.MIN_VALUE.or(Long.fromNumber(b[0] << 24).and(Long.fromNumber(4278190080))); 51 | const j3 = j2.or((b[1] << 16) & 16711680); 52 | const j4 = j3.or((b[2] << 8) & 65280); 53 | // const j = j4.or(b[3] & -1); // newer 54 | const j = j4.or(b[3] & 255); 55 | 56 | return j.or(Long.fromNumber(Math.random() * 16383.0).shiftLeft(48)).toString(); 57 | // return j.or(Long.fromNumber(0.3815323891000282 * 16383.0).shiftLeft(48)).toString(); // TESTING 58 | } 59 | 60 | // TODO: Move to here 61 | export function toUuidString(msb: bigint, lsb: bigint) { 62 | // @ts-ignore 63 | return `${digits(msb >> 32n, 8n)}-${digits(msb >> 16n, 4n)}-${digits(msb, 4n)}-${digits(lsb >> 48n, 4n)}-${digits( 64 | lsb, 65 | 12n, 66 | )}`; 67 | } 68 | 69 | function digits(value: bigint, ds: bigint) { 70 | // @ts-ignore 71 | const hi = 1n << (ds * 4n); 72 | 73 | // @ts-ignore 74 | return (hi | (value & (hi - 1n))).toString(16).slice(1); 75 | } 76 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | 3 | export async function sha1(message: string) { 4 | return hash(message, "SHA-1"); 5 | } 6 | 7 | export async function sha256(message: string) { 8 | return hash(message, "SHA-256"); 9 | } 10 | 11 | export async function hash(message: string, algorithm: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") { 12 | const data = new TextEncoder().encode(message); 13 | const hash = await crypto.subtle.digest(algorithm, data); 14 | 15 | return toHex(new Uint8Array(hash)); 16 | } 17 | 18 | export function toHex(data: Uint8Array) { 19 | return Array.from(data) 20 | .map((b) => b.toString(16).padStart(2, "0")) 21 | .join("") 22 | .toUpperCase(); 23 | } 24 | 25 | export function randomString(length: number = 16) { 26 | const output = new Uint8Array(length / 2); 27 | crypto.getRandomValues(output); 28 | 29 | return toHex(output); 30 | } 31 | 32 | export function randomInt(min: number, max: number) { 33 | return Math.floor(Math.random() * (max - min + 1) + min); 34 | } 35 | 36 | export function randomFloat(min: number, max: number) { 37 | return Math.random() * (max - min + 1) + min; 38 | } 39 | 40 | export function randomItem(items: readonly T[]): T { 41 | return items[Math.floor(Math.random() * items.length)]; 42 | } 43 | 44 | export function sleep(seconds: number): Promise { 45 | return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | "incremental": true, 7 | /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 8 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 9 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 10 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 11 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 12 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 13 | 14 | /* Language and Environment */ 15 | "target": "ES2021", 16 | /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 17 | "lib": ["ES2021", "DOM"], 18 | /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 19 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 20 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 21 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 22 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 23 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 24 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 25 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 26 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 27 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 28 | //"moduleDetection": "force", /* Control what method is used to detect module-format JS files. */ 29 | 30 | /* Modules */ 31 | "module": "ES2022", 32 | /* Specify what module code is generated. */ 33 | // "rootDir": "./src", /* Specify the root folder within your source files. */ 34 | "moduleResolution": "NodeNext", 35 | /* Specify how TypeScript looks up a file from a given module specifier. */ 36 | "baseUrl": "./", 37 | /* Specify the base directory to resolve non-relative module names. */ 38 | "paths": { 39 | "@src/*": ["src/*"] 40 | }, 41 | /* Specify a set of entries that re-map imports to additional lookup locations. */ 42 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 43 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 44 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 45 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 46 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 47 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 48 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 49 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 50 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 51 | "resolveJsonModule": true, 52 | /* Enable importing .json files. */ 53 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 54 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 55 | 56 | /* JavaScript Support */ 57 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 58 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 59 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 60 | 61 | /* Emit */ 62 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 63 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 64 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 65 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 66 | "inlineSourceMap": true, 67 | /* Include sourcemap files inside the emitted JavaScript. */ 68 | // "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. */ 69 | "outDir": "./dist", 70 | /* Specify an output folder for all emitted files. */ 71 | "removeComments": true, 72 | /* Disable emitting comments. */ 73 | // "noEmit": true, /* Disable emitting files from a compilation. */ 74 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 75 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 76 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 77 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 78 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 79 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 80 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 81 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 82 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 83 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 84 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 85 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 86 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 87 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 88 | 89 | /* Interop Constraints */ 90 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 91 | // "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. */ 92 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 93 | "esModuleInterop": true, 94 | /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 95 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 96 | "forceConsistentCasingInFileNames": true, 97 | /* Ensure that casing is correct in imports. */ 98 | 99 | /* Type Checking */ 100 | "strict": true, 101 | /* Enable all strict type-checking options. */ 102 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 103 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 104 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 105 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 106 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 107 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 108 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 109 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 110 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 111 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 112 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 113 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 114 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 115 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 116 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 117 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 118 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 119 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 120 | 121 | /* Completeness */ 122 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 123 | "skipLibCheck": true 124 | /* Skip type checking all .d.ts files. */ 125 | }, 126 | "include": ["src/**/*"], 127 | "exclude": ["node_modules"], 128 | "ts-node": { 129 | "swc": true, 130 | "esm": true 131 | } 132 | } 133 | --------------------------------------------------------------------------------