├── .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 |
--------------------------------------------------------------------------------