@prosopo/client-example-server 2.7.122 → 2.7.124
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/.turbo/turbo-build$colon$cjs.log +15 -31
- package/.turbo/turbo-build$colon$tsc.log +21 -21
- package/.turbo/turbo-build.log +16 -31
- package/CHANGELOG.md +35 -0
- package/dist/_virtual/_rolldown/runtime.js +3 -0
- package/dist/app.d.ts +15 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +122 -93
- package/dist/app.js.map +1 -1
- package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/cjs/app.cjs +148 -106
- package/dist/cjs/controllers/auth.cjs +180 -244
- package/dist/cjs/models/user.cjs +21 -22
- package/dist/cjs/routes/routes.cjs +21 -24
- package/dist/cjs/utils/connection.cjs +20 -23
- package/dist/cjs/utils/database.cjs +3 -28
- package/dist/controllers/auth.d.ts +1 -1
- package/dist/controllers/auth.d.ts.map +1 -1
- package/dist/controllers/auth.js +173 -241
- package/dist/controllers/auth.js.map +1 -1
- package/dist/models/user.d.ts +2 -2
- package/dist/models/user.js +18 -22
- package/dist/routes/routes.d.ts.map +1 -1
- package/dist/routes/routes.js +18 -24
- package/dist/routes/routes.js.map +1 -1
- package/dist/tests/app.unit.test.d.ts +2 -0
- package/dist/tests/app.unit.test.d.ts.map +1 -0
- package/dist/tests/app.unit.test.js +318 -0
- package/dist/tests/app.unit.test.js.map +1 -0
- package/dist/tests/appEntrypoint.unit.test.d.ts +2 -0
- package/dist/tests/appEntrypoint.unit.test.d.ts.map +1 -0
- package/dist/tests/appEntrypoint.unit.test.js +100 -0
- package/dist/tests/appEntrypoint.unit.test.js.map +1 -0
- package/dist/tests/auth.unit.test.d.ts +2 -0
- package/dist/tests/auth.unit.test.d.ts.map +1 -0
- package/dist/tests/auth.unit.test.js +426 -0
- package/dist/tests/auth.unit.test.js.map +1 -0
- package/dist/tests/authHarness.d.ts +40 -0
- package/dist/tests/authHarness.d.ts.map +1 -0
- package/dist/tests/authHarness.js +57 -0
- package/dist/tests/authHarness.js.map +1 -0
- package/dist/tests/clientExampleServer.test-d.d.ts +2 -0
- package/dist/tests/clientExampleServer.test-d.d.ts.map +1 -0
- package/dist/tests/clientExampleServer.test-d.js +84 -0
- package/dist/tests/clientExampleServer.test-d.js.map +1 -0
- package/dist/tests/routes.unit.test.d.ts +2 -0
- package/dist/tests/routes.unit.test.d.ts.map +1 -0
- package/dist/tests/routes.unit.test.js +123 -0
- package/dist/tests/routes.unit.test.js.map +1 -0
- package/dist/tests/utils.unit.test.d.ts +2 -0
- package/dist/tests/utils.unit.test.d.ts.map +1 -0
- package/dist/tests/utils.unit.test.js +125 -0
- package/dist/tests/utils.unit.test.js.map +1 -0
- package/dist/utils/connection.js +14 -19
- package/dist/utils/database.js +4 -8
- package/package.json +17 -14
- package/src/app.ts +114 -73
- package/src/controllers/auth.ts +40 -31
- package/src/routes/routes.ts +5 -2
- package/src/tests/app.unit.test.ts +428 -0
- package/src/tests/appEntrypoint.unit.test.ts +154 -0
- package/src/tests/auth.unit.test.ts +859 -0
- package/src/tests/authHarness.ts +131 -0
- package/src/tests/clientExampleServer.test-d.ts +146 -0
- package/src/tests/routes.unit.test.ts +166 -0
- package/src/tests/utils.unit.test.ts +169 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.test.config.ts +18 -0
package/src/app.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import fs from "node:fs";
|
|
16
16
|
import http from "node:http";
|
|
17
|
+
import type { Server } from "node:http";
|
|
17
18
|
import https from "node:https";
|
|
18
19
|
import type { ServerOptions } from "node:https";
|
|
19
20
|
import path from "node:path";
|
|
@@ -22,41 +23,50 @@ import { ProsopoEnvError } from "@prosopo/common";
|
|
|
22
23
|
import { loadEnv } from "@prosopo/dotenv";
|
|
23
24
|
import { getLogger } from "@prosopo/logger";
|
|
24
25
|
import { getServerConfig } from "@prosopo/server";
|
|
25
|
-
import {
|
|
26
|
+
import type { ProsopoServerConfigOutput } from "@prosopo/types";
|
|
27
|
+
import { at, isMain } from "@prosopo/util";
|
|
26
28
|
import cors from "cors";
|
|
27
29
|
import express from "express";
|
|
28
30
|
import routesFactory from "./routes/routes.js";
|
|
29
31
|
import connectionFactory from "./utils/connection.js";
|
|
30
32
|
import memoryServerSetup from "./utils/database.js";
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
enum ProsopoVerificationType {
|
|
34
|
+
export enum ProsopoVerificationType {
|
|
35
35
|
api = "api",
|
|
36
36
|
local = "local",
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const logger = getLogger("info", "client-example-server:app");
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The siteverify endpoint the deployment talks to. Non-production
|
|
43
|
+
* environments get their name as a subdomain prefix. An unset NODE_ENV is
|
|
44
|
+
* treated as production: prefixing it produced `undefined-api.prosopo.io`,
|
|
45
|
+
* a host that does not exist.
|
|
46
|
+
*/
|
|
47
|
+
export const resolveVerifyEndpoint = (): string => {
|
|
48
|
+
const environment = process.env.NODE_ENV;
|
|
44
49
|
const apiPrefix =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: `${process.env.NODE_ENV}-`;
|
|
48
|
-
const verifyEndpoint =
|
|
50
|
+
!environment || environment === "production" ? "" : `${environment}-`;
|
|
51
|
+
return (
|
|
49
52
|
process.env.PROSOPO_VERIFY_ENDPOINT ||
|
|
50
|
-
`https://${apiPrefix}api.prosopo.io/siteverify
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
).includes(
|
|
53
|
+
`https://${apiPrefix}api.prosopo.io/siteverify`
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Anything the enum doesn't name falls back to the hosted API. */
|
|
58
|
+
export const resolveVerifyType = (): ProsopoVerificationType =>
|
|
59
|
+
Object.keys(ProsopoVerificationType).includes(
|
|
60
|
+
process.env.PROSOPO_VERIFICATION_TYPE as string,
|
|
61
|
+
)
|
|
57
62
|
? (process.env.PROSOPO_VERIFICATION_TYPE as ProsopoVerificationType)
|
|
58
63
|
: ProsopoVerificationType.api;
|
|
59
64
|
|
|
65
|
+
export const isDevelopmentOrTest = (): boolean =>
|
|
66
|
+
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
67
|
+
|
|
68
|
+
/** The express app, minus the routes, which need a database connection. */
|
|
69
|
+
export const createApp = (): express.Express => {
|
|
60
70
|
const app = express();
|
|
61
71
|
|
|
62
72
|
// https://express-rate-limit.mintlify.app/guides/troubleshooting-proxy-issues
|
|
@@ -81,15 +91,79 @@ async function main() {
|
|
|
81
91
|
next();
|
|
82
92
|
});
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
// No explicit OPTIONS handler: cors() above already answers preflight
|
|
95
|
+
// requests with a 204 and never calls through, so the handler that used to
|
|
96
|
+
// sit here could not run.
|
|
97
|
+
|
|
98
|
+
return app;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const DEFAULT_PORT = 9228;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The port is the last segment of the configured server URL. A URL without
|
|
105
|
+
* one used to yield NaN, which node turns into an arbitrary free port — the
|
|
106
|
+
* server came up on an address nothing was configured to talk to.
|
|
107
|
+
*/
|
|
108
|
+
export const resolvePort = (config: ProsopoServerConfigOutput): number => {
|
|
109
|
+
if (!config.serverUrl) return DEFAULT_PORT;
|
|
110
|
+
const segments = config.serverUrl.split(":");
|
|
111
|
+
if (segments.length < 3) return DEFAULT_PORT;
|
|
112
|
+
const port = Number.parseInt(at(segments, 2));
|
|
113
|
+
return Number.isNaN(port) ? DEFAULT_PORT : port;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* HTTPS in development/test when the repo's certificates exist, plain HTTP
|
|
118
|
+
* otherwise — in production Caddy terminates TLS.
|
|
119
|
+
*/
|
|
120
|
+
export const startServer = (
|
|
121
|
+
app: express.Express,
|
|
122
|
+
port: number,
|
|
123
|
+
dirname: string = path.dirname(fileURLToPath(import.meta.url)),
|
|
124
|
+
): Server => {
|
|
125
|
+
if (isDevelopmentOrTest()) {
|
|
126
|
+
const certsDir = path.resolve(dirname, "../../../certs");
|
|
127
|
+
|
|
128
|
+
const keyPath = path.join(certsDir, "server.key");
|
|
129
|
+
const certPath = path.join(certsDir, "server.crt");
|
|
130
|
+
|
|
131
|
+
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
132
|
+
const httpsOptions: ServerOptions = {
|
|
133
|
+
key: fs.readFileSync(keyPath),
|
|
134
|
+
cert: fs.readFileSync(certPath),
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return https.createServer(httpsOptions, app).listen(port, () => {
|
|
138
|
+
logger.info(() => ({ msg: `HTTPS server started on port ${port}` }));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
logger.warn(() => ({
|
|
142
|
+
msg: "Certificates not found, starting HTTP server instead. Run ./setup_certs.sh to enable HTTPS in development.",
|
|
143
|
+
}));
|
|
144
|
+
return http.createServer(app).listen(port, () => {
|
|
145
|
+
logger.info(() => ({ msg: `HTTP server started on port ${port}` }));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return http.createServer(app).listen(port, () => {
|
|
149
|
+
logger.info(() => ({
|
|
150
|
+
msg: `HTTP server started on port ${port} (TLS handled by reverse proxy)`,
|
|
151
|
+
}));
|
|
86
152
|
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export async function main(): Promise<Server> {
|
|
156
|
+
loadEnv();
|
|
87
157
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
158
|
+
const verifyEndpoint = resolveVerifyEndpoint();
|
|
159
|
+
|
|
160
|
+
logger.info(() => ({ data: { verifyEndpoint } }));
|
|
161
|
+
|
|
162
|
+
const verifyType = resolveVerifyType();
|
|
163
|
+
|
|
164
|
+
const app = createApp();
|
|
165
|
+
|
|
166
|
+
if (!process.env.MONGO_URI && !isDevelopmentOrTest()) {
|
|
93
167
|
throw new Error(
|
|
94
168
|
"Cannot run mongo memory when NODE_ENV is neither development nor test",
|
|
95
169
|
);
|
|
@@ -113,57 +187,24 @@ async function main() {
|
|
|
113
187
|
|
|
114
188
|
app.use(routesFactory(mongoose, config, verifyEndpoint, verifyType));
|
|
115
189
|
|
|
116
|
-
const port = config
|
|
117
|
-
? Number.parseInt(at(config.serverUrl.split(":"), 2))
|
|
118
|
-
: 9228;
|
|
190
|
+
const port = resolvePort(config);
|
|
119
191
|
|
|
120
192
|
logger.info(() => ({ msg: "Listening on port", data: { port } }));
|
|
121
193
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const isDev =
|
|
125
|
-
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
126
|
-
|
|
127
|
-
if (isDev) {
|
|
128
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
129
|
-
const __dirname = path.dirname(__filename);
|
|
130
|
-
const certsDir = path.resolve(__dirname, "../../../certs");
|
|
131
|
-
|
|
132
|
-
const keyPath = path.join(certsDir, "server.key");
|
|
133
|
-
const certPath = path.join(certsDir, "server.crt");
|
|
134
|
-
|
|
135
|
-
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
136
|
-
const httpsOptions: ServerOptions = {
|
|
137
|
-
key: fs.readFileSync(keyPath),
|
|
138
|
-
cert: fs.readFileSync(certPath),
|
|
139
|
-
};
|
|
194
|
+
return startServer(app, port);
|
|
195
|
+
}
|
|
140
196
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Production: use plain HTTP, Caddy handles TLS
|
|
154
|
-
http.createServer(app).listen(port, () => {
|
|
155
|
-
logger.info(() => ({
|
|
156
|
-
msg: `HTTP server started on port ${port} (TLS handled by reverse proxy)`,
|
|
157
|
-
}));
|
|
197
|
+
// Only boot when run as a binary: importing this module (a test, or another
|
|
198
|
+
// entrypoint reusing createApp) must not open a port or spin up mongo.
|
|
199
|
+
if (isMain(import.meta.url)) {
|
|
200
|
+
main()
|
|
201
|
+
.then(() => {
|
|
202
|
+
logger.info(() => ({ msg: "Server started" }));
|
|
203
|
+
})
|
|
204
|
+
.catch((err) => {
|
|
205
|
+
logger.error(() => ({ err }));
|
|
206
|
+
// Non-zero, so a boot failure is visible to whatever supervises the
|
|
207
|
+
// process rather than reading as a clean shutdown.
|
|
208
|
+
process.exit(1);
|
|
158
209
|
});
|
|
159
|
-
}
|
|
160
210
|
}
|
|
161
|
-
|
|
162
|
-
main()
|
|
163
|
-
.then(() => {
|
|
164
|
-
logger.info(() => ({ msg: "Server started" }));
|
|
165
|
-
})
|
|
166
|
-
.catch((err) => {
|
|
167
|
-
logger.error(() => ({ err }));
|
|
168
|
-
process.exit();
|
|
169
|
-
});
|
package/src/controllers/auth.ts
CHANGED
|
@@ -85,9 +85,6 @@ const getPairAndSecretForSiteKey = (
|
|
|
85
85
|
]) {
|
|
86
86
|
const newSecret = `${baseSecret}//${captchaType}`;
|
|
87
87
|
pair = getPair(newSecret);
|
|
88
|
-
console.log(
|
|
89
|
-
`[PAIR CHECK] Checking pair for ${captchaType}: ${pair?.address} (expected: ${siteKey})`,
|
|
90
|
-
);
|
|
91
88
|
logger.info(() => ({
|
|
92
89
|
msg: "Checking pair",
|
|
93
90
|
data: {
|
|
@@ -97,7 +94,6 @@ const getPairAndSecretForSiteKey = (
|
|
|
97
94
|
},
|
|
98
95
|
}));
|
|
99
96
|
if (pair.address === siteKey) {
|
|
100
|
-
console.log(`[PAIR CHECK] ✅ Match found for ${captchaType}!`);
|
|
101
97
|
secret = newSecret;
|
|
102
98
|
break;
|
|
103
99
|
}
|
|
@@ -236,29 +232,29 @@ const signup = async (
|
|
|
236
232
|
const salt = randomAsHex(32);
|
|
237
233
|
// !!!DUMMY CODE!!! - Do not use in production. Use bcrypt or similar for password hashing.
|
|
238
234
|
const passwordHash = hashPassword(`${req.body.password}${salt}`);
|
|
239
|
-
if (passwordHash)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
// No `if (passwordHash)` guard: the hash is always a non-empty hex
|
|
236
|
+
// string, and the guard's absent else-branch left the request hanging
|
|
237
|
+
// with no response at all.
|
|
238
|
+
return User.create({
|
|
239
|
+
email: email,
|
|
240
|
+
name: req.body.name,
|
|
241
|
+
password: passwordHash,
|
|
242
|
+
salt: salt,
|
|
243
|
+
})
|
|
244
|
+
.then(() => {
|
|
245
|
+
res.status(200).json({ message: "user created" });
|
|
245
246
|
})
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}));
|
|
254
|
-
res.status(502).json({ message: "error while creating the user" });
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
} else {
|
|
258
|
-
res
|
|
259
|
-
.status(401)
|
|
260
|
-
.json({ message: "user has not completed a captcha", verified });
|
|
247
|
+
.catch((err) => {
|
|
248
|
+
logger.error(() => ({
|
|
249
|
+
err,
|
|
250
|
+
msg: "Error creating user in database",
|
|
251
|
+
}));
|
|
252
|
+
res.status(502).json({ message: "error while creating the user" });
|
|
253
|
+
});
|
|
261
254
|
}
|
|
255
|
+
res
|
|
256
|
+
.status(401)
|
|
257
|
+
.json({ message: "user has not completed a captcha", verified });
|
|
262
258
|
} catch (err) {
|
|
263
259
|
console.error("error", err);
|
|
264
260
|
res
|
|
@@ -347,25 +343,38 @@ const login = async (
|
|
|
347
343
|
|
|
348
344
|
const isAuth = (req: Request, res: Response) => {
|
|
349
345
|
const authHeader = req.get("Authorization") || "";
|
|
346
|
+
// Each branch returns: without the returns a missing header still fell
|
|
347
|
+
// through to `at(headerParts, 1)`, which throws on a one-element array, and
|
|
348
|
+
// a failed verify sent a 500 followed by a 401 on the same response.
|
|
350
349
|
if (!authHeader) {
|
|
351
|
-
res.status(401).json({ message: "not authenticated" });
|
|
350
|
+
return res.status(401).json({ message: "not authenticated" });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const headerParts = authHeader.split(" ");
|
|
354
|
+
if (headerParts.length < 2) {
|
|
355
|
+
return res.status(401).json({ message: "not authenticated" });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const token = at(headerParts, 1);
|
|
359
|
+
// "Bearer " with nothing after it is a malformed credential, not a server
|
|
360
|
+
// fault: jwt.verify("") throws and would otherwise surface as a 500.
|
|
361
|
+
if (!token) {
|
|
362
|
+
return res.status(401).json({ message: "not authenticated" });
|
|
352
363
|
}
|
|
353
364
|
|
|
354
|
-
const token = at(authHeader.split(" "), 1);
|
|
355
365
|
let decodedToken: string | JwtPayload = "";
|
|
356
366
|
try {
|
|
357
367
|
decodedToken = jwt.verify(token, "secret");
|
|
358
368
|
} catch (err) {
|
|
359
|
-
res.status(500).json({
|
|
369
|
+
return res.status(500).json({
|
|
360
370
|
message: (err as Error).message || "could not decode the token",
|
|
361
371
|
});
|
|
362
372
|
}
|
|
363
373
|
|
|
364
374
|
if (!decodedToken) {
|
|
365
|
-
res.status(401).json({ message: "unauthorized" });
|
|
366
|
-
} else {
|
|
367
|
-
res.status(200).json({ message: "here is your resource" });
|
|
375
|
+
return res.status(401).json({ message: "unauthorized" });
|
|
368
376
|
}
|
|
377
|
+
return res.status(200).json({ message: "here is your resource" });
|
|
369
378
|
};
|
|
370
379
|
|
|
371
380
|
export { signup, login, isAuth };
|
package/src/routes/routes.ts
CHANGED
|
@@ -16,14 +16,17 @@ import express from "express";
|
|
|
16
16
|
import type { Connection } from "mongoose";
|
|
17
17
|
import { isAuth, login, signup } from "../controllers/auth.js";
|
|
18
18
|
|
|
19
|
-
const router = express.Router();
|
|
20
|
-
|
|
21
19
|
function getRoutes(
|
|
22
20
|
mongoose: Connection,
|
|
23
21
|
config: ProsopoServerConfigOutput,
|
|
24
22
|
verifyEndpoint: string,
|
|
25
23
|
verifyType: string,
|
|
26
24
|
): express.Router {
|
|
25
|
+
// A router per call: the module-level singleton this used to share meant a
|
|
26
|
+
// second getRoutes() stacked another set of handlers onto the same router,
|
|
27
|
+
// so every request ran the first call's bound config.
|
|
28
|
+
const router = express.Router();
|
|
29
|
+
|
|
27
30
|
router.post(
|
|
28
31
|
"/login",
|
|
29
32
|
login.bind(null, mongoose, config, verifyEndpoint, verifyType),
|