@prosopo/provider-mock 2.8.190 → 2.8.192

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api.ts DELETED
@@ -1,208 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { IncomingHttpHeaders } from "node:http";
16
- import { ProsopoApiError } from "@prosopo/common";
17
- import { type Logger, getLogger } from "@prosopo/logger";
18
- import { getJA4 } from "@prosopo/provider";
19
- import {
20
- ClientApiPaths,
21
- type ProcaptchaOutput,
22
- type ProcaptchaToken,
23
- VerifySolutionBody,
24
- decodeProcaptchaOutput,
25
- } from "@prosopo/types";
26
- import type { VerifySolutionBodyTypeOutput } from "@prosopo/types";
27
- import express, { type RequestHandler, type Router } from "express";
28
- import { rateLimit } from "express-rate-limit";
29
- import { JA4Database, type JA4Store } from "./db.js";
30
- import { verifyProcaptchaOutput } from "./verify.js";
31
-
32
- /** Just enough of an express request for the handlers below. */
33
- export interface ApiRequest {
34
- body: unknown;
35
- headers: IncomingHttpHeaders;
36
- t: (key: string) => string;
37
- }
38
-
39
- /** Just enough of an express response for the handlers below. */
40
- export interface ApiResponse {
41
- json: (body: object) => unknown;
42
- status: (code: number) => { send: (body: string) => unknown };
43
- }
44
-
45
- export type ApiNext = (error: unknown) => void;
46
-
47
- export interface RouterDeps {
48
- db: JA4Store;
49
- getJA4: (
50
- headers: IncomingHttpHeaders,
51
- logger?: Logger,
52
- ) => Promise<{ ja4PlusFingerprint: string }>;
53
- decodeToken: (token: ProcaptchaToken) => ProcaptchaOutput;
54
- logger: Logger;
55
- }
56
-
57
- export const DEFAULT_MONGO_URL = "mongodb://localhost:27017";
58
- export const DEFAULT_MONGO_DBNAME = "client";
59
- export const DEFAULT_MONGO_AUTH_SOURCE = "admin";
60
-
61
- export const createDatabase = (
62
- env: NodeJS.ProcessEnv = process.env,
63
- ): JA4Database =>
64
- new JA4Database(
65
- env.MONGO_URL || DEFAULT_MONGO_URL,
66
- env.MONGO_DBNAME || DEFAULT_MONGO_DBNAME,
67
- env.MONGO_AUTH_SOURCE || DEFAULT_MONGO_AUTH_SOURCE,
68
- );
69
-
70
- export const defaultRouterDeps = (): RouterDeps => ({
71
- db: createDatabase(),
72
- getJA4,
73
- decodeToken: decodeProcaptchaOutput,
74
- logger: getLogger("info", "provider-mock:api"),
75
- });
76
-
77
- /**
78
- * Verify a solution, as the real provider would.
79
- *
80
- * A body that does not parse is the caller's fault and is reported as a 400; a
81
- * token that parses but cannot be decoded is reported as a 500, because at that
82
- * point the request looked well formed and the mock cannot tell the difference
83
- * between a corrupt token and a bug of its own.
84
- */
85
- export const createVerifyHandler =
86
- (deps: RouterDeps) =>
87
- async (req: ApiRequest, res: ApiResponse, next: ApiNext): Promise<void> => {
88
- let body: VerifySolutionBodyTypeOutput;
89
- try {
90
- body = VerifySolutionBody.parse(req.body);
91
- } catch (err) {
92
- return next(
93
- new ProsopoApiError("CAPTCHA.PARSE_ERROR", {
94
- context: { error: err, code: 400 },
95
- logLevel: "info",
96
- }),
97
- );
98
- }
99
- try {
100
- const outcome = verifyProcaptchaOutput(deps.decodeToken(body.token));
101
- res.json({
102
- status: req.t(outcome.statusMessage),
103
- verified: outcome.verified,
104
- ...(outcome.commitmentId === undefined
105
- ? {}
106
- : { commitmentId: outcome.commitmentId }),
107
- });
108
- } catch (err) {
109
- return next(
110
- new ProsopoApiError("API.UNKNOWN", {
111
- context: { error: err, code: 500 },
112
- }),
113
- );
114
- }
115
- };
116
-
117
- /** Record the caller's JA4 fingerprint and hand it back to them. */
118
- export const createTestHandler =
119
- (deps: RouterDeps) =>
120
- async (req: ApiRequest, res: ApiResponse): Promise<void> => {
121
- try {
122
- const { ja4PlusFingerprint } = await deps.getJA4(
123
- req.headers,
124
- deps.logger,
125
- );
126
- try {
127
- await deps.db.connect();
128
- await deps.db.addOrUpdateJA4Record({
129
- ja4_fingerprint: ja4PlusFingerprint,
130
- user_agent_string: req.headers["user-agent"] || "",
131
- });
132
- } finally {
133
- // The close used to be skipped whenever anything above it threw, so
134
- // every failing request leaked its connection.
135
- await deps.db.close();
136
- }
137
- res.json({
138
- ja4: ja4PlusFingerprint,
139
- ua: req.headers["user-agent"],
140
- });
141
- } catch (e) {
142
- // The block above fingerprints and then writes to mongo, so naming
143
- // ClientHello here reported a database outage as a parse failure.
144
- deps.logger.error(() => ({
145
- err: e instanceof Error ? e : new Error(String(e)),
146
- msg: "Failed to record the caller's JA4 fingerprint",
147
- }));
148
- res.status(500).send("Failed to record the caller's JA4 fingerprint.");
149
- }
150
- };
151
-
152
- /**
153
- * Both routes do unauthenticated work on the caller's behalf — one verifies a
154
- * token, the other writes a fingerprint to mongo — so an unthrottled client can
155
- * drive the database as fast as it can send. The window is generous enough that
156
- * the demos and cypress runs never see it.
157
- */
158
- export const verifyRateLimit = (): RequestHandler =>
159
- rateLimit({
160
- windowMs: 60_000,
161
- limit: 600,
162
- standardHeaders: true,
163
- legacyHeaders: false,
164
- });
165
-
166
- /**
167
- * Adapt a handler written against the narrow request/response types above to
168
- * the express signature. Express hands the same objects through untouched; the
169
- * narrow types exist only so tests can pass plain objects.
170
- */
171
- export const toRequestHandler = (
172
- handler: (req: ApiRequest, res: ApiResponse, next: ApiNext) => Promise<void>,
173
- ): RequestHandler => {
174
- const adapted: RequestHandler = (req, res, next): void => {
175
- // Express ignores a returned promise, so a rejection here would surface as
176
- // an unhandled rejection and the request would hang; hand it to the error
177
- // middleware instead.
178
- handler(
179
- req as unknown as ApiRequest,
180
- res as unknown as ApiResponse,
181
- next,
182
- ).catch(next);
183
- };
184
- return adapted;
185
- };
186
-
187
- /**
188
- * Returns a router connected to the database which can interact with the Proposo protocol
189
- *
190
- * @return {Router} - A middleware router that can interact with the Prosopo protocol
191
- */
192
- export function prosopoRouter(deps: RouterDeps = defaultRouterDeps()): Router {
193
- const router = express.Router();
194
-
195
- router.post(
196
- ClientApiPaths.VerifyImageCaptchaSolutionDapp,
197
- verifyRateLimit(),
198
- toRequestHandler(createVerifyHandler(deps)),
199
- );
200
-
201
- router.get(
202
- "/test",
203
- verifyRateLimit(),
204
- toRequestHandler(createTestHandler(deps)),
205
- );
206
-
207
- return router;
208
- }
package/src/db.ts DELETED
@@ -1,168 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { ProsopoDBError } from "@prosopo/common";
16
- import { MongoDatabase } from "@prosopo/database";
17
- import type { Logger } from "@prosopo/logger";
18
- import type { Tables } from "@prosopo/types-database";
19
- import type mongoose from "mongoose";
20
- import { Schema } from "mongoose";
21
-
22
- export enum TableNames {
23
- ja4 = "ja4",
24
- }
25
-
26
- export type JA4Data = {
27
- application?: string;
28
- library?: string;
29
- device?: string;
30
- os?: string;
31
- user_agent_string: string;
32
- certificate_authority?: string;
33
- observation_count?: number;
34
- verified?: boolean;
35
- notes?: string;
36
- ja4_fingerprint: string;
37
- ja4_fingerprint_string?: string;
38
- ja4s_fingerprint?: string;
39
- ja4h_fingerprint?: string;
40
- ja4x_fingerprint?: string;
41
- ja4t_fingerprint?: string;
42
- ja4ts_fingerprint?: string;
43
- ja4tscan_fingerprint?: string;
44
- };
45
-
46
- export type JA4Record = JA4Data & mongoose.Document;
47
-
48
- /**
49
- * The slice of the database the API depends on.
50
- *
51
- * Narrower than JA4Database so a caller — a test, or a different storage
52
- * backend — can supply one without a mongo connection.
53
- */
54
- export interface JA4Store {
55
- connect: () => Promise<void>;
56
- close: () => Promise<void>;
57
- addOrUpdateJA4Record: (record: JA4Data) => Promise<JA4Record | null>;
58
- }
59
-
60
- const JA4Schema = new Schema({
61
- application: String,
62
- library: String,
63
- device: String,
64
- os: String,
65
- user_agent_string: String,
66
- certificate_authority: String,
67
- observation_count: { type: Number, default: 1 },
68
- verified: { type: Boolean, default: false },
69
- notes: { type: String, default: "" },
70
- ja4_fingerprint: { type: String, required: true },
71
- ja4_fingerprint_string: String,
72
- ja4s_fingerprint: String,
73
- ja4h_fingerprint: String,
74
- ja4x_fingerprint: String,
75
- ja4t_fingerprint: String,
76
- ja4ts_fingerprint: String,
77
- ja4tscan_fingerprint: String,
78
- });
79
-
80
- JA4Schema.index({ ja4_fingerprint: 1, user_agent_string: 1 }, { unique: true });
81
-
82
- const DATA_TABLES = [
83
- {
84
- collectionName: TableNames.ja4,
85
- modelName: TableNames.ja4,
86
- schema: JA4Schema,
87
- },
88
- ];
89
-
90
- export class JA4Database extends MongoDatabase {
91
- tables: Tables<TableNames>;
92
-
93
- constructor(
94
- url: string,
95
- dbname?: string,
96
- authSource?: string,
97
- logger?: Logger,
98
- ) {
99
- super(url, dbname, authSource, logger);
100
- this.tables = {} as Tables<TableNames>;
101
- }
102
-
103
- override async connect(): Promise<void> {
104
- await super.connect();
105
- const connection = this.connection;
106
- if (connection === undefined) {
107
- // super.connect() resolves only once the connection is open, so this
108
- // cannot normally happen — but the models below would be read off
109
- // undefined if it ever did, which is a worse error to debug.
110
- throw new ProsopoDBError("DATABASE.CONNECTION_UNDEFINED", {
111
- context: { failedFuncName: this.connect.name },
112
- logger: this.logger,
113
- });
114
- }
115
- for (const { collectionName, modelName, schema } of DATA_TABLES) {
116
- this.tables[collectionName] = connection.model(modelName, schema);
117
- }
118
- }
119
-
120
- /**
121
- * The registered models.
122
- *
123
- * The check used to be `!this.tables`, which the constructor makes
124
- * impossible, so querying before connect() failed with a TypeError on
125
- * undefined rather than the database error it is.
126
- */
127
- getTables(): Tables<TableNames> {
128
- if (this.tables[TableNames.ja4] === undefined) {
129
- throw new ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
130
- context: { failedFuncName: this.getTables.name },
131
- logger: this.logger,
132
- });
133
- }
134
- return this.tables;
135
- }
136
-
137
- async getJA4Records(): Promise<JA4Record[]> {
138
- return this.getTables().ja4.find<JA4Record>({});
139
- }
140
-
141
- async getJA4RecordByFingerprintAndUserAgent(
142
- ja4Fingerprint: string,
143
- userAgentString: string,
144
- ): Promise<JA4Record | null> {
145
- return this.getTables().ja4.findOne<JA4Record>({
146
- ja4_fingerprint: ja4Fingerprint,
147
- user_agent_string: userAgentString,
148
- });
149
- }
150
-
151
- // add a ja4 record or update an existing one if the user agent string and ja4 fingerprint match, incrementing the observation count
152
-
153
- async addOrUpdateJA4Record(ja4Record: JA4Data): Promise<JA4Record | null> {
154
- const existingRecord = await this.getJA4RecordByFingerprintAndUserAgent(
155
- ja4Record.ja4_fingerprint,
156
- ja4Record.user_agent_string ? ja4Record.user_agent_string : "",
157
- );
158
- if (existingRecord) {
159
- existingRecord.observation_count =
160
- (existingRecord.observation_count || 0) + 1;
161
- await existingRecord.save();
162
- return existingRecord;
163
- }
164
- const newRecord = new (this.getTables().ja4)(ja4Record);
165
- await newRecord.save();
166
- return newRecord;
167
- }
168
- }
package/src/index.ts DELETED
@@ -1,18 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- export * from "./api.js";
16
- export * from "./db.js";
17
- export * from "./isMain.js";
18
- export * from "./verify.js";
package/src/isMain.ts DELETED
@@ -1,37 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { resolve } from "node:path";
16
- import { argv } from "node:process";
17
- import { fileURLToPath } from "node:url";
18
-
19
- /**
20
- * Whether the module at moduleUrl is the one node was asked to run.
21
- *
22
- * Guards the module's side effects so importing it — for its exports, or from a
23
- * test — does not start a server.
24
- */
25
- export const isMain = (
26
- moduleUrl: string,
27
- entrypoint: string | undefined = argv[1],
28
- ): boolean => {
29
- if (entrypoint === undefined) {
30
- return false;
31
- }
32
- // argv[1] is whatever was typed, so `node ./dist/start.js` gives a relative
33
- // path while the module url is always absolute. Resolve before comparing or
34
- // the guard is false for every relative entrypoint and the server never
35
- // starts.
36
- return fileURLToPath(moduleUrl) === resolve(entrypoint);
37
- };
package/src/start.ts DELETED
@@ -1,117 +0,0 @@
1
- // Copyright 2021-2026 Prosopo (UK) Ltd.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import { handleErrors } from "@prosopo/api-express-router";
16
- import { i18nMiddleware } from "@prosopo/locale";
17
- import { LogLevel, type Logger, getLogger } from "@prosopo/logger";
18
- import cors from "cors";
19
- import express, { type Express, type RequestHandler } from "express";
20
- import { type RouterDeps, prosopoRouter } from "./api.js";
21
- import { isMain } from "./isMain.js";
22
-
23
- export const DEFAULT_API_PORT = 9229;
24
-
25
- type Router = ReturnType<typeof prosopoRouter>;
26
-
27
- export interface StartDeps {
28
- createApp: () => Express;
29
- i18n: () => Promise<RequestHandler>;
30
- router: (deps?: RouterDeps) => Router;
31
- logger: Logger;
32
- port: number;
33
- exit: (code: number) => void;
34
- }
35
-
36
- export const defaultStartDeps = (
37
- env: NodeJS.ProcessEnv = process.env,
38
- ): StartDeps => ({
39
- createApp: express,
40
- i18n: () => i18nMiddleware({}),
41
- router: prosopoRouter,
42
- logger: getLogger(LogLevel.enum.info, "prosopo:provider-mock:start.ts"),
43
- port: readPort(env),
44
- exit: (code: number): void => {
45
- process.exit(code);
46
- },
47
- });
48
-
49
- /**
50
- * The port to listen on.
51
- *
52
- * The port used to be the string "9229" with no way to change it, so the mock
53
- * could not be run twice on one machine. A value that is not a usable port is
54
- * ignored rather than passed to listen, where it would either throw or — for a
55
- * number out of range — bind somewhere unexpected.
56
- */
57
- export const readPort = (env: NodeJS.ProcessEnv): number => {
58
- const raw = env.PROVIDER_MOCK_PORT;
59
- if (raw === undefined || raw.trim() === "") {
60
- return DEFAULT_API_PORT;
61
- }
62
- const port = Number(raw);
63
- if (!Number.isInteger(port) || port < 0 || port > 65535) {
64
- return DEFAULT_API_PORT;
65
- }
66
- return port;
67
- };
68
-
69
- export const startApi = async (
70
- deps: StartDeps = defaultStartDeps(),
71
- ): Promise<Express> => {
72
- const apiApp = deps.createApp();
73
-
74
- apiApp.use(cors());
75
- apiApp.use(express.json());
76
- apiApp.use(await deps.i18n());
77
- apiApp.use(deps.router());
78
- apiApp.use(handleErrors);
79
-
80
- // listen() reports a failed bind (EADDRINUSE and friends) on the server's
81
- // error event, not by throwing, so returning straight after calling it left
82
- // main()'s try/catch with nothing to catch. Settle on whichever event fires
83
- // first instead.
84
- const server = apiApp.listen(deps.port);
85
- await new Promise<void>((resolvePromise, rejectPromise) => {
86
- server.once("listening", () => {
87
- server.removeListener("error", rejectPromise);
88
- deps.logger.info(() => ({
89
- msg: `Prosopo app listening at http://localhost:${deps.port}`,
90
- }));
91
- resolvePromise();
92
- });
93
- server.once("error", rejectPromise);
94
- });
95
-
96
- return apiApp;
97
- };
98
-
99
- export const main = async (
100
- deps: StartDeps = defaultStartDeps(),
101
- ): Promise<void> => {
102
- try {
103
- await startApi(deps);
104
- } catch (error) {
105
- deps.logger.error(() => ({
106
- err: error instanceof Error ? error : new Error(String(error)),
107
- msg: "Failed to start API",
108
- }));
109
- deps.exit(1);
110
- }
111
- };
112
-
113
- if (isMain(import.meta.url)) {
114
- // Not awaited: a top-level await cannot be emitted in the cjs build, and
115
- // main() already handles its own failures rather than rejecting.
116
- void main();
117
- }