@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.
Files changed (68) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +15 -31
  2. package/.turbo/turbo-build$colon$tsc.log +21 -21
  3. package/.turbo/turbo-build.log +16 -31
  4. package/CHANGELOG.md +35 -0
  5. package/dist/_virtual/_rolldown/runtime.js +3 -0
  6. package/dist/app.d.ts +15 -1
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +122 -93
  9. package/dist/app.js.map +1 -1
  10. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  11. package/dist/cjs/app.cjs +148 -106
  12. package/dist/cjs/controllers/auth.cjs +180 -244
  13. package/dist/cjs/models/user.cjs +21 -22
  14. package/dist/cjs/routes/routes.cjs +21 -24
  15. package/dist/cjs/utils/connection.cjs +20 -23
  16. package/dist/cjs/utils/database.cjs +3 -28
  17. package/dist/controllers/auth.d.ts +1 -1
  18. package/dist/controllers/auth.d.ts.map +1 -1
  19. package/dist/controllers/auth.js +173 -241
  20. package/dist/controllers/auth.js.map +1 -1
  21. package/dist/models/user.d.ts +2 -2
  22. package/dist/models/user.js +18 -22
  23. package/dist/routes/routes.d.ts.map +1 -1
  24. package/dist/routes/routes.js +18 -24
  25. package/dist/routes/routes.js.map +1 -1
  26. package/dist/tests/app.unit.test.d.ts +2 -0
  27. package/dist/tests/app.unit.test.d.ts.map +1 -0
  28. package/dist/tests/app.unit.test.js +318 -0
  29. package/dist/tests/app.unit.test.js.map +1 -0
  30. package/dist/tests/appEntrypoint.unit.test.d.ts +2 -0
  31. package/dist/tests/appEntrypoint.unit.test.d.ts.map +1 -0
  32. package/dist/tests/appEntrypoint.unit.test.js +100 -0
  33. package/dist/tests/appEntrypoint.unit.test.js.map +1 -0
  34. package/dist/tests/auth.unit.test.d.ts +2 -0
  35. package/dist/tests/auth.unit.test.d.ts.map +1 -0
  36. package/dist/tests/auth.unit.test.js +426 -0
  37. package/dist/tests/auth.unit.test.js.map +1 -0
  38. package/dist/tests/authHarness.d.ts +40 -0
  39. package/dist/tests/authHarness.d.ts.map +1 -0
  40. package/dist/tests/authHarness.js +57 -0
  41. package/dist/tests/authHarness.js.map +1 -0
  42. package/dist/tests/clientExampleServer.test-d.d.ts +2 -0
  43. package/dist/tests/clientExampleServer.test-d.d.ts.map +1 -0
  44. package/dist/tests/clientExampleServer.test-d.js +84 -0
  45. package/dist/tests/clientExampleServer.test-d.js.map +1 -0
  46. package/dist/tests/routes.unit.test.d.ts +2 -0
  47. package/dist/tests/routes.unit.test.d.ts.map +1 -0
  48. package/dist/tests/routes.unit.test.js +123 -0
  49. package/dist/tests/routes.unit.test.js.map +1 -0
  50. package/dist/tests/utils.unit.test.d.ts +2 -0
  51. package/dist/tests/utils.unit.test.d.ts.map +1 -0
  52. package/dist/tests/utils.unit.test.js +125 -0
  53. package/dist/tests/utils.unit.test.js.map +1 -0
  54. package/dist/utils/connection.js +14 -19
  55. package/dist/utils/database.js +4 -8
  56. package/package.json +17 -14
  57. package/src/app.ts +114 -73
  58. package/src/controllers/auth.ts +40 -31
  59. package/src/routes/routes.ts +5 -2
  60. package/src/tests/app.unit.test.ts +428 -0
  61. package/src/tests/appEntrypoint.unit.test.ts +154 -0
  62. package/src/tests/auth.unit.test.ts +859 -0
  63. package/src/tests/authHarness.ts +131 -0
  64. package/src/tests/clientExampleServer.test-d.ts +146 -0
  65. package/src/tests/routes.unit.test.ts +166 -0
  66. package/src/tests/utils.unit.test.ts +169 -0
  67. package/tsconfig.tsbuildinfo +1 -1
  68. 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 { at } from "@prosopo/util";
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
- loadEnv();
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
- async function main() {
42
- loadEnv();
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
- process.env.NODE_ENV && process.env.NODE_ENV === "production"
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
- logger.info(() => ({ data: { verifyEndpoint } }));
53
-
54
- const verifyType: ProsopoVerificationType = Object.keys(
55
- ProsopoVerificationType,
56
- ).includes(process.env.PROSOPO_VERIFICATION_TYPE as string)
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
- app.options("/*", (_, res) => {
85
- res.sendStatus(200);
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
- if (
89
- !process.env.MONGO_URI &&
90
- process.env.NODE_ENV !== "development" &&
91
- process.env.NODE_ENV !== "test"
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.serverUrl
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
- // Use HTTPS only in development/test when we have certificates
123
- // In production, use HTTP because Caddy handles TLS termination
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
- https.createServer(httpsOptions, app).listen(port, () => {
142
- logger.info(() => ({ msg: `HTTPS server started on port ${port}` }));
143
- });
144
- } else {
145
- logger.warn(() => ({
146
- msg: "Certificates not found, starting HTTP server instead. Run ./setup_certs.sh to enable HTTPS in development.",
147
- }));
148
- http.createServer(app).listen(port, () => {
149
- logger.info(() => ({ msg: `HTTP server started on port ${port}` }));
150
- });
151
- }
152
- } else {
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
- });
@@ -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
- return User.create({
241
- email: email,
242
- name: req.body.name,
243
- password: passwordHash,
244
- salt: salt,
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
- .then(() => {
247
- res.status(200).json({ message: "user created" });
248
- })
249
- .catch((err) => {
250
- logger.error(() => ({
251
- err,
252
- msg: "Error creating user in database",
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 };
@@ -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),